RelayAPI
Guides

Error Handling

Handle RelayAPI errors, request limits, idempotent retries, and uncertain provider outcomes safely.

Error envelope

API errors use one JSON envelope:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request validation failed",
    "details": {
      "issues": [
        {
          "code": "too_small",
          "message": "Content is required",
          "path": ["content"]
        }
      ]
    }
  }
}

details is optional. Branch on error.code; do not parse the human-readable message.

HTTP status codes

StatusMeaningTypical action
400Malformed JSON/form data or invalid request semanticsCorrect the request; do not retry unchanged
401Missing, invalid, expired, or disabled API keyReplace or reactivate the credential
403Permission or workspace access deniedCorrect permissions/workspace grants
404Resource is absent or belongs to another organizationCorrect the resource ID
409State, idempotency, or concurrent-operation conflictInspect error.code and Retry-After
413Request envelope exceeded its byte limitSend a smaller request or use the media upload flow
415Unsupported media type or compressed buffered bodyCorrect Content-Type/Content-Encoding
422Schema validation or an explicitly gated/unsupported provider capabilityCorrect details.issues, obtain the named approval, or stop offering the unsupported operation
429Rate limit exceededWait for Retry-After, then retry with jitter
500Internal failureRetry reads; retry mutations only with the original idempotency key
502Upstream provider failureInspect whether the provider outcome is known before retrying
503Temporary authority, billing, or service conditionRespect Retry-After and retry safely
504Upstream timeoutRetry reads; reconcile mutations before replaying them

Common error codes

CodeMeaning
BAD_REQUESTThe request is semantically invalid
MALFORMED_JSONJSON could not be decoded
MALFORMED_FORMMultipart/form data could not be decoded
VALIDATION_ERRORA body, path, or query field failed schema validation
UNAUTHORIZEDAuthentication is missing or invalid
FORBIDDENThe credential lacks a required permission
WORKSPACE_ACCESS_DENIEDThe credential cannot access the resource's workspace
NOT_FOUND, ACCOUNT_NOT_FOUND, POST_NOT_FOUNDThe requested resource was not found in the authenticated tenant
PAYLOAD_TOO_LARGEThe route's declared or streamed byte limit was exceeded
UNSUPPORTED_MEDIA_TYPEContent-Type is unsupported for that route
UNSUPPORTED_CONTENT_ENCODINGA buffered authenticated request used a non-identity encoding
RATE_LIMITEDThe request exceeded a rate limit
IDEMPOTENCY_IN_PROGRESSAnother request with the same key is still executing
IDEMPOTENCY_KEY_REUSEDThe same key was used with different route/body bytes
IDEMPOTENCY_OUTCOME_UNKNOWNThe original mutation may have reached an external provider and needs reconciliation
PROVIDER_OUTCOME_UNKNOWN, PROVIDER_RESPONSE_INVALIDA provider request may have been sent, or its nominally successful response lacked the documented operation-specific proof; stop automatic replay and reconcile the durable operation
SOCIAL_MUTATION_IN_PROGRESSThe same provider object already has an active or ambiguous published-edit/social/WhatsApp mutation
PROVIDER_POST_ID_CHANGEDA published-edit optimistic-concurrency fence no longer matches the target's current provider ID
PUBLISHED_EDIT_UNSUPPORTED, MESSAGE_EDIT_UNSUPPORTED, COMMENT_EDIT_UNSUPPORTED, READ_RECEIPT_UNSUPPORTEDThe requested platform has no verified provider contract for that social operation
DISCORD_THREAD_CONTEXT_MISSINGA Discord forum/media-thread edit lacks the exact durable thread ID, so RelayAPI refused to guess a target
MODERATION_ACTION_UNSUPPORTED, ACTION_UNSUPPORTEDThe action name is invalid for that exact provider; use the documented platform matrix
WHATSAPP_GROUPS_UNAVAILABLEThe exact connected WhatsApp phone number did not pass the feature-specific Groups read probe; this does not diagnose a particular Meta eligibility condition
WHATSAPP_PROVIDER_RESPONSE_INVALIDWhatsApp returned HTTP success without the operation-specific success flag or durable resource/message ID; the remote outcome is treated as unknown and is not replayed automatically
ADS_APPROVAL_REQUIREDThe advanced ad implementation exists, but this connection lacks the named provider program/scope
UNSUPPORTED_FEATURERelayAPI has no enabled implementation for this provider/operation; stale capability metadata cannot enable it
UPLOAD_METADATA_MISMATCHA completed upload's stored size or MIME type does not match its declared session
MEDIA_PROCESSING_UNAVAILABLEThe optional media-processing Container/Workflow is disabled for this deployment; the original upload remains usable
MEDIA_PROCESSING_TYPE_UNSUPPORTEDThe requested explicit processing kind does not accept this original media type; a custom cover requires an image or video
INTERNAL_ERRORRelayAPI intentionally hid internal failure details

Provider-specific failures use a specific code where possible. Treat the HTTP status and the documented provider disposition as authoritative rather than assuming every provider error is safe to replay.

Buffered request limits

RelayAPI checks both a trustworthy Content-Length and the bytes received from an unknown-length or chunked body.

RequestLimitNotes
Authenticated application/json or *+json on POST, PUT, PATCH, or DELETE4 MiBNon-identity Content-Encoding is rejected with 415
POST /v1/posts/bulk-csv multipart envelope2 MiBThe CSV file itself remains limited to 1 MiB and 500 rows
POST /v1/ideas/{id}/media multipart envelope3 MiBThe uploaded file itself remains limited to 2 MiB
POST /v1/media/uploads file bytes200 MiBDirect to object storage; multipart above 64 MiB, so bytes do not traverse the Worker JSON buffer
POST /v1/media/upload file bytes50 MiBStreamed through the Worker with an independent counting limit

An inaccurate or omitted Content-Length does not bypass the limit. Do not compress buffered authenticated JSON or the two multipart envelopes; send Content-Encoding: identity or omit the header.

Upstream response containment

RelayAPI independently caps publisher-provider JSON and diagnostic text that it materializes inside the API Worker at 2 MiB. An oversized or malformed provider response fails closed as an upstream/provider error; it is not a client request-envelope 413. Streamed provider media keeps its route-specific streaming and byte limits.

Safe retries

Reads can normally retry 429, 502, 503, and 504 with exponential backoff and jitter. Always honor Retry-After when present.

Mutations require more care:

  1. Generate one Idempotency-Key for the logical operation.
  2. Reuse the same key and exact body bytes for every attempt.
  3. Never switch to a new key merely because a request timed out or returned 500/502.
  4. If RelayAPI returns IDEMPOTENCY_IN_PROGRESS, wait for Retry-After and retry the same request.
  5. If it returns IDEMPOTENCY_OUTCOME_UNKNOWN, stop automatic retries and reconcile the resource/provider state.

The same rule applies to asynchronous operation resources whose status becomes unknown. In particular, a TikTok/X ad-report submission or provider-backed social mutation may have crossed the provider boundary before a response was lost. Poll the existing operation and use its documented recovery path; do not create a second logical operation with a new idempotency key.

Published edits, social actions, and WhatsApp administration return their durable operation even when its embedded status is failed or unknown. Inspect that field; an HTTP success/acceptance response means the operation resource was returned, not necessarily that the provider mutation completed.

async function retryRead(url: string, apiKey: string, attempts = 4) {
  for (let attempt = 0; attempt < attempts; attempt++) {
    const response = await fetch(url, {
      headers: { Authorization: `Bearer ${apiKey}` },
    });

    if (![429, 502, 503, 504].includes(response.status)) return response;
    if (attempt === attempts - 1) return response;

    const retryAfter = Number(response.headers.get('Retry-After'));
    const baseMs = Number.isFinite(retryAfter)
      ? retryAfter * 1000
      : Math.min(30_000, 500 * 2 ** attempt);
    await new Promise((resolve) =>
      setTimeout(resolve, baseMs + Math.random() * 250),
    );
  }

  throw new Error('unreachable');
}

Logging

Log the HTTP status, error.code, request ID/correlation headers, route, and your own idempotency key. Do not log bearer tokens, OAuth codes, webhook URLs, provider tokens, raw credentials, or sensitive request bodies.

Found something wrong? Help us improve this page.

On this page