API OAuth 2.0 for integrations

Use OAuth 2.0 with PKCE to connect your integration to a customer's SnitchFeed organization: authorize, refresh, revoke and verify revoke webhooks.

Last updated

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 when you call the API for your own organization.

SnitchFeed provisions OAuth clients; there is no self-serve client registration. Email hello@snitchfeed.com to register your integration and get a client_id and client_secret.

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).

MethodPathDescription
GET/auth/oauth2/authorizeStart the authorization flow
POST/auth/oauth2/tokenExchange a code (or refresh token) for an access token
POST/auth/oauth2/revokeRevoke a token (RFC 7009)
GET/auth/.well-known/oauth-authorization-serverDiscovery metadata

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

Scopes

Every scope on the Scopes page is available to OAuth clients. OAuth adds one more:

ScopeGrants
offline_accessIssues 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.

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).

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

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

    ParamValue
    client_idYour client ID
    redirect_uriMust 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_typecode
    scopeSpace-separated scopes, for example mentions:read feeds:read offline_access
    resourcehttps://api.snitchfeed.com/v1 (RFC 8707 audience)
    stateOpaque value you verify on callback
    code_challenge, code_challenge_method=S256PKCE, 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.

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

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.

{
  "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:

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

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

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

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

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

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.

{
  "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:

HeaderValue
Content-Typeapplication/json
X-SnitchFeed-Signaturesha256= followed by the hex HMAC-SHA256 of the raw request body, keyed with your revoke webhook secret
X-SnitchFeed-Delivery-IdUnique ID for this delivery, the same on every retry
Idempotency-KeySame 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.

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

OAuth endpoints use the standard OAuth 2.0 error and error_description shape, not the /v1 error envelope. 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 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.

On this page