Skip to main content
AdCP uses a consistent error handling approach across all operations. Understanding error categories and implementing proper recovery strategies is essential for building robust integrations.

Compliance Levels

Sellers can adopt error handling incrementally. Each level builds on the previous:
LevelWhat to implementWhat agents can do
Level 1Return code + message on every errorAgents match on error code to classify failures
Level 2Add recovery, retry_after, field, and suggestionAgents auto-retry transient errors and self-correct correctable ones
Level 3Use transport bindings to put errors in structuredContent (MCP) or artifact DataPart (A2A)Programmatic clients get typed errors without parsing text
Level 1 is the minimum for a conformant implementation. Level 2 is where agent-driven recovery becomes possible — without recovery, agents must guess from the error code. Level 3 is where client libraries like @adcp/client can provide fully typed error objects.

Error Categories

1. Protocol Errors

Transport/connection issues not related to AdCP business logic:
  • Network timeouts
  • Connection refused
  • TLS/SSL errors
  • JSON parsing errors
Handling: Retry with exponential backoff.

2. Task Errors

Business logic failures returned as status: "failed":
  • Insufficient inventory
  • Invalid targeting
  • Budget validation failures
  • Resource not found
Handling: Check the recovery field to determine whether to retry, fix the request, or escalate.

3. Validation Errors

Malformed requests that fail schema validation:
  • Missing required fields
  • Invalid field types
  • Out-of-range values
Handling: Fix request format and retry. Usually development-time issues.

Error Response Format

Failed operations return status failed with error details. The error object follows the error.json schema:
{
  "status": "failed",
  "message": "Budget is below the seller's minimum for this product",
  "errors": [
    {
      "code": "BUDGET_TOO_LOW",
      "message": "Budget is below the seller's minimum for this product",
      "recovery": "correctable",
      "field": "budget.total",
      "suggestion": "Increase budget to at least 500 USD",
      "details": {
        "minimum_budget": 500,
        "currency": "USD"
      }
    }
  ]
}

Error Object Fields

These fields are defined by the error.json schema:
FieldTypeRequiredDescription
codestringYesMachine-readable error code from the standard vocabulary or a seller-specific code
messagestringYesHuman-readable error description
recoverystringNoAgent recovery classification: transient, correctable, or terminal
retry_afternumberNoSeconds to wait before retrying (transient errors)
fieldstringNoField path that caused the error (e.g., packages[0].targeting)
suggestionstringNoSuggested fix for the error
detailsobjectNoAdditional context-specific information

Standard Error Codes

AdCP defines 20 standard error codes in error-code.json. Sellers MAY use codes not in this vocabulary for platform-specific errors. Agents MUST handle unknown codes by falling back to the recovery classification.

Authentication and Access

CodeRecoveryDescriptionResolution
AUTH_REQUIREDcorrectableAuthentication is required or credentials are invalidProvide credentials via auth header
ACCOUNT_NOT_FOUNDterminalAccount reference could not be resolvedVerify via list_accounts or contact seller
ACCOUNT_SETUP_REQUIREDcorrectableAccount needs setup before useCheck details.setup for URL or instructions
ACCOUNT_AMBIGUOUScorrectableNatural key resolves to multiple accountsPass explicit account_id or a more specific natural key
ACCOUNT_PAYMENT_REQUIREDterminalOutstanding balance requires paymentBuyer must resolve billing
ACCOUNT_SUSPENDEDterminalAccount has been suspendedContact seller to resolve

Request Validation

CodeRecoveryDescriptionResolution
INVALID_REQUESTcorrectableRequest is malformed or violates schema constraintsCheck request parameters and fix
UNSUPPORTED_FEATUREcorrectableRequested feature not supported by this sellerCheck get_adcp_capabilities and remove unsupported fields
POLICY_VIOLATIONcorrectableRequest violates content or advertising policiesReview policy requirements in the error details
COMPLIANCE_UNSATISFIEDcorrectableRequired disclosure cannot be satisfied by the target formatChoose a format that supports the required disclosure capabilities

Inventory and Products

CodeRecoveryDescriptionResolution
PRODUCT_NOT_FOUNDcorrectableReferenced product IDs are unknown or expiredRemove invalid IDs, or re-discover with get_products
PRODUCT_UNAVAILABLEcorrectableProduct is sold out or no longer availableChoose a different product
PROPOSAL_EXPIREDcorrectableReferenced proposal has passed its expires_atRun get_products to get a fresh proposal
AUDIENCE_TOO_SMALLcorrectableAudience segment below minimum sizeBroaden targeting or upload more audience members

Budget and Creative

CodeRecoveryDescriptionResolution
BUDGET_TOO_LOWcorrectableBudget below seller’s minimumIncrease budget or check capabilities.media_buy.limits
BUDGET_EXHAUSTEDterminalAccount or campaign budget fully spentBuyer must add funds or increase budget cap
CREATIVE_REJECTEDcorrectableCreative failed content policy reviewRevise per seller’s advertising_policies

System

CodeRecoveryDescriptionResolution
RATE_LIMITEDtransientRequest rate exceededWait for retry_after seconds, then retry
SERVICE_UNAVAILABLEtransientSeller service temporarily unavailableRetry with exponential backoff
CONFLICTtransientConcurrent modification detectedRe-read the resource and retry with current state

Recovery Classification

Use the recovery field to determine how to handle errors:
RecoveryMeaningAction
transientTemporary failure (rate limit, service unavailable, conflict)Retry after retry_after or with exponential backoff
correctableRequest can be fixed and resent (invalid field, budget too low, creative rejected)Modify the request and retry
terminalRequires human action (account suspended, payment required)Escalate to a human operator
For unknown recovery values (forward compatibility), treat as terminal.
function isRetryable(error) {
  // Use recovery field when available
  if (error.recovery) {
    return error.recovery === 'transient';
  }

  // Network errors are retryable
  if (error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT') {
    return true;
  }

  // Fall back to error code matching
  return ['RATE_LIMITED', 'SERVICE_UNAVAILABLE', 'CONFLICT'].includes(error.code);
}

Retry Logic

Exponential Backoff

Implement exponential backoff for retryable errors:
async function retryWithBackoff(fn, options = {}) {
  const {
    maxRetries = 3,
    baseDelay = 1000,
    maxDelay = 60000
  } = options;

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      if (!isRetryable(error) || attempt === maxRetries) {
        throw error;
      }

      // Use retry_after when available, otherwise exponential backoff
      const retryAfter = error.retry_after ||
        Math.min(baseDelay * Math.pow(2, attempt), maxDelay);

      // Add jitter to prevent thundering herd
      const jitter = retryAfter * (0.75 + Math.random() * 0.5);
      await sleep(jitter);
    }
  }
}

Rate Limit Handling

async function handleRateLimit(error, retryFn) {
  if (error.recovery !== 'transient' &&
      error.code !== 'RATE_LIMITED') {
    throw error;
  }

  const retryAfter = error.retry_after || 60;
  console.log(`Rate limited. Waiting ${retryAfter} seconds...`);

  await sleep(retryAfter * 1000);
  return retryFn();
}

Error Handling Patterns

Basic Error Handler

async function handleAdcpError(error) {
  // Use recovery classification when available
  switch (error.recovery) {
    case 'transient':
      const delay = error.retry_after
        ? error.retry_after * 1000
        : 5000;
      await sleep(delay);
      return retry();

    case 'correctable':
      // Surface suggestion so the request can be fixed
      if (error.suggestion) {
        console.log('Suggestion:', error.suggestion);
      }
      if (error.field) {
        console.log('Problem field:', error.field);
      }
      throw error;

    case 'terminal':
      console.error('Terminal error:', error.message);
      throw error;
  }

  // Fall back to error code matching
  switch (error.code) {
    case 'AUTH_REQUIRED':
      await refreshCredentials();
      return retry();

    case 'INVALID_REQUEST':
      console.error('Validation error:', error);
      throw error;

    default:
      console.error('AdCP error:', error);
      throw error;
  }
}

User-Friendly Messages

Convert technical errors to user-friendly messages:
const USER_MESSAGES = {
  'RATE_LIMITED': 'Too many requests. Please wait a moment and try again.',
  'BUDGET_TOO_LOW': 'This is below the seller\'s minimum budget. Increase your budget.',
  'PRODUCT_NOT_FOUND': 'One or more products could not be found. Try searching again.',
  'ACCOUNT_SUSPENDED': 'Your account has been suspended. Contact the seller to resolve.',
  'SERVICE_UNAVAILABLE': 'The service is temporarily unavailable. Please try again in a few minutes.',
  'CREATIVE_REJECTED': 'Your creative did not pass policy review. Check the suggestion for details.',
  'AUDIENCE_TOO_SMALL': 'Your target audience is too small. Try broadening your targeting.'
};

function getUserMessage(code, fallbackMessage) {
  return USER_MESSAGES[code] || fallbackMessage || 'An unexpected error occurred. Please try again.';
}

Structured Error Logging

Log errors with context for debugging:
function logError(error, context = {}) {
  console.error('AdCP Error:', {
    code: error.code,
    recovery: error.recovery,
    message: error.message,
    field: error.field,
    timestamp: new Date().toISOString(),
    ...context,
    // Don't log sensitive data
    // NO: credentials, briefs, PII
  });
}

Webhook Error Handling

Failed Webhook Delivery

When webhook delivery fails, fall back to polling:
class WebhookErrorHandler {
  async onDeliveryFailure(taskId, error) {
    console.warn(`Webhook delivery failed for ${taskId}:`, error);

    // Start polling as fallback
    this.startPolling(taskId);

    // Track failure for monitoring
    this.metrics.incrementCounter('webhook_failures');
  }

  async startPolling(taskId) {
    const response = await adcp.call('tasks/get', {
      task_id: taskId,
      include_result: true
    });

    if (['completed', 'failed', 'canceled'].includes(response.status)) {
      await this.processResult(taskId, response);
    } else {
      // Schedule next poll
      setTimeout(() => this.startPolling(taskId), 30000);
    }
  }
}

Webhook Handler Errors

Handle errors in your webhook endpoint gracefully:
app.post('/webhooks/adcp', async (req, res) => {
  try {
    // Always respond quickly
    res.status(200).json({ status: 'received' });

    // Process asynchronously
    await processWebhookAsync(req.body);
  } catch (error) {
    // Log error but don't fail the response
    console.error('Webhook processing error:', error);

    // Move to dead letter queue for investigation
    await deadLetterQueue.add(req.body, error);
  }
});

Recovery Strategies

Context Recovery

If context expires, start a new conversation:
async function callWithContextRecovery(request) {
  try {
    return await adcp.call(request);
  } catch (error) {
    if (error.code === 'INVALID_REQUEST' &&
        error.message?.includes('context not found')) {
      // Clear stale context and retry
      delete request.context_id;
      return await adcp.call(request);
    }
    throw error;
  }
}

Partial Success Handling

Some operations may partially succeed:
{
  "status": "completed",
  "message": "Created media buy with warnings",
  "media_buy_id": "mb_123",
  "errors": [
    {
      "code": "COMPLIANCE_UNSATISFIED",
      "message": "Required disclosure position not supported by one placement",
      "field": "packages[0].placements[2]",
      "suggestion": "Choose a format that supports the required disclosure positions"
    }
  ]
}
Handle partial success:
function handlePartialSuccess(response) {
  if (response.status === 'completed' && response.errors?.length) {
    // Show warnings to user
    for (const warning of response.errors) {
      showWarning(warning.message, warning.suggestion);
    }
  }

  // Continue with successful result
  return response;
}

Governance Error Patterns

check_governance returns a status field rather than an error object. Governance results are not errors in the protocol sense — they are decisions. Handle them separately from AdCP task errors.
Governance statusMeaningAction
approvedPlan passes governanceProceed
conditionsApproved with constraintsApply conditions, re-check
deniedPlan violates governanceBlock the operation
If the governance agent needs human review internally (e.g., the action exceeds the agent’s authority), check_governance behaves like any async task — it returns submitted/working status and eventually resolves to approved or denied. Handle this with the standard async task lifecycle, not special-case logic. Governance errors from the protocol layer (as opposed to governance decisions) use the standard error format. The most common:
CodeRecoveryWhen it occurs
PLAN_NOT_FOUNDcorrectablesync_plans was not called before check_governance
INVALID_REQUESTcorrectableMissing required fields (e.g., plan_id, caller)
AUTH_REQUIREDcorrectableGovernance agent requires authentication

Best Practices

  1. Check recovery first — it’s the most reliable signal for how to handle an error
  2. Implement retries — use exponential backoff for transient errors
  3. Respect rate limits — honor retry_after values
  4. Handle unknown codes gracefully — fall back to the recovery classification
  5. Log with context — include code, recovery, and field for debugging
  6. Fallback strategies — always have a backup (e.g., polling for webhooks)
  7. Don’t retry terminal errors — escalate to a human operator
  8. Handle partial success — process warnings in successful responses

Next Steps

  • Transport Bindings: See Transport Errors for how errors travel over MCP and A2A
  • Task Lifecycle: See Task Lifecycle for status handling
  • Webhooks: See Webhooks for webhook error handling
  • Security: See Security for authentication errors