Core conceptsSeller authorization

Seller authorization

Before you can read or write a seller's data, they must grant your app scoped access. The flow is OAuth-style: you send them to a consent page, they authenticate, and you exchange the resulting code for tokens.

Signing the auth endpoints. /open/v1/auth/token, /token/refresh and /revoke are signed exactly like any other call — HMAC v3 with a nonce. Send X-MallPlus-Signature-Version and X-MallPlus-Nonce with those signed requests. They are app-authenticated, so they carry no seller token. Signature version 2 is rejected on these endpoints.

{timestamp}:{clientId}:GET:/open/v1/auth/authorize:{queryCanonical}:{sha256("")}:{nonce}

The flow

Sandbox consent sequence

  1. seller-verify

    Validate the sandbox seller login on POST /open/v1/auth/seller-verify. Sandbox consent uses the fixed OTP 111111; no SMS is sent.

  2. seller-verify-otp

    Verify the OTP on POST /open/v1/auth/seller-verify-otp.

  3. seller-consent

    Record consent on POST /open/v1/auth/seller-consent, then continue to the redirect callback.

  1. Build the authorization URL
    GET/open/v1/auth/authorizeHMAC-signed, no seller token

    Sign the request as usual and redirect the seller to it. The platform validates your registered redirect_uri — a mismatch returns REDIRECT_URL_MISMATCH.

  2. The seller consents

    They authenticate and approve the scopes your app requests. On sandbox, identity is a sandbox shop (Shop ID plus password). If they decline, the platform calls POST /open/v1/auth/deny and returns them to your registered redirect URL.

  3. Receive the authorization code

    The seller lands back on your redirect_uri with a code parameter.

    const res = await fetch(authorizeUrl, { redirect: 'manual' })

    The code expires in 10 minutes and is single-use. Exchange it immediately. A second exchange returns AUTH_CODE_USED.

  4. Exchange it for tokens
    POST/open/v1/auth/token

    The request body is strict. Send code, client_id, and seller_id. Standard OAuth fields such as grant_type, redirect_uri, and client_secret are unsupported and rejected.

    {
      "code": "auth_code_...",
      "client_id": "mp_...",
      "seller_id": "seller_..."
    }
    {
      "success": true,
      "data": {
        "access_token":  "…",
        "refresh_token": "…",          // single-use; rotates on every refresh
        "expires_in":    14400,          // seconds — 4 hours
        "expires_at":    "2026-08-15T13:30:00.000Z",
        "seller_id":     "seller_nike_ph",
        "seller_name":   "Nike Philippines",   // may be null
        "scopes":        ["catalog:read", "orders:read", "orders:write"]
      }
    }

    Store seller_id — it is the value you send as X-MallPlus-Seller-Id.

    expires_at is ISO-8601 UTC. Example token expiry values include "expires_at": "2026-07-16T10:30:00.000Z" on exchange and "expires_at": "2026-07-16T14:30:00.000Z" after refresh.

    Check scopes against what you requested: a seller can consent to fewer scopes than you asked for, and calls outside the granted set return 403 FORBIDDEN.

  5. Call seller-scoped endpoints

    Send X-MallPlus-Access-Token and X-MallPlus-Seller-Id alongside your five HMAC headers. Remember these two are not signed.

  6. Refresh before expiry
    POST/open/v1/auth/token/refresh

    Single-flight your refreshes. Refresh tokens rotate on use, and reuse of a rotated-out token is treated as compromise: the entire token chain is revoked and you get REFRESH_TOKEN_REUSED. Two concurrent refreshes will lock you out and require the seller to re-authorize. Serialise refresh through a mutex or a single worker.

Token lifetimes

TokenLifetimeNotes
Authorization code10 minutesSingle-use
Access token4 hoursSHA-256 hashed at rest
Refresh token30 daysRotates on every use; reuse revokes the chain

Subscribe to authorization.expiring to prompt re-consent before a seller's grant lapses, and to authorization.revoked to stop calling immediately when they disconnect.

Authorization expiry event

AUTHORIZATION.EXPIRING / authorization.expiring fires 7 days before the 365-day grant expires.

{
  "event_type": "authorization.expiring",
  "event_id": "evt_01HK...",
  "authorization_id": "auth_01HK...",
  "expires_at": 1798675200
}

expires_at in the event payload is Unix epoch seconds.

Listing your authorizations

GET/open/v1/authorizationsHMAC-signed, no seller token

Lists the sellers who have authorized your app — the source of the seller_id values used by seller-scoped calls and by revocation. Defaults to status=active; pass status=all (or revoked / expired) for the full history. Standard page/limit pagination.

{
  "success": true,
  "data": [{
    "seller_id":   "sel_mWeoDRkTMksY",
    "seller_name": "Nike Philippines",   // may be null
    "scopes":      ["catalog:read", "orders:read"],
    "status":      "active",             // active | revoked | expired
    "granted_at":  "2026-08-15T09:30:00.000Z",
    "expires_at":  "2027-08-15T09:30:00.000Z"  // 365-day wall — re-consent after this
  }],
  "meta": { "page": 1, "limit": 20, "total": 1 }
}

Revocation

POST/open/v1/auth/revoke

Either side may revoke. The call is HMAC-signed and app-authenticated (no seller token). The body is strict and requires both ids; client_id must match the authenticated app:

{
  "client_id": "mp_...",
  "seller_id": "sel_..."
}

The response is { "revoked": true }false means there was no active grant for that seller. After revocation every seller-scoped read returns 404 NOT_FOUND rather than 403 — the platform does not confirm the existence of resources you can no longer see.

Authorization errors

CodeMeaningResolution
REDIRECT_URL_MISMATCHRedirect URI is not registered for this appRegister it in the console; exact match including scheme and trailing slash
INVALID_AUTHORIZATION_CODECode not recognisedRestart the consent flow
AUTH_CODE_EXPIREDOlder than 10 minutesExchange immediately on receipt
AUTH_CODE_USEDAlready exchangedCodes are single-use; store the resulting tokens
INVALID_REFRESH_TOKENNot recognised or already rotatedRe-authorize the seller
REFRESH_TOKEN_EXPIREDOlder than 30 daysRe-authorize the seller
REFRESH_TOKEN_REUSEDA rotated-out token was replayed — chain revokedSingle-flight your refresh logic, then re-authorize
AUTHORIZATION_REVOKEDThe seller disconnected your appStop calling; prompt re-consent
RE_AUTHORIZATION_REQUIREDScopes changed or grant invalidatedSend the seller through consent again