ShopEazydevelopers
Get started

Quickstart: Merchant

Register an OAuth client, authorize with PKCE, then read a session and a store's orders.

Beta

The Merchant API is in Beta. Connections are opened and paused by ShopEazy. While new AI connections are paused, registering a client answers 503 temporarily_unavailable.

The Merchant API acts as a person. Your app never holds a merchant's password. The merchant signs in at ShopEazy, sees what your app asks for, and chooses what to allow. You get a short-lived access token and a refresh token.

You will register a client, send a merchant through the consent screen with PKCE, exchange the code for tokens, then call getSession and listOrders. Read Merchant OAuth for the full rules.

The authorization server is the ShopEazy console, at https://app.shopeazy.co. The API is at https://api.shopeazy.co.

1. Register a client

Dynamic client registration is open to any public client. There are no client secrets: ShopEazy only supports public clients, so your app proves itself with PKCE.

curl https://app.shopeazy.co/oauth/register \
  -H "Content-Type: application/json" \
  -d '{
    "client_name": "Orders dashboard",
    "redirect_uris": ["http://127.0.0.1:8976/callback"],
    "token_endpoint_auth_method": "none",
    "grant_types": ["authorization_code", "refresh_token"]
  }'
{
  "client_id": "<client_id>",
  "client_id_issued_at": 1792636800,
  "client_name": "Orders dashboard",
  "redirect_uris": ["http://127.0.0.1:8976/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none"
}

Give between 1 and 10 redirect URIs. Each must be https, or http on localhost, 127.0.0.1 or [::1]. The consent screen shows the merchant your client_name and the host the merchant is sent back to, so name your app honestly.

2. Make a PKCE pair

Make a random code_verifier of 43 to 128 characters from A-Z a-z 0-9 - . _ ~. The code_challenge is the base64url SHA-256 of the verifier, with no padding. Only S256 is accepted.

CODE_VERIFIER=$(openssl rand -base64 48 | tr -d '=+/\n' | cut -c1-64)
CODE_CHALLENGE=$(printf '%s' "$CODE_VERIFIER" | openssl dgst -sha256 -binary | openssl base64 -A | tr '+/' '-_' | tr -d '=')

Open this URL in the merchant's browser. Ask only for the scopes you need. The merchant can only narrow your request. resource names the API the token is for, so keep it exactly as shown.

https://app.shopeazy.co/oauth/authorize
  ?response_type=code
  &client_id=<client_id>
  &redirect_uri=http%3A%2F%2F127.0.0.1%3A8976%2Fcallback
  &code_challenge=<code_challenge>
  &code_challenge_method=S256
  &resource=https%3A%2F%2Fapi.shopeazy.co
  &scope=store%3Aread%20orders%3Aread
  &state=<random state>

The merchant signs in, picks the business and stores to connect, and chooses Allow or Cancel. On Allow, the browser is sent to your redirect URI with a one-time code:

http://127.0.0.1:8976/callback?code=<code>&state=<random state>&iss=https%3A%2F%2Fapp.shopeazy.co

On Cancel you get ?error=access_denied instead. Check state matches what you sent. The code lives for 60 seconds and works once.

4. Exchange the code for tokens

curl https://app.shopeazy.co/oauth/token \
  -d grant_type=authorization_code \
  -d client_id=<client_id> \
  -d code=<code> \
  -d redirect_uri=http://127.0.0.1:8976/callback \
  -d code_verifier="$CODE_VERIFIER" \
  -d resource=https://api.shopeazy.co
{
  "access_token": "<access_token>",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "<refresh_token>",
  "scope": "store:read orders:read"
}

The access token lasts one hour. The refresh token lasts 60 days and is single use: each refresh returns a new pair. scope is what the merchant actually allowed, which may be less than you asked for.

5. Call the API

Your app can also find the sign-in details from the API alone. GET https://api.shopeazy.co/.well-known/oauth-protected-resource names the authorization server and the scopes. A call without a valid token answers 401 with a WWW-Authenticate header that points to it.

Send the access token as a Bearer token. First, getSession says who connected and which stores you can reach.

curl https://api.shopeazy.co/v1/merchant/session \
  -H "Authorization: Bearer <access_token>"
{
  "data": {
    "user": {
      "id": "user_123",
      "email": "thandi@example.co.za",
      "displayName": "Thandi Mkhize",
      "photoURL": null
    },
    "stores": [
      {
        "id": "store_123",
        "workspaceId": "workspace_123",
        "name": "Thandi's Teas",
        "slug": "thandis-teas",
        "logoUrl": null,
        "currency": "ZAR",
        "role": "owner",
        "kitTypes": ["physical"]
      }
    ],
    "defaultStoreId": "store_123",
    "grant": {
      "id": "grant_123",
      "clientName": "Orders dashboard",
      "scopes": ["store:read", "orders:read"],
      "storeIds": null
    }
  }
}

grant is only present for an OAuth connection. It tells your app what it was granted, so you can skip a call the token cannot make. storeIds: null means every store the person can reach.

Now list that store's orders. The store is always named in the path.

curl "https://api.shopeazy.co/v1/merchant/stores/store_123/orders?limit=2" \
  -H "Authorization: Bearer <access_token>"
{
  "data": {
    "items": [
      {
        "id": "order_789",
        "number": "1042",
        "channel": "online",
        "customerName": "Sipho Dlamini",
        "soldByName": null,
        "total": { "amount": 23000, "currency": "ZAR" },
        "status": "open",
        "paymentStatus": "paid",
        "fulfillmentStatus": "unfulfilled",
        "placedAt": 1792636800000,
        "lineCount": 1,
        "kinds": ["physical"]
      }
    ],
    "nextCursor": null
  }
}

With the typed client, which takes your token from a function:

import { createMerchantClient } from '@shopeazy/merchant-client';

const api = createMerchantClient({
  baseUrl: 'https://api.shopeazy.co',
  getToken: () => accessToken,
});

const session = await api.getSession();
const page = await api.listOrders({
  path: { storeId: session.defaultStoreId! },
  query: { limit: 2 },
});

6. Refresh

When the access token expires, or answers 401 invalid_token, trade the refresh token for a new pair. Store the new refresh token: the old one is spent.

curl https://app.shopeazy.co/oauth/token \
  -d grant_type=refresh_token \
  -d client_id=<client_id> \
  -d refresh_token=<refresh_token>

If a spent refresh token is ever presented again, ShopEazy assumes it leaked and ends the whole connection. The merchant has to connect again.

What to know before you go further

  • A store the person cannot reach is 404 store_not_found, never 403. That includes a store your connection was not given.
  • Writes need an Idempotency-Key. Every non-GET operation requires one. See Idempotency.
  • Some operations are only for the ShopEazy app. They accept only the ShopEazy app's own sign-in. A connected app that calls one gets 403 forbidden.
  • Scopes only narrow. Your real power is the scopes granted, and the person's role today, and the plan, checked on every call.

Next: Connect an AI app.

On this page