Build an app with OAuth
OAuth 2.1 authorization server for public clients (no client secret): dynamic client registration, authorization code with mandatory PKCE S256, refresh token rotation with reuse detection, token revocation. Built for MCP clients such as Claude, but the access tokens also work on the REST API.
Ecomone supports public clients only: there is no client secret. Security comes from PKCE (S256), exact redirect URI matching and short-lived access tokens. The same flow is used by Claude and other MCP clients.
How it works#
Register once
Your app registers a client and receives a
client_id.Merchant consents
You redirect the merchant to Ecomone. They log in, pick the shops and approve the scopes.
Exchange code
Ecomone redirects back with a one-time code. Your server exchanges it for an access and a refresh token.
Call the API
Send
Authorization: Bearer at_…to the REST API or MCP. Refresh before the access token expires.
1. Register your client#
POST https://app.ecomone.shop/api/oauth/register — dynamic client registration (RFC 7591). Registration is rate limited (20 per hour per IP); register once and store the client_id.
Body parameters
1-120 characters (trimmed).
1-5 URIs, max 2000 chars each, no fragment; https, or http only for localhost/127.0.0.1/[::1]. Duplicates removed.
Must be "none".
Only authorization_code and refresh_token allowed.
https URL.
https URL.
curl -X POST "https://app.ecomone.shop/api/oauth/register" \
-H "Content-Type: application/json" \
-d '{"client_name":"Acme Fulfillment","redirect_uris":["https://acme.example/ecomone/callback"]}'{
"client_id": "oc_0123456789abcdef0123456789abcdef",
"client_id_issued_at": 1794556800,
"client_name": "My Integration",
"redirect_uris": [
"https://example.com/callback"
],
"grant_types": [
"authorization_code",
"refresh_token"
],
"token_endpoint_auth_method": "none"
}2. Send the merchant to the consent screen#
Create a random code_verifier, derive the code_challenge (SHA-256, base64url) and redirect the merchant's browser to https://app.ecomone.shop/oauth/authorize.
Query parameters
Must be code.
Registered client id.
Must exactly equal one registered redirect URI.
PKCE challenge (base64url SHA-256 of the verifier).
Must be S256.
Space-separated scopes. If given, the user can only narrow them. If omitted, the user chooses a preset ('build' default, 'build and manage') or individual scopes.
Returned unchanged.
Stored with the grant (RFC 8707 resource indicator); not otherwise validated.
import crypto from "node:crypto";
const verifier = crypto.randomBytes(32).toString("base64url");
const challenge = crypto.createHash("sha256").update(verifier).digest("base64url");
const state = crypto.randomBytes(16).toString("hex");
// store verifier + state in the user's session, then redirect:
const url = new URL("https://app.ecomone.shop/oauth/authorize");
url.search = new URLSearchParams({
response_type: "code",
client_id: "oc_...",
redirect_uri: "https://acme.example/ecomone/callback",
scope: "orders:read orders:fulfillment",
state,
code_challenge: challenge,
code_challenge_method: "S256",
});- Not logged in: redirect to /login?next=... and back.
- Unknown client or redirect_uri not registered: error page, no redirect.
- response_type != code, method != S256, missing challenge or any unknown scope: redirect to redirect_uri with error=invalid_request (&state).
- User picks one or more shops. Only shops the user OWNS (and that are active) are offered; each selected shop needs an active subscription that includes MCP.
- Option 'also grant future shops automatically' (auto_new_shops).
- Approve: redirect_uri?code=ac_...&state=... (code valid 10 minutes, single use). Deny: redirect_uri?error=access_denied&state=...
After approval Ecomone redirects to your redirect_uri with code and state. Always compare state with the value you stored.
3. Exchange the code for tokens#
POST https://app.ecomone.shop/api/oauth/token with application/x-www-form-urlencoded (JSON is accepted too).
| Grant | Parameters | Notes |
|---|---|---|
authorization_code | grant_type=authorization_codecoderedirect_uriclient_idcode_verifier (43-128 chars) | Code must not be expired, must match client_id and redirect_uri, PKCE S256 must verify. Re-using a code revokes the tokens issued for it. |
refresh_token | grant_type=refresh_tokenrefresh_tokenclient_id | Rotation: every refresh returns a new refresh token and invalidates the old one. Presenting an already-used refresh token revokes the whole token chain. A scope parameter is ignored (scopes stay as granted). |
curl -X POST "https://app.ecomone.shop/api/oauth/token" \
-d grant_type=authorization_code \
-d code=ac_... \
-d redirect_uri=https://acme.example/ecomone/callback \
-d client_id=oc_... \
-d code_verifier=<verifier>{
"access_token": "at_000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "rt_000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"scope": "shop:read products:read products:write"
}4. Call the API#
curl "https://api.ecomone.shop/api/public/v1/orders?shop_id=<SHOP_UUID>" -H "Authorization: Bearer at_..."If the merchant connected several shops, pass shop_id (REST) or the shop argument (MCP tools).
5. Refresh tokens#
Access tokens expire after one hour. Exchange the refresh token with grant_type=refresh_token. Every refresh returns a new refresh token; the old one stops working. Reusing an old refresh token is treated as theft and revokes the whole grant, so store the newest token atomically.
curl -X POST "https://app.ecomone.shop/api/oauth/token" \
-d grant_type=refresh_token \
-d refresh_token=rt_... \
-d client_id=oc_...Revoke#
POST https://app.ecomone.shop/api/oauth/revoke with token (access or refresh token) and client_id (RFC 7009). Merchants can also disconnect your app in the backend at any time; then your tokens return 401.
Discovery metadata#
Standard clients can discover everything automatically:
/.well-known/oauth-authorization-server(RFC 8414)/.well-known/oauth-protected-resource(RFC 9728)
{
"issuer": "https://app.ecomone.shop",
"authorization_endpoint": "https://app.ecomone.shop/oauth/authorize",
"token_endpoint": "https://app.ecomone.shop/api/oauth/token",
"registration_endpoint": "https://app.ecomone.shop/api/oauth/register",
"revocation_endpoint": "https://app.ecomone.shop/api/oauth/revoke",
"scopes_supported": "<all scopes>",
"response_types_supported": [
"code"
],
"grant_types_supported": [
"authorization_code",
"refresh_token"
],
"code_challenge_methods_supported": [
"S256"
],
"token_endpoint_auth_methods_supported": [
"none"
],
"op_policy_uri": "https://app.ecomone.shop/datenschutz",
"op_tos_uri": "https://app.ecomone.shop/agb"
}Token lifetimes#
| Token | Prefix | Lifetime |
|---|---|---|
| Authorization code | ac_ | 10 minutes, single use |
| Access token | at_ | 1 hour |
| Refresh token | rt_ | 60 days, rotated on every use |
| Client id | oc_ | Permanent |