MerchantFlowMerchantFlow Docs
IntegrationsPublic API

Public API Authentication - OAuth 2.1 with PKCE

Step-by-step OAuth 2.1 authorization code flow for connecting a partner application to a MerchantFlow merchant, including PKCE, the resource indicator, and refresh tokens.

Authentication

The Public API uses the OAuth 2.1 authorization code flow with PKCE. A merchant approves your application in their browser, and you receive tokens scoped to one of their stores.

What you need

MerchantFlow provisions your application and gives you:

  • client_id
  • client_secret - shown once, stored hashed, and not recoverable afterwards
  • The redirect URI you registered, which must be https

Your application is a confidential client: it authenticates to the token endpoint with its secret, so that secret must stay on your server. PKCE is required on top of it.

Endpoints

PurposeURL
Authorizehttps://merchantflow.ai/api/auth/oauth2/authorize
Tokenhttps://merchantflow.ai/api/auth/oauth2/token
Discoveryhttps://merchantflow.ai/.well-known/oauth-authorization-server
Resource metadatahttps://merchantflow.ai/.well-known/oauth-protected-resource/api/v1

You can discover the first two from the authorization-server document rather than hardcoding them; the resource metadata document names which authorization server to ask.

Step 1: send the merchant to authorize

Generate a PKCE verifier and challenge, and a random state. Then redirect:

https://merchantflow.ai/api/auth/oauth2/authorize
  ?response_type=code
  &client_id=YOUR_CLIENT_ID
  &redirect_uri=https://app.example.com/oauth/merchantflow/callback
  &scope=store:read%20orders:read%20profitability:read%20offline_access
  &state=RANDOM_STATE
  &code_challenge=BASE64URL_SHA256_OF_VERIFIER
  &code_challenge_method=S256

Three parameters deserve attention.

code_challenge_method must be S256. plain is rejected.

Do not send a resource parameter here. MerchantFlow runs two APIs behind one authorization server - this one and the MCP server for AI clients - and the authorize endpoint currently accepts a resource indicator only for the MCP server. Sending resource=https://merchantflow.ai/api/v1 at this step is rejected with invalid_target before the merchant sees a login screen. Your scopes are what identify the Public API at this step; the resource indicator goes in the token request instead (Step 3).

Include offline_access if you want to stay connected. Without it you get an access token that expires in one hour and no refresh token, and the merchant has to repeat this whole flow. Almost every integration wants it.

Step 2: the merchant approves

MerchantFlow signs the merchant in if needed, then shows a consent screen naming your application, the scopes you asked for, and - importantly - a store picker.

A MerchantFlow account can contain several stores. The merchant chooses exactly one for your application, and your tokens will only ever read that store. A merchant user holds one connection per application: if they run the flow again and pick a different store, the connection moves to that store and the tokens you hold for the previous store stop working with 401 invalid_token. To read two stores, two different MerchantFlow users must each connect.

The consent screen also states plainly, under the orders scope, that your application receives a pseudonymous customer reference and never receives names, email addresses, phone numbers, or shipping addresses, and its footer repeats that the connection is read-only. Your integration should not contradict that.

Step 3: exchange the code

The merchant returns to your redirect URI with code and state. Verify state matches, then exchange:

curl -X POST https://merchantflow.ai/api/auth/oauth2/token \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  -d grant_type=authorization_code \
  -d code="$CODE" \
  -d redirect_uri=https://app.example.com/oauth/merchantflow/callback \
  -d code_verifier="$PKCE_VERIFIER" \
  -d resource=https://merchantflow.ai/api/v1

resource binds the token to the Public API (RFC 8707). If you omit it here, MerchantFlow infers it from your scopes and your registered client, and the result is the same; sending it makes the intent unambiguous.

{
  "access_token": "eyJhbGciOi...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "mf_rt_...",
  "scope": "store:read orders:read profitability:read offline_access"
}

The redirect_uri must match byte-for-byte the one you sent at authorize.

Step 4: call the API

Authorization: Bearer <access_token>

Header only. MerchantFlow does not accept tokens in query strings, because they leak into access logs, proxy logs and Referer headers.

Refreshing

Access tokens last one hour. Refresh tokens last 90 days and rotate: each exchange issues a new refresh token and revokes the previous one, so store the new value every time.

curl -X POST https://merchantflow.ai/api/auth/oauth2/token \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  -d grant_type=refresh_token \
  -d refresh_token="$REFRESH_TOKEN"

An actively used connection renews indefinitely. One that goes unused for 90 days lapses and the merchant must reconnect.

When a connection stops working

Statuserror.codeWhat happened
401missing_tokenNo Authorization: Bearer header on the request.
401invalid_tokenExpired or malformed, or issued for a store the connection no longer points at. Refresh, or re-run the flow.
401grant_revokedThe merchant disconnected your app, or their workspace no longer has a plan. Do not retry - ask them to reconnect.
401invalid_token_claimsThe token is not bound to a store. Re-run the authorization flow.
403client_not_authorizedYour application is not provisioned, or has been disabled. Contact MerchantFlow.
403insufficient_scopeThe token lacks the scope this endpoint needs.

A merchant can disconnect you at any time, with Disconnect on the Connected apps page in their settings (/dashboard/settings/developer/apps). That takes effect within about a minute, not at token expiry. Treat grant_revoked as a terminal state for that connection: stop polling, and surface it to your own user rather than retrying in a loop.

Every 401 also carries a WWW-Authenticate header pointing at the protected-resource metadata document, so a conformant OAuth client can rediscover the authorization server without configuration.

Handling multiple merchants

One connection is one merchant user's one store. Store the tokens per connection, and use GET /api/v1/account to confirm which store a given token reads - the store.id it returns is stable and is the right key for your own records.

Do not assume a merchant's store id from anything else, and do not send a store identifier with your requests. The token itself determines what you can read.

Next

Last updated: September 22, 2026

Last updated on

On this page