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 keysAPI 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
| Endpoint | Method | Authentication | Purpose |
|---|---|---|---|
https://app.fairing.co/oauth/api/authorize | GET (browser) | Merchant's Fairing session | Consent screen; issues a single-use authorization code |
https://app.fairing.co/oauth/api/token | POST | HTTP 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.
| Scope | Grants |
|---|---|
responses:read | GET /api/responses and GET /api/questions |
sources:write | POST /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
| Parameter | Required | Description |
|---|---|---|
response_type | Yes | Must be code. |
client_id | Yes | Your client ID. |
redirect_uri | Yes | One of your registered redirect URIs, byte-for-byte. |
scope | Yes | Space-delimited scopes to request. |
state | Yes | An opaque, non-guessable value your app generates per attempt. Echoed back on the redirect — verify it before doing anything else. |
code_challenge | Yes | Base64url-encoded SHA-256 of your code_verifier. |
code_challenge_method | Yes | Must 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 shopEach 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
| Outcome | What your callback receives |
|---|---|
| Merchant approves | 302 to redirect_uri?code=...&state=... |
| Merchant declines | 302 to redirect_uri?error=access_denied&state=... |
Invalid scope, response_type, or PKCE parameters | 302 to redirect_uri?error=invalid_request&state=... |
Unknown client, unregistered redirect_uri, or missing state | Error 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. Honorexpires_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.
TheBearerprefix is requiredAPI key authentication sends the raw key in the
Authorizationheader. OAuth access tokens must be prefixed withBeareror 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.
error | When | What to do |
|---|---|---|
invalid_request | Malformed or missing parameter | Fix the request |
invalid_client | Bad credentials or a disabled client (401) | Configuration problem — contact Fairing; re-running consent won't help |
invalid_grant | Bad, expired, or already-used code; PKCE mismatch; expired or revoked refresh token | The connection is gone — send the merchant through the authorization flow again |
unauthorized_client | Client not allowed this grant type | Contact Fairing |
unsupported_grant_type | Unknown grant_type | Fix the request |
invalid_scope | Scope outside your allowed scopes | Fix 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
| Status | Body | Meaning |
|---|---|---|
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_secretserver-side only; never ship it to a browser or mobile app. - Generate a fresh
code_verifierandstatefor every authorization attempt, and verifystateon the callback before exchanging the code. - Store refresh tokens encrypted at rest, one per connection.
- Honor
expires_ininstead of hardcoding token lifetimes. - Treat
invalid_grantas a dead connection and re-run consent — never retry it automatically.
