Skip to content

CLI OAuth

Flint registers flint-cli as a public OAuth client, with no client secret or HTTP Basic authentication. Login uses OAuth 2.0 Device Authorization Grant, renewal uses the refresh token grant, and logout uses token revocation. A person approves access in the Flint dashboard, and the client receives short-lived access tokens without handling an API key.

The Flint CLI runs these calls for flint login, flint context, flint reauth, and flint logout, and the HTTP contract here is what they rely on. For other jobs, use an API key on your server, a workload identity in CI, or partner OAuth for an app that merchants install.

Sessions, contexts, and tokens#

One browser approval creates a session. A session holds one or more contexts, and each context is one merchant in one mode: its live account, or one of its sandboxes. The person who approves chooses the contexts and the scopes each one holds.

CredentialPrefixLifetimeUse
Device codefcdc_10 minutes, single useRedeem it for the first tokens once the person approves. Keep it private.
User codeNone, such as WDJB-MJHTSame as its device codeShow it in your terminal. The browser shows the same code so the person can check they match.
Access tokenfcat_15 minutes at mostCall the API in exactly one context.
Refresh tokenfcrt_Until the session endsMint an access token for any context in the session. Every use replaces it.

The session keeps its oauth_session_id through every refresh and consent change. Each context has a context_id whose merchant, mode, and sandbox never change. If a consent review removes a context and a later review in the same session approves that merchant and sandbox again, the context returns with its original ID.

A session ends 30 days after its last token was issued, 90 days after login, when any of its tokens is revoked, or when a spent refresh token is reused. Ending the session invalidates every token in every context.

Note: Access follows the person who approved

Approving a context requires permission to manage that merchant's API keys. Flint checks the approver's access again on every API request and every refresh. If they leave the merchant or lose that permission, the context stops working in every session they approved.

Requests and responses#

These rules apply to the three CLI OAuth endpoints and to the CLI's use of the shared token and revocation endpoints:

  • Send an application/x-www-form-urlencoded body, with each field once and no URL query string. Anything else returns 400 invalid_request. The device authorization and consent review endpoints also reject fields they don't define.
  • Send client_id=flint-cli, with no client_secret and no Authorization header. Otherwise the request fails with 401 invalid_client.
  • Responses are flat OAuth JSON without the Flint envelope, sent with Cache-Control: no-store. Flint never replays these requests from an idempotency cache.

Sign in#

Device loginResponse
CLIFlintBrowserdevice/authorizedevice_code, user_codeopens the linkapprovespolls /oauth/tokenauthorization_pendingtokens, context_idAPI calls
  1. CLI sends device/authorize to Flint
  2. Flint returns device_code, user_code to CLI
  3. CLI sends opens the link to Browser
  4. Browser sends approves to Flint
  5. CLI sends polls /oauth/token to Flint
  6. Flint returns authorization_pending to CLI
  7. Flint returns tokens, context_id to CLI
  8. CLI sends API calls to Flint

Start the login#

cURL
curl https://api.withflintpay.com/v1/oauth/device/authorize \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "client_id=flint-cli" \
  --data-urlencode "session_mode=contexts" \
  --data-urlencode "mode=test" \
  --data-urlencode "scope=payments.payment_intents.read payments.payment_intents.write"
FieldRequiredValue
client_idYesflint-cli.
scopeYesSpace-separated API scope names. The person can grant each context any of them, and nothing beyond them. scopes_supported in OAuth discovery lists every name the CLI can request; an unknown name returns invalid_scope.
session_modeNoDefaults to contexts when omitted. Any other value returns 400 invalid_request naming session_mode. This creates a session that can hold several contexts and supports context listing and consent review.
modeNotest (the default) or live. The approval must include a context in this mode, and the first access token is for it. The person can still approve contexts in the other mode.
merchant_idNoLimits every approved context to this merchant.
sandbox_idNoLimits every approved context to this sandbox, so no live context can be approved. Don't combine it with mode=live.
JSON
{
  "device_code": "fcdc_example",
  "user_code": "WDJB-MJHT",
  "verification_uri": "https://app.withflintpay.com/cli/activate",
  "verification_uri_complete": "https://app.withflintpay.com/cli/activate?user_code=WDJB-MJHT",
  "expires_in": 600,
  "interval": 5
}

Open verification_uri_complete in the person's browser, or show verification_uri and user_code for them to open and type. The link only identifies the request; nothing is approved until the person approves it.

What the person approves#

The person signs in to the dashboard, checks that the code matches, and chooses contexts. Flint starts them on a likely context, and they can add other sandboxes, other merchants, or live access, or remove scopes, before they approve or deny. Live contexts carry a live label.

Each approved context needs at least one scope, and only scopes from your request. A scope that a mode doesn't allow, such as a live-only scope in a sandbox, can't be granted in that mode. A session can track up to 100 contexts, including ones later removed.

Poll for tokens#

Poll the token endpoint with the device code, waiting interval seconds between requests:

cURL
curl https://api.withflintpay.com/v1/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:device_code" \
  --data-urlencode "client_id=flint-cli" \
  --data-urlencode "device_code=$DEVICE_CODE"

Until the login finishes, each poll returns 400 with one of these error values:

errorMeaningNext step
authorization_pendingThe person hasn't decided yet.Wait interval seconds and poll again.
slow_downYou polled before the interval passed, or polled too often.Add five seconds to your interval, keep the longer interval for later polls, and poll again.
access_deniedThe person denied the request.Stop.
expired_tokenThe code expired after expires_in seconds, or was already redeemed.Start a new login.

Once the person approves, the next poll returns the session's first tokens:

JSON
{
  "access_token": "fcat_example",
  "token_type": "Bearer",
  "expires_in": 900,
  "refresh_token": "fcrt_example",
  "scope": "payments.payment_intents.read payments.payment_intents.write",
  "oauth_session_id": "session_example",
  "context_id": "ctx_example"
}

scope lists what the person granted to this context, which can be fewer scopes than you asked for. Store the refresh token as a secret; the Flint CLI keeps it in the OS keychain.

Call the API#

Send the access token in the Authorization header. It acts only in its context, so you don't need Flint-Mode or Flint-Sandbox-Id. GET /v1/auth-context shows which context a token belongs to:

cURL
curl https://api.withflintpay.com/v1/auth-context \
  -H "Authorization: Bearer $ACCESS_TOKEN"
JSON
{
  "data": {
    "auth_type": "oauth",
    "oauth_session_id": "session_example",
    "context_id": "ctx_example",
    "oauth_grant_id": "clg_01KEZF7C00Q6S9W2D8HF4YBN3T",
    "name": "Example store",
    "merchant_id": "mer_01KEZF7C00MPP0T9S0KH5BB4CC",
    "environment": "sandbox",
    "sandbox_id": "test_01KEZF7C00JVWYYN2SBWFFSA36",
    "scopes": [
      "payments.payment_intents.read",
      "payments.payment_intents.write"
    ]
  },
  "request_id": "req_01KWM64QRVW8VG2M0B0BAHH3Q3"
}

oauth_grant_id identifies the context's grant and stays the same across refreshes and consent changes. API requests made with a CLI token return the usual Flint error envelope:

  • HTTP 401
    details[0].reason unknown: the access token expired, its session ended, or consent removed or changed its context. Refresh for that context, and sign in again if the refresh returns invalid_grant. malformed with param X-API-Key: the request also sent X-API-Key. Send credentials only in Authorization.
  • HTTP 403
    details[0].reason oauth_context_mismatch: a Flint-Merchant-Id, Flint-Mode, or Flint-Sandbox-Id header names a different merchant, mode, or sandbox than the token's context, and param names the header. Remove the header, or refresh for the context you meant.
  • HTTP 403
    The context doesn't hold a scope the operation needs. details[0].missing_scopes lists them. Add them to the context.
  • HTTP 503
    SERVICE_UNAVAILABLERetry the same request
    Flint couldn't check the token. Retry the request.

Refresh and switch contexts#

Refresh to get a new access token for any context in the session. Naming a different context_id is how you switch contexts.

cURL
curl https://api.withflintpay.com/v1/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "grant_type=refresh_token" \
  --data-urlencode "client_id=flint-cli" \
  --data-urlencode "refresh_token=$REFRESH_TOKEN" \
  --data-urlencode "context_id=ctx_example"

The response has the same fields as the login response, with a new access_token and refresh_token.

FieldRequiredValue
context_idYesThe context the new access token is for.
scopeNoA space-separated subset of the context's scopes, to narrow this one access token. The context keeps its full set, and the next refresh without scope returns all of it. A scope the context doesn't hold returns invalid_scope.

Warning: Save every replacement refresh token

Each successful refresh replaces the session's refresh token. Store the new one before you make another request. Sending a refresh token that was already replaced ends the whole session with invalid_grant, including when you never received the response that replaced it. Don't retry a refresh automatically; if one fails without a response, sign in again.

  • Access tokens issued earlier keep working until they expire, in every context. A refresh doesn't revoke them.
  • invalid_context means context_id is missing or isn't part of this session. context_access_denied means consent removed the context, or the person lost access to it. Neither error uses up the refresh token: list the contexts and refresh for one of them.
  • invalid_grant means the session has ended. Start a new login.
  • Each refresh restarts the 30-day idle period. Near the 90-day limit, expires_in is shorter than 900 seconds.

List the contexts#

cURL
curl https://api.withflintpay.com/v1/oauth/contexts \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "client_id=flint-cli" \
  --data-urlencode "refresh_token=$REFRESH_TOKEN"
JSON
{
  "oauth_session_id": "session_example",
  "contexts": [
    {
      "id": "ctx_example",
      "name": "Example store",
      "merchant_id": "mer_01KEZF7C00MPP0T9S0KH5BB4CC",
      "environment": "sandbox",
      "sandbox_id": "test_01KEZF7C00JVWYYN2SBWFFSA36"
    },
    {
      "id": "ctx_example_live",
      "name": "Example store",
      "merchant_id": "mer_01KEZF7C00MPP0T9S0KH5BB4CC",
      "environment": "live"
    }
  ]
}

Send the current refresh token. Listing checks it without replacing it, so it works while the access token is expired. It grants no API access and doesn't return scopes; read those from a token response's scope or from GET /v1/auth-context.

environment is sandbox for a test-mode context, which also has sandbox_id, and live for a live one. The list holds only contexts the person can still use, in the order they were last approved, with no pagination. An empty list means no context is usable; review consent to add one.

Change what a session can access#

POST /v1/oauth/device/reauthorize starts a new browser approval for an existing session. Send the current refresh token and session_mode=contexts. The response, the browser step, and polling work as they do at login. Starting a review changes nothing, and a denied or expired review leaves the session as it was. Redeeming the device code for a superseded review returns 400 expired_token; start a new review. The existing session remains active.

There are two kinds of review:

Full reviewAdd scopes to one context
scope_modeOmitadditive
context_idOptional. The context the new access token should be for.Required. The context that gains scopes.
scopeEvery scope any context may hold afterward. The person can't approve a scope you didn't request.Only the scopes to add. A scope the context's mode doesn't allow returns invalid_scope.
In the browserThe person sees every current context, and can add or remove contexts and reduce scopes.The person sees that context and approves some or all of the added scopes.
After approvalThe approved contexts replace the old set. Access tokens for removed contexts, and for contexts whose scopes changed, stop working.The approved scopes join the context's existing scopes. If its scopes change, its earlier access tokens stop working. Other contexts are unchanged.

To add scopes to one context:

cURL
curl https://api.withflintpay.com/v1/oauth/device/reauthorize \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "client_id=flint-cli" \
  --data-urlencode "refresh_token=$REFRESH_TOKEN" \
  --data-urlencode "session_mode=contexts" \
  --data-urlencode "scope_mode=additive" \
  --data-urlencode "context_id=ctx_example" \
  --data-urlencode "scope=commerce.subscriptions.read"

merchant_id and sandbox_id limit a full review's contexts as they do at login. An additive review takes its merchant and mode from the context, so a merchant_id, sandbox_id, or mode that disagrees returns invalid_context, and a context the session no longer holds returns context_access_denied.

Redeem the review's device code at the token endpoint as you would at login. Redemption:

  • Keeps oauth_session_id and replaces the session's refresh token. Discard the refresh token you started the review with: sending it afterward ends the session.
  • Returns an access token for context_id while that context remains. Otherwise the token is for a sandbox context, so a full review must keep context_id's context or at least one sandbox context.
  • Reports in scope what the person granted, which can be a subset of what you asked for.

Approving a review cancels every other review of the same session that hasn't been redeemed yet. Start a new review if you still need one.

Sign out#

Revoke any access or refresh token from the session, including a refresh token that was already replaced:

cURL
curl https://api.withflintpay.com/v1/oauth/revoke \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "client_id=flint-cli" \
  --data-urlencode "token=$REFRESH_TOKEN" \
  --data-urlencode "token_type_hint=refresh_token"

A 200 with an empty body means the session has ended, whether this request ended it or it had already ended. An unknown token also returns 200. Every token in every context stops working, and webhook event streams opened with them close. People can also review and end their sessions on the dashboard's CLI sessions page.

OAuth errors#

Every call above returns failures as error with a fixed error_description. Branch on error; the description is for people.

StatuserrorCause and next step
400invalid_requestThe body isn't a valid form, repeats a field, has a query string, or sends a field the endpoint doesn't define, or a required field is missing or invalid. Fix the request.
401invalid_clientclient_id isn't flint-cli, or the request sent a client secret or an Authorization header.
400invalid_grantThe refresh token is spent, revoked, or expired, or the session has ended. Start a new login.
400invalid_scopeA scope is unknown, missing, not allowed in the context's mode, or outside the context's scopes on refresh.
400invalid_contextcontext_id is missing or isn't in this session, or an additive review's hints disagree with its context.
400context_access_deniedThe context was removed, or the person lost access to it. Choose another context.
400unsupported_grant_typeUse urn:ietf:params:oauth:grant-type:device_code or refresh_token.
400authorization_pending, slow_down, access_denied, expired_tokenDevice code polling only. See Poll for tokens.
429temporarily_unavailableA rate limit outside device code polling. Wait at least Retry-After seconds; Flint-Rate-Limited-Reason names the limit.
500server_errorRetry later, and quote X-Request-Id to Flint support.
Flint CLISign in, switch contexts, add permissions, and sign out from the terminal.OAuthThe shared token and revocation endpoints and their client rules.OAuth discovery and signing keysEndpoints, grants, and the scopes the CLI can request.AuthenticationAPI keys, scopes, and the other credentials Flint accepts.

List authorized CLI contexts#

POST/v1/oauth/contexts

No API key required

Rate limit: per IP

First-party public client flint-cli. Uses application/x-www-form-urlencoded and flat OAuth JSON without a Flint envelope. No client secret or HTTP Basic authentication. Requests are not replayed through idempotency caching. Flint session control extension. Authenticates the current refresh token without consuming or rotating it. Returns only currently authorized contexts, including an empty list. Listing does not change consent, revoke contexts, or update session state. Read approved scopes from the selected context's token response or /v1/auth-context. No business access is granted. Default order: the order of the contexts approved at the latest consent.

Response · 200

OAuth response. Credential responses include Cache-Control: no-store and Pragma: no-cache.

contextsarray of objectRequired
oauth_session_idstringRequired
curl -X POST https://api.withflintpay.com/v1/oauth/contexts \
  -H "Flint-Version: 2026-09-07"

Start CLI browser login#

POST/v1/oauth/device/authorize

No API key required

Rate limit: per IP

First-party public client flint-cli. Uses application/x-www-form-urlencoded and flat OAuth JSON without a Flint envelope. No client secret or HTTP Basic authentication. Requests are not replayed through idempotency caching. Send session_mode=contexts for a session that supports additive reauthorization. The required scope field contains the full requested set as space-separated names. Normal login sends the unchanged x-flint-cli-bootstrap-scopes; login with extra permissions sends the desired baseline and additions. No scopes are implicitly added. The browser may approve a subset per context; token scope reports the approved set. Unknown scopes return HTTP 400 invalid_scope. Selected scopes are checked against the selected merchant and environment before approval.

Response · 200

OAuth response. Credential responses include Cache-Control: no-store and Pragma: no-cache.

device_codestringRequired
expires_inintegerRequired
intervalintegerRequired
user_codestringRequired
verification_uristringRequired
verification_uri_completestringRequired
curl -X POST https://api.withflintpay.com/v1/oauth/device/authorize \
  -H "Flint-Version: 2026-09-07"

Review CLI session consent#

POST/v1/oauth/device/reauthorize

No API key required

Rate limit: per IP

First-party public client flint-cli. Uses application/x-www-form-urlencoded and flat OAuth JSON without a Flint envelope. No client secret or HTTP Basic authentication. Requests are not replayed through idempotency caching. Flint session control extension. Starts device consent bound to the current session and user without consuming the refresh token. Without scope_mode, approval replaces consent; denial or expiry leaves consent unchanged. Redeem using the device_code grant to rotate the latest family token. Set scope_mode=additive and context_id to request additional space-separated scope names for one existing context. Merchant and environment are derived from that context; conflicting hints return invalid_context. Approval unions only browser-approved additions with that context's existing scopes and preserves all other contexts. No implicit bootstrap scopes are added. Unsupported or environment-disallowed scopes return HTTP 400 invalid_scope; browser denial returns access_denied when polling. Partial approval succeeds and scope in the token response is authoritative. A superseded consent review returns expired_token when its device code is redeemed; start a new review. The existing session remains active.

Response · 200

OAuth response. Credential responses include Cache-Control: no-store and Pragma: no-cache.

Same response as Start CLI browser login.

curl -X POST https://api.withflintpay.com/v1/oauth/device/reauthorize \
  -H "Flint-Version: 2026-09-07"

Was this helpful?