# 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 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` (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 SocialScope in the platform's settings, because a grant may still be active there. They see SocialScope there, not your app. Doing so ends that account's connection in every app that uses SocialScope. 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.
