ecomone developers
Open Ecomone
Docs / Build an app

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.

i

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#

  1. Register once

    Your app registers a client and receives a client_id.

  2. Merchant consents

    You redirect the merchant to Ecomone. They log in, pick the shops and approve the scopes.

  3. Exchange code

    Ecomone redirects back with a one-time code. Your server exchanges it for an access and a refresh token.

  4. 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

client_namestringrequired

1-120 characters (trimmed).

redirect_urisstring[]required

1-5 URIs, max 2000 chars each, no fragment; https, or http only for localhost/127.0.0.1/[::1]. Duplicates removed.

token_endpoint_auth_methodstring

Must be "none".

grant_typesstring[]

Only authorization_code and refresh_token allowed.

logo_uristring

https URL.

client_uristring

https URL.

Request
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"]}'
201Response
{
  "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

response_typestringrequired

Must be code.

client_idstringrequired

Registered client id.

redirect_uristringrequired

Must exactly equal one registered redirect URI.

code_challengestringrequired

PKCE challenge (base64url SHA-256 of the verifier).

code_challenge_methodstringrequired

Must be S256.

scopestring

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.

statestring

Returned unchanged.

resourcestring

Stored with the grant (RFC 8707 resource indicator); not otherwise validated.

Build the authorization URL
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).

GrantParametersNotes
authorization_codegrant_type=authorization_code
code
redirect_uri
client_id
code_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_tokengrant_type=refresh_token
refresh_token
client_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).
Request
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>
200Response
{
  "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
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
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)
GET /.well-known/oauth-authorization-server
{
  "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#

TokenPrefixLifetime
Authorization codeac_10 minutes, single use
Access tokenat_1 hour
Refresh tokenrt_60 days, rotated on every use
Client idoc_Permanent

Authorize requests

Requests go straight from your browser to https://api.ecomone.shop. Your credentials stay in this browser tab and are never sent to this documentation site.