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
| Code | Category | Common causes |
|---|---|---|
400 | Validation error | Missing required field, invalid format, constraint violation |
401 | Not authenticated | Missing or invalid API key / JWT / MCP token |
402 | Payment required | Insufficient credits — see X-Credits-Required header |
403 | Forbidden | Your API key doesn't have access to this resource |
404 | Not found | Resource doesn't exist or was deleted |
409 | Conflict | Duplicate idempotency key, or operation not valid for current state |
422 | Unprocessable | Request is syntactically valid but semantically incorrect |
500 | Server error | Internal 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_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxFind 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 1Assignment 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
| Code | Meaning | Fix |
|---|---|---|
MEDIA_FETCH_FAILED | Media URL returned an error when we tried to download it | Ensure the URL is publicly accessible without authentication |
UPLOAD_FAILED | Platform upload failed after download | Retry the assignment — may be a transient platform error |
QUOTA_EXCEEDED | YouTube API daily quota exhausted | Retry after midnight Pacific time when the quota resets |
TOKEN_EXPIRED | The connection's OAuth token has expired | Re-authorize the connection via the OAuth ticket flow — see Connecting Channels |
PLATFORM_REJECTED | Platform rejected the content (e.g. title too long, unsupported format) | Check platform-specific payload constraints in the Publishing guide |
CONNECTION_DISABLED | The connection was disconnected or revoked on the platform side | Reconnect 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"