Skip to content

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.

StatusMeaningWhat to do
200Successful read, update, or deleteParse data, and pagination if present
201Resource createdRecord the returned ID
400Invalid request, or an external source couldn’t be processed, such as a feed or a media URLCorrect it. Don’t retry unchanged
401Missing, invalid, or revoked credentialRe-authenticate
403Authenticated but not permitted: a plan or role limit, or a sensitive action that isn’t enabledCheck role, plan, resource access, and the account’s API settings
404Not found, or not visible to this keyRe-resolve IDs within the correct brand
409Conflict: the post is already published and can’t be changedRead the post’s current state before acting on it
422Validation failed, on a field or on a business ruleFix the fields listed in errors
429Rate limit exceededWait for Retry-After or X-RateLimit-Reset. See Rate Limits
500Internal server error, normalized by middlewareReconcile mutation state before retrying

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.

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.

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.

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.

Safe to retry, after the required wait:

  • GET requests after a 429 or 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, and DELETE requests 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.

  • 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 422 in a successful-looking envelope will otherwise let a failure pass as a result.