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_idclient_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
| Purpose | URL |
|---|---|
| Authorize | https://merchantflow.ai/api/auth/oauth2/authorize |
| Token | https://merchantflow.ai/api/auth/oauth2/token |
| Discovery | https://merchantflow.ai/.well-known/oauth-authorization-server |
| Resource metadata | https://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=S256Three 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/v1resource 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
| Status | error.code | What happened |
|---|---|---|
| 401 | missing_token | No Authorization: Bearer header on the request. |
| 401 | invalid_token | Expired or malformed, or issued for a store the connection no longer points at. Refresh, or re-run the flow. |
| 401 | grant_revoked | The merchant disconnected your app, or their workspace no longer has a plan. Do not retry - ask them to reconnect. |
| 401 | invalid_token_claims | The token is not bound to a store. Re-run the authorization flow. |
| 403 | client_not_authorized | Your application is not provisioned, or has been disabled. Contact MerchantFlow. |
| 403 | insufficient_scope | The 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
MerchantFlow Public API - Read a Merchant's Profitability Data
How third-party applications connect to MerchantFlow with OAuth 2.1 and read order economics, products, and profit and loss on a merchant's behalf.
Public API Scopes - What Each Permission Grants
The four read-only scopes a MerchantFlow partner application can request, what data each unlocks, and how merchants see them on the consent screen.