Connect with OAuth

Fairing supports OAuth 2.0 for partners building integrations on behalf of merchants. Instead of asking a merchant to copy an API key out of their Fairing dashboard, your app sends them through a standard authorization flow: the merchant approves the connection on a Fairing consent screen, and your app receives tokens scoped to that merchant's shop.

Fairing implements the authorization code flow with PKCE. PKCE with the S256 method is required on every authorization request.

📘

OAuth vs. API keys

API key authentication continues to work as documented in Authentication. OAuth is for partners connecting many merchants' shops to their own platform. If you are a merchant calling the API for your own shop, an API key is the simpler option.

Getting credentials

There is no self-serve app registration. To onboard as a partner, send Fairing:

  • Your app's name (shown to merchants on the consent screen)
  • The exact HTTPS redirect_uri(s) your app will use
  • The scopes your app needs

You'll receive a client_id and a client_secret. The secret is generated once - store it like a password. Your app is a confidential client: keep the secret server-side, and never expose it in browser code or mobile apps.

Redirect URIs are matched exactly (scheme, host, port, and path), must use HTTPS, and must not contain a fragment. For local development, http://localhost and http://127.0.0.1 loopback URIs are allowed on any port.

Endpoints

EndpointMethodAuthenticationPurpose
https://app.fairing.co/oauth/api/authorizeGET (browser)Merchant's Fairing sessionConsent screen; issues a single-use authorization code
https://app.fairing.co/oauth/api/tokenPOSTHTTP Basic (client_id:client_secret)Exchanges a code for tokens; refreshes access tokens
https://app.fairing.co/api/...Authorization: Bearer <access_token>The Fairing API

Scopes

Scopes are space-delimited in the authorization request. You can only request scopes that were enabled for your client_id at onboarding.

ScopeGrants
responses:readGET /api/responses and GET /api/questions
sources:writePOST /api/sources

Step 1: Redirect the merchant to Fairing

For each connection attempt, generate a fresh PKCE code_verifier, derive its S256 code_challenge, and generate a random state. Store both in the pending session, then redirect the merchant's browser to the authorize endpoint:

https://app.fairing.co/oauth/api/authorize
  ?response_type=code
  &client_id=fairing_AbCd1234EfGh5678
  &redirect_uri=https%3A%2F%2Fexample-partner.com%2Fcallback
  &scope=responses%3Aread
  &state=opaque-state-xyz
  &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
  &code_challenge_method=S256
ParameterRequiredDescription
response_typeYesMust be code.
client_idYesYour client ID.
redirect_uriYesOne of your registered redirect URIs, byte-for-byte.
scopeYesSpace-delimited scopes to request.
stateYesAn opaque, non-guessable value your app generates per attempt. Echoed back on the redirect — verify it before doing anything else.
code_challengeYesBase64url-encoded SHA-256 of your code_verifier.
code_challenge_methodYesMust be S256.

The merchant signs in to Fairing (if they aren't already), sees your app's name and the requested scopes, picks which of their shops to connect, and approves or declines.

📘

One connection = one shop

Each authorization connects exactly one shop. A merchant with several shops goes through the flow once per shop, and each connection gets its own tokens — store credentials per connection, not per merchant.

Handling the callback

OutcomeWhat your callback receives
Merchant approves302 to redirect_uri?code=...&state=...
Merchant declines302 to redirect_uri?error=access_denied&state=...
Invalid scope, response_type, or PKCE parameters302 to redirect_uri?error=invalid_request&state=...
Unknown client, unregistered redirect_uri, or missing stateError page shown to the merchant — no redirect is made

Verify state first, then exchange the code immediately: it is single-use and expires after 5 minutes.

Step 2: Exchange the code for tokens

POST to the token endpoint with HTTP Basic authentication and a form-encoded body. The redirect_uri must match the one used in Step 1 exactly.

curl -X POST https://app.fairing.co/oauth/api/token \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code" \
  -d "code=$CODE" \
  -d "redirect_uri=https://example-partner.com/callback" \
  -d "code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
{
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 900,
  "scope": "responses:read",
  "refresh_token": "k3H9..."
}
  • access_token — a short-lived bearer token for API calls. Honor expires_in (currently 900 seconds) rather than hardcoding a lifetime.
  • refresh_token — a long-lived credential for minting new access tokens. Store it encrypted, per connection.
  • scope — the scopes the merchant actually granted. Read it from the response rather than assuming it matches your request.

Step 3: Call the API

Send the access token as a bearer token. The token identifies the connected shop — there is no shop parameter.

curl "https://app.fairing.co/api/responses?limit=50" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Query parameters, pagination, and response shapes are the same as documented under Responses and Questions.

⚠️

The Bearer prefix is required

API key authentication sends the raw key in the Authorization header. OAuth access tokens must be prefixed with Bearer or the request is rejected.

Cache the access token and reuse it until shortly before expiry (for example, refresh about 60 seconds early).

Step 4: Refresh the access token

When the access token expires, request a new one with the refresh token, again with HTTP Basic authentication:

curl -X POST https://app.fairing.co/oauth/api/token \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=refresh_token" \
  -d "refresh_token=$REFRESH_TOKEN"

The response contains a new access_token but no refresh_token: refresh tokens are not rotated. The same refresh token keeps working, and each successful refresh extends its expiry — it only expires after 30 days without use. Run one refresh at a time per connection; retries are safe and idempotent.

If a connection sits unused for more than 30 days, the refresh token expires and the merchant must reconnect through the authorization flow.

Error handling

Token endpoint errors

Errors are JSON with an error code. All errors return HTTP 400, except invalid_client, which returns 401.

errorWhenWhat to do
invalid_requestMalformed or missing parameterFix the request
invalid_clientBad credentials or a disabled client (401)Configuration problem — contact Fairing; re-running consent won't help
invalid_grantBad, expired, or already-used code; PKCE mismatch; expired or revoked refresh tokenThe connection is gone — send the merchant through the authorization flow again
unauthorized_clientClient not allowed this grant typeContact Fairing
unsupported_grant_typeUnknown grant_typeFix the request
invalid_scopeScope outside your allowed scopesFix the request

Two error_description values on invalid_grant are stable and safe to key behavior on: "refresh token expired" and "grant revoked". Both mean the same thing for your app — prompt the merchant to reconnect. Any other error_description text may change without notice; branch on error alone.

When a connection dies, show the merchant a reconnect prompt. Don't retry invalid_grant in a loop — the result won't change.

API request errors

StatusBodyMeaning
401{"error": "unauthorized"}Invalid or expired access token — refresh and retry
403{"error": "forbidden"}The connection was revoked or disabled — re-run consent
403{"error": "insufficient_scope"}The token's scopes don't cover this endpoint

Disconnecting

To disconnect a shop, delete the stored refresh token on your side; the credential expires on Fairing's side after 30 days of inactivity. If a connection is revoked from Fairing's side, your app sees invalid_grant on refresh (or 403 on API calls) — treat either as a disconnect and prompt the merchant to reconnect.

If a merchant re-runs the authorization flow for a shop that's already connected, the new consent replaces the old one: the granted scopes are reset to exactly the newly approved set, and previously issued refresh tokens for that connection stop working. Always store the tokens from the most recent authorization.

Rate limits

The OAuth endpoints are rate limited per IP address: 30 requests per minute on the authorize endpoint and 10 per minute on the token endpoint. Responses include x-ratelimit-limit, x-ratelimit-remaining, and x-ratelimit-reset headers; exceeding the limit returns 429 with a retry-after header. Refreshing once per token lifetime keeps you far below these limits.

API requests authenticated with a bearer token are subject to the standard rate limits.

Security checklist

  • Keep the client_secret server-side only; never ship it to a browser or mobile app.
  • Generate a fresh code_verifier and state for every authorization attempt, and verify state on the callback before exchanging the code.
  • Store refresh tokens encrypted at rest, one per connection.
  • Honor expires_in instead of hardcoding token lifetimes.
  • Treat invalid_grant as a dead connection and re-run consent — never retry it automatically.