> ## Documentation Index
> Fetch the complete documentation index at: https://www.ayrshare.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# GraphQL Errors

> How the Ayrshare GraphQL API reports errors, from HTTP status codes on malformed requests to the errors array, partial success, idempotent retries, and rate limits.

## Resolver errors arrive with HTTP 200

This is the single most important thing to know before you write error handling.

Following the GraphQL over HTTP specification, a request that reaches our resolvers returns **HTTP 200 even when an operation failed**. The failure is reported inside the response body, not by the status code.

So this check does not work:

```javascript theme={"system"}
// WRONG - will treat a failed request as a success
if (response.ok) {
  return "everything worked";
}
```

Check the body instead. A few transport errors, such as 415 for an unsupported `Content-Type`, have an empty body, so check that the response is JSON before parsing it:

```javascript theme={"system"}
if (!response.headers.get("content-type")?.includes("json")) {
  throw new Error(`GraphQL request failed with HTTP ${response.status}`);
}

const result = await response.json();

if (result.errors) {
  for (const error of result.errors) {
    // Errors caught before execution have no extensions, so read it safely.
    console.log(error.message, error.extensions?.status);
  }
}
```

Some failures are caught before anything runs. None of them reaches a resolver or uses an API call:

<ul class="custom-bullets">
  <li>A malformed request returns an error status: <strong>HTTP 400</strong> for invalid JSON or a missing <code>query</code>, <strong>413</strong> for a body over 64 KB, <strong>405</strong> for a method other than <code>POST</code>, and <strong>415</strong> for an unsupported <code>Content-Type</code>.</li>
  <li>A query that is not valid for the schema returns <strong>HTTP 400</strong> only if your request sends <code>Accept: application/graphql-response+json</code>. This covers a syntax error, an unknown field, a wrong argument type, an invalid enum value, and a query over the <a href="/docs/apis/graphql/limits#query-limits">query limits</a>. If you send <code>Accept: application/json</code>, or no <code>Accept</code> header, the same error returns <strong>HTTP 200</strong>. Either way, the body has an <code>errors</code> array and no <code>data</code>.</li>
</ul>

Once execution begins, resolver-level input validation and API failures — including authentication, authorization, upstream rate limits, and upstream 5xx responses — return HTTP 200 with an `errors` array.

## The error shape

Every resolver error carries its HTTP-like API status in `extensions`:

```json theme={"system"}
{
  "data": { "postHistory": null },
  "errors": [
    {
      "message": "Rate limit exceeded",
      "path": ["postHistory"],
      "extensions": {
        "status": 429,
        "retryAfter": 7
      }
    }
  ]
}
```

<ul class="custom-bullets">
  <li><code>message</code> — a human-readable description. We remove API keys and other credentials from resolver error messages, but an error about a value you sent, such as a variable of the wrong type, can repeat that value. Treat <code>message</code> as possibly sensitive before you log it.</li>
  <li><code>path</code> — which field in your query failed. With several fields in one request, this is how you tell which.</li>
  <li><code>extensions.status</code> — the HTTP status the equivalent REST call would have returned. Switch on this.</li>
  <li><code>extensions.code</code> — the numeric Ayrshare error code, when the underlying endpoint supplies one.</li>
  <li><code>extensions.retryAfter</code> — present on upstream quota or rate-limit 429s when a retry delay is available. It is not present on the per-request dispatch-budget 429.</li>
</ul>

Use `extensions.code` with the [error code reference](/docs/errors/overview), or pass it to `explainError`.

## Partial success

If a request asks for several things and only some succeed, you get **both** `data` and `errors`:

```graphql theme={"system"}
{
  history: postHistory(lastDays: 7)
  analytics: socialAnalytics(platforms: [INSTAGRAM])
}
```

```json theme={"system"}
{
  "data": {
    "history": { "history": [{ "id": "RkQ8uXg2jT7bNd1Wq0Ya", "status": "success", "post": "Hello world" }], "refId": "9d2a7c41f0b8e6d35a1c", "count": 1, "lastUpdated": "2026-09-24T12:00:00.000Z", "nextUpdate": "2026-09-24T12:00:00.000Z" },
    "analytics": null
  },
  "errors": [
    {
      "message": "Rate limit exceeded",
      "path": ["analytics"],
      "extensions": { "status": 429 }
    }
  ]
}
```

`history` succeeded and its data is usable. Only `analytics` needs retrying. Do not discard the whole response because `errors` is present — that would throw away data you have already been billed for.

## Partial success on multi-network posts

A post to several networks can succeed on some and fail on others. This is **not** reported as a GraphQL error, because the operation itself worked — it did exactly what you asked and is telling you what happened.

The result separates the three outcomes:

```graphql theme={"system"}
mutation {
  createPost(input: {
    post: "Hi"
    platforms: [FACEBOOK, INSTAGRAM]
    idempotencyKey: "multi-network-hi-001"
  }) {
    status
    postIds { platform postUrl }
    errors { platform message code }
    blocked { platform message }
  }
}
```

<ul class="custom-bullets">
  <li><code>postIds</code> — networks that published, with their post URLs. Successes only.</li>
  <li><code>errors</code> — networks that failed, with the reason.</li>
  <li><code>blocked</code> — networks stopped before an attempt, for example by the per-network circuit breaker after repeated deterministic failures. These are separate from provider attempts that returned errors.</li>
</ul>

`status` is `"error"` if **any** network failed, so it is not a reliable signal that nothing published. Read `postIds` to find out what did.

## Retries

**The GraphQL API never retries automatically.** If a request fails, nothing was silently attempted again on your behalf.

This matters most for mutations. `createPost` exposes `idempotencyKey`: generate a unique key before the first attempt, then reuse the same key and identical payload for every retry. A settled retry replays the original result instead of repeating the write. Do not switch to a new key after an ambiguous response.

`boostFacebookPost` and `instagramBoostPost` also take an `idempotencyKey`, and it protects real money. A retry with the same key and the same arguments returns the original result, marked `idempotentReplayed: true`, instead of creating and paying for a second ad. One key covers your whole account on both Meta platforms, so never reuse a Facebook boost's key for an Instagram boost.

When a key is refused, `extensions.status` tells you why. `createPost` returns 409 while the first attempt is still processing, so wait and retry with the same key, and 400 when the payload differs from the first attempt. The boost mutations return 409 in both cases, and also when the key was used for a boost on the other platform.

Other writes, such as `addComment`, have no idempotency key. If their response is lost, check the social network or API state before deciding whether another attempt is safe.

Two different limits both report 429, and they need different handling. The dispatch budget only rejects root fields once five API calls have already started in the same request, and its message begins `Query exceeds the per-request dispatch budget`: split those fields into requests of at most five, with no need to wait. Any other 429 is a rate limit or quota: wait `extensions.retryAfter` seconds when it is present, otherwise back off before retrying, and retry only the fields that failed. For a 5xx on a **read**, retrying is safe, with one exception: `generatePost` is a query, but a retry calls the AI generator again, counts as another API call, and returns different text. For a 5xx on a write without idempotency, reconcile whether it took effect before retrying.

## Common statuses

| `extensions.status` | Meaning | What to do |
| - | - | - |
| 400 | Invalid input our API rejected | Fix the request; retrying will not help |
| 401 | Missing or invalid API key | Check the `Authorization` header |
| 402 | Plan does not include this feature | Check your plan |
| 403 | Key valid but not permitted for this action | Check profile keys and plan |
| 404 | The thing you asked for does not exist | Verify the id |
| 409 | The idempotency key is in use: the first request is still running, or a boost key was used with different arguments | If the first request is still running, wait and retry unchanged; otherwise use a new key for the new operation |
| 429 | Rate limit, quota, or dispatch budget | If the message names the dispatch budget, split the request. Otherwise wait `retryAfter`, or back off if it is absent |
| 500 | Something went wrong on our side | Retry a read (not `generatePost`, which bills again); reuse the same idempotency key where supported, otherwise verify a write before retrying |

Errors raised by GraphQL itself before execution, such as an unknown field, a wrong argument type, or an invalid enum value, have no `extensions.status`. They return HTTP 400 or 200 depending on your `Accept` header, as described above. They mean the query itself is wrong, and no API call was made or billed.
