# Errors

Read the error envelope, branch on type and code, know which failures to retry, and handle per-platform publishing errors.

Pozy uses HTTP status codes and returns a JSON body on every failed request. Branch your code on the HTTP status and the `code` field. Show `error` to people, but never parse it: the wording can change.

## The error envelope

```json
{
  "error": "`name` must be 1 to 50 characters.",
  "type": "invalid_request_error",
  "code": "invalid_field",
  "param": "name"
}
```

| Field | Always present | Meaning |
| --- | --- | --- |
| `error` | Yes | A sentence for humans. May be reworded at any time. |
| `type` | Usually | The class of error. See the table below. |
| `code` | Usually | A stable, machine-readable reason, such as `read_only_key`. |
| `param` | When one field is at fault | The field or query parameter, dotted when nested. |
| `details` | Sometimes | Structured context: the conflicting id, field errors, limits. |
| `platform`, `platformError` | When a platform refused | Which platform said no, and its original error payload. |

Some endpoints add their own fields next to these. Payment errors carry `reason` and `dashboard_url`, for example ([billing](https://pozy.ai/docs/billing/billing)).

## Status codes and types

| Status | `type` | When |
| --- | --- | --- |
| 400 | `invalid_request_error` | A field is missing or invalid, the JSON is malformed, or a precondition is not met. |
| 401 | `authentication_error` | No credential, or it is invalid, revoked or expired. |
| 402 | `payment_required` | The action needs a payment method or would pass a spend cap, or the workspace is paused because a payment failed. |
| 403 | `permission_error` | The credential is valid but not allowed: read-only key, disabled group, role, scope, an unconfirmed email address, a feature that is not enabled, or a workspace paused by Pozy. |
| 404 | `not_found` | The resource does not exist, or this credential cannot see it. |
| 405 | `invalid_request_error` | The path exists but not with this method. `error` lists the allowed methods. |
| 409 | `invalid_request_error` | A conflict: a duplicate post, an `externalId` already in use, a queue slot already taken, or a request with the same `Idempotency-Key` still running. |
| 422 | `invalid_request_error` | The request is well formed but cannot be processed as sent. |
| 429 | `rate_limit_error` | Too many requests, or a daily cap was reached. Wait `Retry-After` seconds when present ([rate limits](https://pozy.ai/docs/getting-started/rate-limits)). |
| 500 | `api_error` | Something failed on Pozy's side. |
| 501 | `api_error` | The endpoint exists in the spec but is not available yet (`not_implemented`). |
| 502 | `platform_error` or `api_error` | A platform returned an error or did not answer, or Pozy could not send an email. |
| 503 | `platform_error` | A platform service Pozy relies on is unavailable right now. Nothing is wrong with your request. Retry later, after `Retry-After` seconds when present. |

## Common codes

| `code` | Status | Meaning |
| --- | --- | --- |
| `invalid_api_key` | 401 | Missing or invalid credential. |
| `read_only_key` | 403 | A write with a read-only key. |
| `resource_group_disabled` | 403 | The key cannot use this area of the API. |
| `insufficient_permissions` | 403 | The key type cannot do this. |
| `role_forbidden` | 403 | Your workspace role cannot do this. |
| `profile_out_of_scope` | 403 | A profile-scoped key named another profile, or reached for the balance, transfers or payouts. |
| `account_out_of_scope` | 403 | A profile-scoped key named an account that belongs to another profile. |
| `feature_not_available` | 403 | Not enabled for the workspace. Contact support@pozy.ai. |
| `payment_required` | 402 | Add a payment method to continue. |
| `workspace_suspended` | 402 | The workspace is paused because a payment failed. Update the card in Billing to resume. Reads, billing and paying keep working; writes, connects and publishing are refused. |
| `workspace_suspended` | 403 | Pozy paused the workspace. Write to support@pozy.ai to resume it. |
| `email_not_verified` | 403 | Confirm your email address first, or add a payment method. Needed to send invitation emails and to connect accounts without a payment method. |
| `missing_field`, `missing_required_field` | 400 | A required field is absent. `param` names it. |
| `invalid_field`, `invalid_field_value` | 400 | Wrong type, format or value. `param` names it. |
| `method_not_allowed` | 405 | Wrong HTTP method for this path. |
| `endpoint_not_found` | 404 | No endpoint at this method and path. Check for typos and the `/v1` prefix. |
| `resource_not_found` | 404 | The id is unknown or not visible to this credential. Specific variants exist, such as `profile_not_found` and `webhook_not_found`. |
| `rate_limit_exceeded` | 429 | Your workspace used its requests for this minute. |
| `rate_limited` | 429 | A platform or a per-endpoint quota said wait. |
| `invite_limit_reached` | 429 | The workspace sent its invitation emails for today (20 without a payment method, 500 with one), or has 50 email invitations waiting without a payment method. Share an invite link instead. |
| `invite_resend_too_soon` | 429 | An invitation can be emailed again once an hour. |
| `support_ticket_limit` | 429 | A workspace without a payment method sent its 5 questions to the Pozy team for today. Write to support@pozy.ai for anything urgent. |
| `idempotency_key_in_use` | 409 | A request with this `Idempotency-Key` is still running. Retry in a moment with the same key. |
| `email_not_sent` | 502 | Pozy could not send the email. Copy the invite link and share it yourself. |
| `service_unavailable` | 503 | Pozy cannot reach a platform service it relies on. Retry later. |
| `temporarily_unavailable` | 503 | A platform service Pozy relies on is temporarily unavailable, for example for new connections. Retry later. |
| `platform_api_error` | 4xx or 502 | The platform refused the call. Read `platform` and `platformError`. |
| `not_implemented` | 501 | Not available yet. |

New codes can appear at any time, so always keep a default branch.

## Platform errors

When Instagram, Google, Meta or another platform refuses a request, the response says which one and includes its payload untouched:

```json
{
  "error": "Google rejected the ad: NOT_ENOUGH_SQUARE_MARKETING_IMAGE_ASSET",
  "type": "platform_error",
  "code": "platform_api_error",
  "platform": "google",
  "platformError": { "code": 3, "message": "Request contains an invalid argument." }
}
```

A platform `4xx` usually means the input needs fixing. A `502` means the platform failed or timed out, and a retry often works.

## Publishing errors live on the post

A scheduled post fails later, long after your request returned `200`. Those failures do not come back as an error response. Instead, the post ends with `status: "failed"` (no target published) or `"partial"` (some did), and each failed entry in `platforms[]` explains why:

```json
{
  "platform": "instagram",
  "status": "failed",
  "errorMessage": "The aspect ratio of this video is not supported for reels.",
  "errorCategory": "user_content",
  "errorSource": "user"
}
```

| `errorCategory` | Meaning | What to do |
| --- | --- | --- |
| `auth_expired` | The platform revoked access. | Ask the owner to reconnect the account, then retry. |
| `user_content` | The content broke a platform rule: length, format, media size. | Fix the post and retry. |
| `user_abuse` | The platform flagged the content or the account. | Review the content. Do not retry as is. |
| `account_issue` | The account is restricted or missing a permission. | Fix it on the platform side. |
| `platform_rejected` | The platform refused without a clearer reason. | Read `errorMessage`, adjust, retry. |
| `platform_error` | The platform failed on its side. | Retry later. |
| `platform_rate_limit` | The platform throttled the account. | Retry later. |
| `quota_exhausted` | A daily or per-app quota is used up. | Retry after the quota resets. |
| `system_error` | Pozy failed to publish. | Retry. Contact support if it repeats. |
| `unknown` | Not classified. | Read `errorMessage`. |

`errorSource` tells you who has to act: `user`, `platform` or `system`. `POST /v1/posts/{postId}/retry` runs only the failed targets again. Subscribe to `post.failed`, `post.partial` and `post.platform.failed` to hear about these as they happen ([post lifecycle](https://pozy.ai/docs/publishing/lifecycle)).

> **207 is a success code:**
> 
> Publishing now (`publishNow: true`) or retrying can return `207 Multi-Status` when the post was saved but at least one platform failed. Most HTTP clients treat `207` as success, so check `post.status` as well as the status code.

## What to retry

| Response | Retry? | How |
| --- | --- | --- |
| `429` | Yes | Wait `Retry-After` seconds, then send the same request. |
| `500`, `502`, `503` | Yes | Back off exponentially (1 s, 2 s, 4 s, up to about a minute). For creates, send the same `Idempotency-Key` ([idempotency](https://pozy.ai/docs/getting-started/idempotency)). |
| Network timeout | Yes, carefully | The request may have succeeded. Retry with the same `Idempotency-Key`, or read the resource first. |
| `409` `idempotency_key_in_use` | Yes | Wait a moment, then send the same request with the same `Idempotency-Key`. |
| Other `409` | No | Read `details`. A duplicate post comes with the id of the existing one. |
| Other `4xx` | No | Fix the request. Retrying the same body fails the same way. |

## Request ids

Every response carries an `X-Request-Id` header. Log it with the error. Support can trace any request from that id ([support](https://pozy.ai/docs/resources/support)).

You can send your own `x-request-id` (for example the id of your job) and Pozy uses it for that request, so your logs and Pozy's line up.

```bash Terminal
curl -i https://api.pozy.ai/v1/posts/6721a0c47b2e5f9d3a18c6e0 \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "x-request-id: 3e0f7a52-9b1c-4c6d-8f2e-5a7b9c1d3e4f"
```

## Handle errors in code

```ts Node
// A network error throws here: it is safe to retry with the same Idempotency-Key.
const res = await fetch("https://api.pozy.ai/v1/posts", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.POZY_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ content: "Hello", platforms: [] }),
});
if (!res.ok) {
  const body = await res.json();
  const requestId = res.headers.get("x-request-id");
  if (res.status === 429 || res.status >= 500) {
    // retry later, see "What to retry"
  } else if (body.type === "invalid_request_error") {
    console.error(`Fix ${body.param ?? "the request"}:`, body.error, body.details);
  } else {
    console.error(res.status, body.code, body.error, `request ${requestId}`);
  }
}
```
```python Python
import os, requests

resp = requests.post(
    "https://api.pozy.ai/v1/posts",
    headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
    json={"content": "Hello", "platforms": []},
    timeout=30,
)
if not resp.ok:
    body = resp.json()
    request_id = resp.headers.get("x-request-id")
    if resp.status_code == 429 or resp.status_code >= 500:
        pass  # retry later, see "What to retry"
    else:
        raise RuntimeError(f"{resp.status_code} {body.get('code')}: {body.get('error')} (request {request_id})")
```
