Guides
Error Handling

Error Handling

All errors follow a consistent JSON format, and HTTP status codes map directly to error categories. This makes it straightforward to build a single error-handling layer in your integration.


Standard error format

Every error response has at least an error field. Validation errors also include a details object mapping field names to lists of messages:

{
  "error": "Validation failed",
  "details": {
    "title": ["This value should not be blank."],
    "connectionId": ["This is not a valid UUID."]
  }
}

Single-message errors omit details:

{
  "error": "Assignment not found"
}

Parsing errors in code

import requests
 
def call_api(method: str, url: str, **kwargs) -> dict:
    response = requests.request(method, url, **kwargs)
    if not response.ok:
        body = response.json()
        details = body.get("details", {})
        raise RuntimeError(
            f"[{response.status_code}] {body['error']}"
            + (f" — {details}" if details else "")
        )
    return response.json()

HTTP status codes

CodeCategoryCommon causes
400Validation errorMissing required field, invalid format, constraint violation
401Not authenticatedMissing or invalid API key / JWT / MCP token
402Payment requiredInsufficient credits — see X-Credits-Required header
403ForbiddenYour API key doesn't have access to this resource
404Not foundResource doesn't exist or was deleted
409ConflictDuplicate idempotency key, or operation not valid for current state
422UnprocessableRequest is syntactically valid but semantically incorrect
500Server errorInternal error — retry with exponential backoff

Common errors and solutions

400 — Validation failed

{"error": "Validation failed", "details": {"scheduledAt": ["Invalid datetime format."]}}

Fix: Ensure scheduledAt is RFC 3339 with an explicit timezone offset:

2026-04-25T18:00:00+03:00   ✅
2026-04-25T18:00:00         ❌  (missing timezone)
2026-04-25                  ❌  (date only)

Check the details object — each key is a request field, each value is a list of constraint messages.


401 — Unauthorized

{"error": "Invalid API key"}

Fix: Verify the Authorization header format:

Authorization: Bearer ds_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Find and rotate your key in Settings → Credentials. See Authentication for the full auth guide.

⚠️

For MCP clients, a 401 on /_mcp requests means the access token has expired. Use the refresh token to get a new one — do not re-enter the full OAuth flow unless the refresh token has also expired.


402 — Insufficient credits

HTTP/1.1 402 Payment Required
X-Credits-Required: 3
X-Credits-Balance: 0
 
{"error": "Insufficient credits", "required": 3, "balance": 0}

Fix: Top up credits via the console. The X-Credits-Required header tells you exactly how many credits the operation needs. Check your current balance:

curl https://pharlo.io/api/v1/billing/status \
  -H "Authorization: Bearer $DELIVERY_API_KEY"

See Billing & Credits for the full billing guide, including how to handle 402 programmatically.


403 — Forbidden

{"error": "Access denied"}

Fix: The resource exists but your API client doesn't own it. Resources are scoped to the organization of your API key. If you manage multiple organizations, ensure you're using the correct key. Check member roles if you're operating on behalf of an org member.


404 — Not found

{"error": "Assignment not found"}

Fix: Double-check the UUID in your request path. All resources are scoped to your API client — you cannot access resources belonging to other clients even if you know their IDs.


409 — Conflict

{"error": "Assignment cannot be updated in status: published"}

409 covers two distinct cases:

State conflict — the operation is not valid for the resource's current state. For example, you cannot update a published YouTube video without setting updatePublished: true in the payload. Check the Publishing guide for allowed transitions.

Idempotency conflict — you submitted a request with an Idempotency-Key that was already used for a different payload. Use a fresh key for new requests.


422 — Unprocessable

{"error": "Connection platform does not support video assignments"}

The request body is valid JSON and passes field validation, but the combination of values is semantically incorrect — for example, sending a video assignment to a connection that only accepts text posts. Check the platform capabilities via GET /api/v1/platforms.


Retrying on 5xx errors

Server errors are transient. Retry with exponential backoff — do not retry 4xx errors, they indicate a problem with the request itself.

MAX_ATTEMPTS=3
ATTEMPT=0
DELAY=1
 
while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do
  STATUS=$(curl -s -o /tmp/response.json -w "%{http_code}" \
    https://pharlo.io/api/v1/assignments \
    -H "Authorization: Bearer $DELIVERY_API_KEY")
 
  if [ "$STATUS" -lt 500 ]; then
    cat /tmp/response.json
    exit 0
  fi
 
  ATTEMPT=$((ATTEMPT + 1))
  echo "Attempt $ATTEMPT failed with $STATUS, retrying in ${DELAY}s..."
  sleep $DELAY
  DELAY=$((DELAY * 2))
done
 
echo "All attempts failed"
exit 1

Assignment errors

When an assignment reaches failed status, the errors array contains details about what went wrong on the platform side. These are distinct from API-level HTTP errors — the HTTP response itself was 200, but the async job failed later.

{
  "id": "019500ab-...",
  "status": "failed",
  "errors": [
    {
      "code": "MEDIA_FETCH_FAILED",
      "message": "Could not fetch media file: HTTP 403"
    }
  ]
}

You can retrieve a failed assignment's errors by polling GET /api/v1/assignments/{id}, or receive them proactively via Webhooks.

Assignment error codes

CodeMeaningFix
MEDIA_FETCH_FAILEDMedia URL returned an error when we tried to download itEnsure the URL is publicly accessible without authentication
UPLOAD_FAILEDPlatform upload failed after downloadRetry the assignment — may be a transient platform error
QUOTA_EXCEEDEDYouTube API daily quota exhaustedRetry after midnight Pacific time when the quota resets
TOKEN_EXPIREDThe connection's OAuth token has expiredRe-authorize the connection via the OAuth ticket flow — see Connecting Channels
PLATFORM_REJECTEDPlatform rejected the content (e.g. title too long, unsupported format)Check platform-specific payload constraints in the Publishing guide
CONNECTION_DISABLEDThe connection was disconnected or revoked on the platform sideReconnect the channel — see Connecting Channels

Retrying a failed assignment

Do not create a new assignment to retry — use the dedicated retry endpoint to preserve the original assignment ID and audit trail.

curl -X POST https://pharlo.io/api/v1/assignments/019500ab-.../retry \
  -H "Authorization: Bearer $DELIVERY_API_KEY"