Errors
Use the HTTP status code as the primary signal. The response body then comes in one of four shapes, and they are not the same shape. Parse defensively.
Status codes
Section titled “Status codes”| Status | Meaning | What to do |
|---|---|---|
200 | Successful read, update, or delete | Parse data, and pagination if present |
201 | Resource created | Record the returned ID |
400 | Invalid request, or an external source couldn’t be processed, such as a feed or a media URL | Correct it. Don’t retry unchanged |
401 | Missing, invalid, or revoked credential | Re-authenticate |
403 | Authenticated but not permitted: a plan or role limit, or a sensitive action that isn’t enabled | Check role, plan, resource access, and the account’s API settings |
404 | Not found, or not visible to this key | Re-resolve IDs within the correct brand |
409 | Conflict: the post is already published and can’t be changed | Read the post’s current state before acting on it |
422 | Validation failed, on a field or on a business rule | Fix the fields listed in errors |
429 | Rate limit exceeded | Wait for Retry-After or X-RateLimit-Reset. See Rate Limits |
500 | Internal server error, normalized by middleware | Reconcile mutation state before retrying |
The four shapes
Section titled “The four shapes”Field validation (422) uses a Laravel-style payload where each field maps to an array of messages, with no status field:
{ "message": "The given data was invalid.", "errors": { "per_page": ["The per page must not be greater than 100."], "media.0.url": ["The media.0.url must be a valid URL."] }}Note the dotted paths for nested array items. media.0.url is the url of the first item in the media array.
Domain errors (400, 401, 403, 404, 409, 500, and some 422s) carry status: "error", and each field maps to a single string:
{ "status": "error", "message": "One or more channels do not belong to the specified brand", "errors": { "channels": "One or more channels do not belong to the specified brand" }}A business rule that fails validation is a 422 in this shape, not the first one. A scheduledAt less than 10 minutes away is the common case:
{ "status": "error", "message": "Scheduled post date must be at least 10 minutes in the future", "errors": { "scheduledAt": "Scheduled post date must be at least 10 minutes in the future" }}Resource rate limit (429) has errors but no status:
{ "message": "Too many requests. Please try again later.", "errors": { "rate_limit": "Too many requests. Please try again later." }}Route rate limit (429) has a message and nothing else:
{ "message": "Too Many Attempts."}So a 422 can arrive in either of the first two shapes, errors can be missing altogether, and a parser that assumes errors[field] is always a string or always an array will crash on one of them. Check the type of each value, and treat errors as optional.
External sources fail as 400
Section titled “External sources fail as 400”Some requests fetch something from the outside world while you wait. When that fetch fails, it comes back as a domain error, not a validation error.
Creating an automation fetches and parses the feed:
{ "status": "error", "message": "Failed to fetch or parse the feed. Please check the URL and try again.", "errors": { "feedUrl": "Failed to fetch or parse the feed. Please check the URL and try again." }}Creating a post downloads every media[].url:
{ "status": "error", "message": "Failed to download media from url: https://cdn.example.com/missing.jpg", "errors": { "media": "Failed to download media from url: https://cdn.example.com/missing.jpg" }}Retrying either one unchanged will fail identically. Open the URL in a browser first.
Resource-not-found bodies
Section titled “Resource-not-found bodies”A 404 names the parameter that failed to resolve, so you can tell which ID was wrong:
{ "status": "error", "message": "Brand not found", "errors": { "brand_id": "Brand not found" }}{ "status": "error", "message": "Brand not found or you are not a member of the selected brand", "errors": { "brand_id": "Brand not found or you are not a member of the selected brand" }}{ "status": "error", "message": "Collection not found for this brand", "errors": { "collection_id": "Collection not found for this brand" }}{ "status": "error", "message": "Media with id encoded_media_id not found", "errors": { "media": "Media with id encoded_media_id not found" }}The second one is the important case: a valid brand ID that your key cannot see usually returns 404, not 403. Creating or updating a post is the exception, where it returns 403 with errors.brand_id set to You are not a member of this brand. If you are sure the resource exists, check which user the key belongs to with GET /me before assuming it was deleted.
Permission and conflict bodies
Section titled “Permission and conflict bodies”Deleting a post is a sensitive action. Until the account owner enables it in the API settings, the request is refused:
{ "status": "error", "message": "Deleting posts through the API is disabled. Enable sensitive actions for AI in your API settings to allow it.", "errors": { "permissions": "Deleting posts through the API is disabled. Enable sensitive actions for AI in your API settings to allow it." }}Updating a post that has already been published is a conflict:
{ "status": "error", "message": "Only draft, queued or scheduled posts can be edited", "errors": { "post_id": "Only draft, queued or scheduled posts can be edited" }}See Authentication for the setting, and PATCH /posts/{post_id} for what can be changed.
When a retry is safe
Section titled “When a retry is safe”Safe to retry, after the required wait:
GETrequests after a429or a transient connection failure.- Any request rejected before processing with an explicit validation or authentication error, once you’ve corrected the cause.
Reconcile before retrying:
POST,PATCH, andDELETErequests after an ambiguous timeout.- Any mutation that received a
5xx, because the server may have accepted the payload before it failed.
Reconciling means calling the matching list endpoint and looking for what you tried to create, change, or remove. If the state is still unclear, ask support rather than guessing.
Client implementation
Section titled “Client implementation”- Set connection and response timeouts. A hung socket is the case that produces duplicates.
- Log the HTTP status and the response body, with credentials and private content redacted.
- Return non-zero exit codes from scripts.
- Treat an HTTP error as an error even when an intermediary hands it back as ordinary content. An MCP client or an SDK that wraps a
422in a successful-looking envelope will otherwise let a failure pass as a result.