# API OAuth 2.0 for integrations (https://snitchfeed.com/docs/api/oauth)





Integrations can access a customer's SnitchFeed data with **OAuth 2.0** instead of asking the customer for an API key. The customer authorizes your integration, picks which organization (workspace) to grant, and your integration gets access tokens limited to that organization and the scopes they approved.

Use OAuth when you build an integration other customers will install. Use an [API key](/docs/api/authentication) when you call the API for your own organization.

<Callout type="info">
  SnitchFeed provisions OAuth clients; there is no self-serve client registration. Email [hello@snitchfeed.com](mailto:hello@snitchfeed.com) to register your integration and get a `client_id` and `client_secret`.
</Callout>

## Endpoints [#endpoints]

All OAuth protocol endpoints are on the API origin (`https://api.snitchfeed.com`), under `/auth`. Sign-in, organization selection and consent screens run on the app (`https://app.snitchfeed.com`).

| Method | Path                                           | Description                                                         |
| ------ | ---------------------------------------------- | ------------------------------------------------------------------- |
| `GET`  | `/auth/oauth2/authorize`                       | Start the authorization flow                                        |
| `POST` | `/auth/oauth2/token`                           | Exchange a code (or refresh token) for an access token              |
| `POST` | `/auth/oauth2/revoke`                          | Revoke a token ([RFC 7009](https://www.rfc-editor.org/rfc/rfc7009)) |
| `GET`  | `/auth/.well-known/oauth-authorization-server` | Discovery metadata                                                  |

Discovery: `https://api.snitchfeed.com/auth/.well-known/oauth-authorization-server`

## Scopes [#scopes]

Every scope on the [Scopes](/docs/api/scopes) page is available to OAuth clients. OAuth adds one more:

| Scope            | Grants                                                                                   |
| ---------------- | ---------------------------------------------------------------------------------------- |
| `offline_access` | Issues a refresh token, so your app can stay connected until the customer revokes access |

Request only the scopes your integration needs. The customer sees them on the consent screen. A client can only request scopes it was registered with; SnitchFeed enforces that at authorization time.

<Callout type="warn">
  The credit-metered `POST /v1/data/*` search endpoints currently accept API keys only and return `401` for OAuth access tokens, even with `search:read`. OAuth tokens work on all organization data endpoints (mentions, listeners, feeds, organization context and usage).
</Callout>

## Tokens and organization scope [#tokens-and-organization-scope]

* Read the access token lifetime from `expires_in` in the token response instead of hard-coding it, and refresh before it runs out. Exchange authorization codes as soon as you receive them; they are short-lived and single-use.
* Each authorization grants access to exactly **one** organization: the one the customer picks on the app's organization picker (`/oauth/select-org`). A token cannot span organizations. To connect a second organization, run the authorize flow again.
* The API plan check that applies to API keys does not apply to OAuth tokens.
* There is no sandbox. Integrations are built and tested against production; use a test organization if you don't want to touch real data.

## Authorize a customer [#authorize-a-customer]

1. Redirect the customer to `/auth/oauth2/authorize` with:

   | Param                                          | Value                                                                                                                                                                                                            |
   | ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
   | `client_id`                                    | Your client ID                                                                                                                                                                                                   |
   | `redirect_uri`                                 | Must **exactly** match one of your registered redirect URIs, including scheme and path. Registered URIs must be `https://` (`http://` is accepted only for `localhost` and `127.0.0.1` during local development) |
   | `response_type`                                | `code`                                                                                                                                                                                                           |
   | `scope`                                        | Space-separated scopes, for example `mentions:read feeds:read offline_access`                                                                                                                                    |
   | `resource`                                     | `https://api.snitchfeed.com/v1` ([RFC 8707](https://www.rfc-editor.org/rfc/rfc8707) audience)                                                                                                                    |
   | `state`                                        | Opaque value you verify on callback                                                                                                                                                                              |
   | `code_challenge`, `code_challenge_method=S256` | PKCE, **required** for all clients                                                                                                                                                                               |

2. The customer signs in if needed, picks which organization to connect, and approves the requested scopes.

3. SnitchFeed redirects back to your `redirect_uri` with `code` and your `state`.

```bash
GET https://api.snitchfeed.com/auth/oauth2/authorize
  ?client_id=YOUR_CLIENT_ID
  &redirect_uri=https://yourapp.com/oauth/callback
  &response_type=code
  &scope=mentions:read%20feeds:read%20offline_access
  &resource=https://api.snitchfeed.com/v1
  &state=RANDOM_OPAQUE_STATE
  &code_challenge=YOUR_CODE_CHALLENGE
  &code_challenge_method=S256
```

## Exchange the code for a token [#exchange-the-code-for-a-token]

```bash
curl -sS -X POST "https://api.snitchfeed.com/auth/oauth2/token" \
  -u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \
  -d "grant_type=authorization_code" \
  -d "code=THE_CODE_FROM_CALLBACK" \
  -d "redirect_uri=https://yourapp.com/oauth/callback" \
  -d "code_verifier=YOUR_PKCE_VERIFIER"
```

Client authentication is HTTP Basic (`client_secret_basic`) only. Send your client ID and secret with `-u` or an `Authorization: Basic` header, not as body parameters.

```json
{
  "access_token": "...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "...",
  "scope": "mentions:read feeds:read offline_access"
}
```

Call the API with the access token in the same `Authorization: Bearer` header an API key uses. See [Authentication](/docs/api/authentication):

```bash
curl -sS "https://api.snitchfeed.com/v1/organization/context" \
  -H "Authorization: Bearer THE_ACCESS_TOKEN"
```

Access tokens are opaque (not JWTs) and are checked against SnitchFeed's database on every request, so a revoked token stops working immediately.

### Refreshing [#refreshing]

If you requested `offline_access`, exchange the refresh token for a new access token:

```bash
curl -sS -X POST "https://api.snitchfeed.com/auth/oauth2/token" \
  -u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \
  -d "grant_type=refresh_token" \
  -d "refresh_token=YOUR_REFRESH_TOKEN"
```

If the response includes a new `refresh_token`, store it and use it for the next refresh.

## Rate limits [#rate-limits]

Calls to `/v1/*` with an access token count toward the customer organization's [rate limits](/docs/api/rate-limits). Limits are enforced per organization, not per credential, so your integration shares them with the customer's own API keys.

## Revoking access [#revoking-access]

Either side can end the connection:

* **Your app** can revoke a token it holds with `POST /auth/oauth2/revoke` (`token`, plus your client credentials).
* **The customer** can disconnect your app at any time from **Connected apps** on the Integrations page in SnitchFeed.

### Revoke webhook [#revoke-webhook]

If you registered a revoke webhook URL and secret with SnitchFeed, your app gets a `POST` when an installation ends: the customer disconnects it (`user_revoked`), the organization is deleted (`org_deleted`), or SnitchFeed suspends your client (`client_suspended`). Use it to delete stored tokens on your side.

```json
{
  "type": "oauth.installation.revoked",
  "specVersion": "1",
  "revokedAt": "2026-01-15T10:30:00.000Z",
  "clientId": "YOUR_CLIENT_ID",
  "organizationId": 123,
  "reason": "user_revoked"
}
```

Each delivery has these headers:

| Header                     | Value                                                                                                    |
| -------------------------- | -------------------------------------------------------------------------------------------------------- |
| `Content-Type`             | `application/json`                                                                                       |
| `X-SnitchFeed-Signature`   | `sha256=` followed by the hex HMAC-SHA256 of the raw request body, keyed with your revoke webhook secret |
| `X-SnitchFeed-Delivery-Id` | Unique ID for this delivery, the same on every retry                                                     |
| `Idempotency-Key`          | Same value as `X-SnitchFeed-Delivery-Id`                                                                 |

To verify a delivery, compute the HMAC over the raw body bytes exactly as received (before parsing JSON), prefix it with `sha256=`, and compare it with the header using a constant-time comparison. Reject the request if they differ.

```js
import { createHmac, timingSafeEqual } from "node:crypto";

function isValidSignature(rawBody, header, secret) {
  const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(header.trim());
  return a.length === b.length && timingSafeEqual(a, b);
}
```

Respond with any `2xx` status to acknowledge. Other responses and network errors are retried with exponential backoff starting at 30 seconds, up to 10 attempts in total. Because retries reuse the delivery ID, use `Idempotency-Key` to process each delivery once.

## Errors [#errors]

OAuth endpoints use the standard OAuth 2.0 `error` and `error_description` shape, not the `/v1` [error envelope](/docs/api/introduction#response-format). The two endpoints deliver errors differently:

* **`/auth/oauth2/authorize`**: once your `client_id` and `redirect_uri` are recognized, errors (`access_denied`, `invalid_scope`, `invalid_request` and others) come back as a &#x2A;*redirect to your `redirect_uri`** with `error`, `error_description` and `state` as query parameters. There is no JSON body. If `client_id` or `redirect_uri` itself is invalid or unregistered, SnitchFeed cannot safely redirect and shows an error page instead.
* **`/auth/oauth2/token`**: errors are a JSON body with **HTTP 400**, including bad client credentials (`invalid_client` is 400 here, not 401). Common cases: `invalid_grant` (the code, refresh token or PKCE verifier didn't match, or the code expired or was already used), `invalid_scope` (a scope your client isn't registered for) and `invalid_client` (bad client ID or secret).

On `/v1/*`, a revoked or expired access token returns `401 unauthorized`, as described in the [API introduction](/docs/api/introduction#error-codes).

## Related [#related]

* [API authentication](/docs/api/authentication): the Bearer header both credential types use
* [API scopes and permissions](/docs/api/scopes): what each scope grants
* [API rate limits](/docs/api/rate-limits): limits shared per organization
* [SnitchFeed API](/docs/api/introduction): endpoints and error codes
