# 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 and the other accounts are re-checked                        | 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`, `disconnectConnection` | 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` (in production, while SocialScope re-checks it after another disconnect, which ends in `CONNECTED` or, only on a provider refusal, a lost scope or account, an expired or inactive grant, or 24 hours of provider errors, `RECONNECT_REQUIRED`), 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)                                 | Full row, or a degraded row when it is no longer readable | Allowed when the capability is available, otherwise the same errors |
| `SUSPENDED`          | Row with no profile, capabilities `UNAVAILABLE` with `reason` `AUTHORIZATION_SUSPENDED`          | Same as `connection`                                      | `AUTHORIZATION_SUSPENDED`                                           |
| `RECONNECT_REQUIRED` | Row with no profile, capabilities `UNAVAILABLE` with `reason` `RECONNECT_REQUIRED`               | Same as `connection`                                      | `RECONNECT_REQUIRED`                                                |
| `DISCONNECTED`       | Row with no profile, capabilities `UNAVAILABLE` with `reason` `RECONNECT_REQUIRED`, until erased | Same as `connection`                                      | `RECONNECT_REQUIRED`                                                |
| Erased               | `NOT_FOUND`                                                                                      | Left out of the page                                      | `NOT_FOUND`                                                         |

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

| Code                      | Why                                                                    | Status in `connections` | Per-account state to show |
| ------------------------- | ---------------------------------------------------------------------- | ----------------------- | ------------------------- |
| `AUTHORIZATION_SUSPENDED` | SocialScope paused the provider registration                           | `SUSPENDED`             | Temporarily unavailable   |
| `RECONNECT_REQUIRED`      | The grant is no longer active, or its refresh credential expired. See [Token lifetime](/concepts.md#token-lifetime-and-reconnects) | `RECONNECT_REQUIRED`    | Needs reconnect           |
| `CAPABILITY_UNAVAILABLE`  | The provider registration no longer serves your app                    | `SUSPENDED`             | Unavailable               |
| `FORBIDDEN`               | Your app's policy no longer allows this tenant, provider or capability | `SUSPENDED`             | Unavailable               |

`connections` never fails because of one account. It returns such a row degraded instead: the status above, every capability `UNAVAILABLE` with the code as its `reason`, `syncStatus` `STALE`, and a null `profile`, `lastSuccessfulSyncAt` and `nextRetryAt`. Rows already `SUSPENDED`, `RECONNECT_REQUIRED` or `DISCONNECTED` read the same way in both reads, with `syncStatus` `STALE`. So in both reads, a capability `reason` always names the code that explains the account's state. Read the code from `reason` and map it to that account's state, the same way you map a `connection` error. A row erased while the page is read is left out. See [List connections](/reading-data.md#list-connections).

### 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`                                                                                                                  |
| Other accounts on the provider kept connecting during a disconnect | `RATE_LIMITED` from `disconnectConnection` with `retryAfterSeconds: 1`. Nothing changed, so it is safe to retry          |

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 returned an error instead of a consent decision, did not answer the code exchange, or another failure                                                                     |
| `FAILED`    | `CAPABILITY_UNAVAILABLE`      | The provider registration stopped serving your app during the attempt                                                                                                                  |
| `FAILED`    | `INVALID_CLIENT`              | The provider rejected SocialScope's own app credentials, not the person's account. Show a generic retry. The app owner must fix the provider credentials, so tell the SocialScope team |

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 two cases 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. Usual 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 |
| `PROVIDER_REMOVED`               | Revoke, on `SUCCEEDED` | The person removed the app at the provider, which told SocialScope. Nothing is left to revoke, so `creatorAction` is null |

## 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 redirect back to your return URL                 | `FAILED` with `UPSTREAM_UNAVAILABLE`         |
| Your app's access changed before the provider answered            | A redirect back to your return URL                 | `FAILED` with `POLICY_WITHDRAWN`             |
| The provider registration stopped serving your app meanwhile      | A redirect back to your return URL                 | `FAILED` with `CAPABILITY_UNAVAILABLE`       |
| 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.
