# Errors and result codes

SocialScope reports problems in four places: GraphQL errors on a request, `resultCode` on a Connect attempt, `errorCode` on a sync job, and `reason` or `incompleteReason` on data. This page lists every value a consumer can see and what your app should do about it. Treat any code you do not recognize as a retryable failure that shows no success, and log its `correlationId`.

## GraphQL error shape

A failed request returns an `errors` array. Each error looks like this:

```json
{
  "errors": [
    {
      "message": "RATE_LIMITED",
      "path": ["requestRefresh"],
      "extensions": {
        "code": "RATE_LIMITED",
        "correlationId": "4f1c2a9e-0b7d-4e7a-9a51-2a3f0c1d9e88",
        "retryAfterSeconds": 60
      }
    }
  ],
  "data": null
}
```

- `extensions.code` is one of the codes below. Branch on it, not on `message`.
- `message` is the code itself, except for the request-limit messages `GraphQL alias limit exceeded`, `GraphQL depth limit exceeded`, `GraphQL cost limit exceeded`, `GraphQL operation limit exceeded` and `Operation batching disabled`. Any other detail, such as a parse or validation error naming a bad field, is replaced by the bare code.
- `extensions.correlationId` is a UUID on every error. Log it and quote it when you ask for help.
- `extensions.retryAfterSeconds` appears only on some `RATE_LIMITED` errors.
- Do not rely on the HTTP status. Most failed operations arrive with HTTP 200. Parse, validation, limit and batching errors arrive with HTTP 400 and the same `errors` shape. A body over 64 KiB gets HTTP 413 before GraphQL runs, with no GraphQL body.

## GraphQL error codes

| Code                      | Where                                                                                           | Meaning                                                                                                                                                                                                                                                                              | What to do                                                                                                                                                                                                             |
| ------------------------- | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `UNAUTHENTICATED`         | Any authenticated operation                                                                     | The key is missing, malformed, unknown, revoked or expired                                                                                                                                                                                                                           | Check configuration. Ask the admin for a new key. Do not retry in a loop                                                                                                                                               |
| `FORBIDDEN`               | Any                                                                                             | Your app is disabled, or the tenant, provider, capability or registration is outside your app's policy. Also a connection whose policy changed. On `createConnectSession` it can also be transient, while a disconnect at the same provider settles in production                    | Treat the feature as unavailable for now and retry later. If it persists, ask the admin                                                                                                                                |
| `NOT_FOUND`               | Reads and mutations                                                                             | The attempt, connection, post or job does not exist, belongs to another scope or app, or has expired. Also an unregistered tenant on most reads                                                                                                                                      | Refresh your local state. Never reveal whether another user's item exists                                                                                                                                              |
| `BAD_USER_INPUT`          | Any                                                                                             | Invalid window, `first`, URL, capability list, scope length or return URL. Also a query that does not parse or does not match the schema, such as a misspelled field, and a request over a [limit](/authentication.md#request-limits). The message does not say which field is wrong | Fix the request. This is a bug, not a transient error. Validate your queries against [/schema.graphql](/schema.graphql) locally, for example with `graphql-js` `validate()`, because the error will not name the field |
| `STALE_CURSOR`            | `connections`, `content`, `requestRefresh`                                                      | A cursor is tampered with, from another owner or window, expired, or from before a reconnect                                                                                                                                                                                         | Restart from the first page or a new refresh                                                                                                                                                                           |
| `SYNC_WINDOW_BUSY`        | `requestRefresh`                                                                                | A scan for a different window is already queued or running on this connection                                                                                                                                                                                                        | Wait for that job, then retry                                                                                                                                                                                          |
| `RATE_LIMITED`            | `createConnectSession`, `createGoogleIdentitySession`, `requestRefresh`, `requestContentLookup` | A cap was reached. See [Rate limits](#rate-limits)                                                                                                                                                                                                                                   | Wait `retryAfterSeconds` when present. Otherwise back off and retry later                                                                                                                                              |
| `RECONNECT_REQUIRED`      | Reads and refreshes                                                                             | The connection is not `CONNECTED`, its grant stopped working, or a reconnect target cannot be reconnected                                                                                                                                                                            | Show a reconnect action. See [Connect](/connect.md#reconnect)                                                                                                                                                          |
| `AUTHORIZATION_SUSPENDED` | Reads and refreshes                                                                             | The connection is `SUSPENDED`, or SocialScope paused the provider registration                                                                                                                                                                                                       | Show "temporarily unavailable". Retry later. If it persists, ask the SocialScope team                                                                                                                                  |
| `CAPABILITY_UNAVAILABLE`  | Reads, refreshes, identity start                                                                | The connection lacks the capability, the provider no longer serves your app, or Google identity is unavailable                                                                                                                                                                       | Hide the feature for this connection                                                                                                                                                                                   |
| `DISCONNECT_IN_PROGRESS`  | `createConnectSession`                                                                          | SocialScope is still removing an earlier connection for this subject and provider                                                                                                                                                                                                    | Show "try again later". No attempt was created                                                                                                                                                                         |
| `UPSTREAM_UNAVAILABLE`    | Any                                                                                             | An unexpected failure inside SocialScope or at a provider                                                                                                                                                                                                                            | Retry with backoff. Show a safe error, never inferred success                                                                                                                                                          |

### Reads by connection status

| `Connection.status`  | `connection`, `connections`                                      | `content`, `contentItem`, `requestRefresh`, `requestContentLookup`  |
| -------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------- |
| `CONNECTED`          | Full row, or an error when the row is no longer readable (below) | Allowed when the capability is available, otherwise the same errors |
| `SUSPENDED`          | Row with no profile                                              | `AUTHORIZATION_SUSPENDED`                                           |
| `RECONNECT_REQUIRED` | Row with no profile                                              | `RECONNECT_REQUIRED`                                                |
| `DISCONNECTED`       | Row with no profile, until erased                                | `RECONNECT_REQUIRED`                                                |
| Erased               | `NOT_FOUND`                                                      | `NOT_FOUND`                                                         |

A `CONNECTED` connection is checked again on every read. `connection` then throws instead of returning the row when:

| Code                      | Why                                                                    | Per-account state to show |
| ------------------------- | ---------------------------------------------------------------------- | ------------------------- |
| `AUTHORIZATION_SUSPENDED` | SocialScope paused the provider registration                           | Temporarily unavailable   |
| `RECONNECT_REQUIRED`      | The grant is no longer active, or its refresh credential expired       | Needs reconnect           |
| `CAPABILITY_UNAVAILABLE`  | The provider registration no longer serves your app                    | Unavailable               |
| `FORBIDDEN`               | Your app's policy no longer allows this tenant, provider or capability | Unavailable               |

`connections` reads every row on the page the same way, so one such account makes the whole list call fail with that code. Keep the connection IDs you received from Connect in your own database, read each one with `connection`, and map each error code to that account's state. Treat `connections` as a convenience. If it fails, fall back to per-ID reads rather than showing "no accounts".

### Local codes from the sample helper

The sample helper in [Authentication](/authentication.md) adds codes of its own. SocialScope never sends them.

| Code                            | Meaning                                                        |
| ------------------------------- | -------------------------------------------------------------- |
| `SOCIALSCOPE_NOT_CONFIGURED`    | The GraphQL URL or key is missing from your server environment |
| `SOCIALSCOPE_REQUEST_TOO_LARGE` | The request body was over 64 KiB and got HTTP 413              |
| `SOCIALSCOPE_UNAVAILABLE`       | Network failure, timeout, or a response that was not GraphQL   |
| `SOCIALSCOPE_JOB_TIMEOUT`       | The polling helper gave up waiting for a job                   |

## Rate limits

| Limit                                                  | Error                                                                                                                               |
| ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- |
| 5 unfinished Connect attempts per subject and provider | `RATE_LIMITED` from `createConnectSession`, no retry hint. Attempts stop counting once they finish or pass their 10-minute deadline |
| 20 unfinished Google identity attempts per app         | `RATE_LIMITED` with `retryAfterSeconds` from 1 to 600                                                                               |
| 20 queued or running read jobs per app                 | `RATE_LIMITED` with `retryAfterSeconds: 60`                                                                                         |
| One scan window at a time per connection               | `SYNC_WINDOW_BUSY`                                                                                                                  |

There is no minimum interval between refreshes, but repeated requests for the same window or post while one is in flight return the existing job.

## Connect result codes

`connectSessionResult` returns `status`, `resultCode` and `creatorAction`.

| `status`    | `resultCode`                  | Meaning                                                                                                  |
| ----------- | ----------------------------- | -------------------------------------------------------------------------------------------------------- |
| `COMPLETED` | `CONNECTED`                   | The attempt committed a connection. Read `connection` for its current status                             |
| `DENIED`    | `PROVIDER_DENIED`             | The person declined at the provider                                                                      |
| `EXPIRED`   | `SESSION_EXPIRED`             | The 10-minute authorization deadline passed                                                              |
| `CANCELED`  | `USER_CANCELED`               | The person cancelled on SocialScope's page                                                               |
| `CANCELED`  | `RECONNECT_REQUIRED`          | SocialScope stopped the attempt while a disconnect at the same provider settled                          |
| `FAILED`    | `POLICY_WITHDRAWN`            | Your app's access changed during the attempt                                                             |
| `FAILED`    | `DISCONNECT_IN_PROGRESS`      | The chosen account is still being removed for this subject                                               |
| `FAILED`    | `ACCOUNT_NOT_ELIGIBLE`        | Wrong account type, such as a personal Instagram account or a Google account without exactly one channel |
| `FAILED`    | `ACCOUNT_MISMATCH`            | The provider returned inconsistent account details                                                       |
| `FAILED`    | `MISSING_REQUIRED_PERMISSION` | The grant lacks a permission SocialScope needs                                                           |
| `FAILED`    | `OFFLINE_ACCESS_REQUIRED`     | The provider did not issue a refresh token                                                               |
| `FAILED`    | `RECONNECT_REQUIRED`          | A reconnect picked a different account, or the target changed during the attempt                         |
| `FAILED`    | `RATE_LIMITED`                | The provider or SocialScope was busy with another token operation                                        |
| `FAILED`    | `UPSTREAM_UNAVAILABLE`        | The provider did not answer the code exchange, or another failure                                        |
| `FAILED`    | `CAPABILITY_UNAVAILABLE`      | The provider registration stopped serving your app during the attempt                                    |

For every non-`COMPLETED` result, offer a retry in your app. Never fall back to your own provider OAuth.

`creatorAction` is null or `REMOVE_APP_AT_PROVIDER`. When set, ask the person to remove the app in their provider account settings, because a grant may still be active there. Treat any unknown value the same way. It can change from null to `REMOVE_APP_AT_PROVIDER` on a later read of the same attempt.

## Sync job error codes

`syncJob.errorCode` explains a `FAILED` job, and in one case a `SUCCEEDED` one. A `QUEUED` job that is waiting to retry also carries the last error in `errorCode`, with `nextRetryAt` set. That is not a final failure.

| `errorCode`                      | Job kind               | Meaning and action                                                                                    |
| -------------------------------- | ---------------------- | ----------------------------------------------------------------------------------------------------- |
| `CONTENT_NOT_OWNED`              | Lookup                 | The post belongs to another account. Tell the person                                                  |
| `CONTENT_UNAVAILABLE`            | Lookup                 | The provider did not return the post. It may be private, deleted or not theirs                        |
| `LOOKUP_INCOMPLETE`              | Lookup                 | Instagram only. The post is not among the 100 most recent                                             |
| `BAD_PROVIDER_ID`                | Lookup                 | The URL's ID is not valid for that provider                                                           |
| `RECONNECT_REQUIRED`             | Any read               | The grant stopped working. Offer a reconnect                                                          |
| `CAPABILITY_UNAVAILABLE`         | Any read               | The capability or provider is no longer available for this connection                                 |
| `ACCOUNT_MISMATCH`               | Any read               | The provider now reports a different account for this grant. Offer a reconnect                        |
| `MISSING_REQUIRED_PERMISSION`    | Any read               | The grant lost a permission. Offer a reconnect                                                        |
| `UPSTREAM_UNAVAILABLE`           | Any                    | The provider kept failing after retries. Try again later                                              |
| `RATE_LIMITED`                   | Any                    | The provider kept rate limiting after retries. Try again later                                        |
| `UPSTREAM_REVOCATION_UNVERIFIED` | Revoke                 | SocialScope could not confirm the revoke. Always the case for Instagram. `creatorAction` is set       |
| `ALREADY_INVALID`                | Revoke, on `SUCCEEDED` | The provider said the token was already invalid. The grant may still exist, so `creatorAction` is set |

## Availability reasons

On a `MetricObservation`:

| `availability` | `reason`                                                                           | Meaning                                                                    |
| -------------- | ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| `AVAILABLE`    | null                                                                               | `value` holds the count                                                    |
| `UNSUPPORTED`  | `PROVIDER_UNSUPPORTED`                                                             | The provider does not offer this metric for this item                      |
| `UNAVAILABLE`  | `PROVIDER_OMITTED`                                                                 | The provider did not return the value                                      |
| `UNAVAILABLE`  | `PROVIDER_INVALID`                                                                 | The provider returned something that was not a count                       |
| `UNAVAILABLE`  | `CONTENT_NOT_OWNED`, `CONTENT_UNAVAILABLE`, `LOOKUP_INCOMPLETE`, `BAD_PROVIDER_ID` | A later metrics refresh or lookup could not read the post. `value` is null |

On a `ContentItem`:

| `availability` | `reason`                                                      | Meaning                                                          |
| -------------- | ------------------------------------------------------------- | ---------------------------------------------------------------- |
| `AVAILABLE`    | null                                                          | Normal                                                           |
| `UNAVAILABLE`  | `EXPIRED`                                                     | Last observed more than 30 days ago. Fields are null. Refresh it |
| `UNAVAILABLE`  | `CONTENT_NOT_OWNED`                                           | A lookup found it belongs to another account                     |
| `UNAVAILABLE`  | `CONTENT_UNAVAILABLE`, `LOOKUP_INCOMPLETE`, `BAD_PROVIDER_ID` | A metrics refresh or lookup could not read it                    |

In every case except `AVAILABLE`, show "not available", never `0`.

## Completeness reasons

`pageInfo.incompleteReason` and `syncJob.incompleteReason`:

| Value                      | Meaning                              |
| -------------------------- | ------------------------------------ |
| `NOT_FULLY_SCANNED`        | No qualifying scan covers the window |
| `SCAN_BOUNDED`             | The scan hit its page or item limit  |
| `MISSING_PUBLICATION_DATE` | An item had no publication date      |
| `CONTENT_UNAVAILABLE`      | An item could not be read            |

A `syncJob` for a metrics refresh or lookup that could not read its post also sets `completeForWindow: false` and `incompleteReason` to `CONTENT_NOT_OWNED`, `CONTENT_UNAVAILABLE`, `LOOKUP_INCOMPLETE` or `BAD_PROVIDER_ID`. Treat any other value as incomplete too.

See [Reading data](/reading-data.md#completeness).

## Google identity outcomes

`redeemGoogleIdentityResult.status` is `PENDING`, `COMPLETED`, `DENIED`, `FAILED` or `EXPIRED`. A second redeem, a wrong proof or tenant, another app's attempt, an expired result, or identity turned off for your app all return the same `NOT_FOUND`. See [Google identity](/google-identity.md#statuses).

## Browser-side failures

These happen in the person's browser on SocialScope's host, not in your GraphQL calls. You learn about them by reading the attempt from your server.

| Failure                                                           | What the person sees                               | What your server reads                       |
| ----------------------------------------------------------------- | -------------------------------------------------- | -------------------------------------------- |
| Launch posted from the wrong origin, or a reused or expired token | A `403` with a small JSON body                     | The attempt stays unfinished, then `EXPIRED` |
| Consent opened in a different browser or profile                  | A `403` at the callback                            | Unfinished, then `EXPIRED`                   |
| Provider returned an unexpected error                             | A `403` at the callback                            | Unfinished, then `EXPIRED`                   |
| Your return URL was removed during the attempt                    | A static "Return to the app you started from" page | The result is still readable                 |

In each case, let the person start again from your app. An unfinished attempt reads as `EXPIRED` once its 10-minute deadline passes.
