| # | Where it failed | What you see | Jump to |
|---|---|---|---|
| 1 | The authorize redirect — the merchant clicked Install | Browser lands on the merchant's dashboard with ?error_code=EC… | Install & authorize errors |
| 2 | The token endpoint — POST https://oauth.zid.sa/oauth/token | A JSON error body | Token endpoint errors |
| 3 | An API call — https://api.zid.sa/v1/… | Zid's error envelope with a message.code | API request errors |
{
"status": "error",
"message": {
"type": "error",
"code": "ERROR_SESSION_INVALID",
"name": "Sorry",
"description": "Invalid authentication"
}
}message.code is what you branch on. It is stable.message.description is not. It is editorial, localised text. Seecode and name are sometimes null — the table in each section says when.422 replaces message with a validations array and sets a different status:{
"status": "validation_error",
"message": {
"type": "validation_errors",
"code": "ERROR_POPUP_OK",
"name": "Inputs",
"description": "Input errors!",
"validations": [
{ "field": "email", "errors": ["The email field is required."] }
]
}
}message.description is always the whole story will silently dropstatus first.error_code query parameter:https://web.zid.sa/…?error_code=EC3⚠️ The merchant will not notice this. The dashboard does not currently render a
message forerror_code, so from the merchant's point of view the install button simply
did nothing. When a merchant reports "installing your app does nothing", ask them to
paste the full URL from their address bar — theerror_codeis the only diagnostic
signal that exists.
EC codes| Code | OAuth equivalent | What actually happened | What to do |
|---|---|---|---|
| EC1 | invalid_scope | Your app is requesting a scope Zid does not recognise. Usually a scope that was renamed or retired while your app still had it granted. | Open your app in the Partner Dashboard, re-save the scope list, and confirm every scope appears in the current scopes list. Contact Partner Support if a scope you need is missing. |
| EC2 | invalid_grant | The client_id in the authorize URL does not belong to the app record Zid resolved. This is an identity-hijack guard. | You are almost certainly mixing environments or apps — a staging client_id with a production authorize URL, or a copied URL from another app. Re-copy client_id from the Partner Dashboard for this app. |
| EC3 | access_denied | Two distinct causes: (a) the merchant clicked Decline on the consent screen; (b) your app is not published and this store has no private grant for it. | (a) Nothing to fix — ask the merchant to retry and approve. (b) Check your app's listing status. For testing, use a development store or request a private grant for the merchant's store. |
| EC4 | access_denied (Zid-specific) | Your app has paid plans, and this store has no active plan subscription for it. | The merchant must complete or renew the app subscription before the install can finish. Confirm the subscription state in the Partner Dashboard before debugging anything else. |
| EC5 | invalid_request | client_id was missing, empty, or matched no app at all. | Check the authorize URL actually carries client_id=…. A common cause is a templating bug emitting an empty value. |
| UNKNOWN | — | An OAuth error outside the mapped set. | Capture the full redirect URL, the client_id, the store id, and the timestamp, and open a ticket. |
EC3 is the ambiguous one. "Merchant declined" and "app not visible to this store"
produce the identical code. Rule out the listing/private-grant cause first, because it is
the one you can fix.
| Situation | What the merchant sees |
|---|---|
| Your app needs the Direct Integration (APIs) add-on and the store is not subscribed | HTTP 403 page: "This app needs the Direct Integration (APIs) add-on. Subscribe to it to activate the app." — with a button to subscribe |
| The app requires an active Mazeed account that the store does not have | HTTP 403 page: "This app requires your store to be activated in Mazeed. Please activate to complete the installation." |
Your app is a shipping app, or the client_id resolved to no app | HTTP error page carrying the real OAuth message and status, rather than an EC redirect |
error_code, no error page, just the merchant's dashboard. The merchant is not logged…/apps/{app_id}, or the shipping-providers page for shipping apps) rather than returningclient_id matches this app in this environment.redirect_uri exactly matches a registered allowed redirection URL — scheme, host,response_type=code.POST https://oauth.zid.sa/oauth/token⚠️ Zid's token endpoint returns two different error shapes. Most failures come back in
Zid's standard envelope with HTTP 400. One specific failure comes back in the RFC 6749
shape. Handle both — branch on whether the body has a top-levelerrorkey.
code and name both null. The OAuth reason is inmessage.description:{
"status": "error",
"message": {
"type": "error",
"code": null,
"name": null,
"description": "Client authentication failed"
}
}Note on status codes: these are returned as 400 even where RFC 6749 specifies a
different status —invalid_clientand an invalid refresh token are both 400 here rather
than 401. Do not dispatch on the HTTP status alone at this endpoint; read the description.
description contains | OAuth error | Cause | Fix |
|---|---|---|---|
| "Client authentication failed" | invalid_client | Wrong client_id, wrong client_secret, or mixed environments | Re-copy both from the Partner Dashboard. Check for whitespace or newlines from copy-paste, and that your secret store isn't truncating. |
| "The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed." | invalid_request | Six different problems return this one message. Most often the authorization code was already used, or it expired. | Work through the six causes, in order |
| "The refresh token is invalid." | invalid_request | The refresh token was rejected — reused, expired, revoked by an uninstall, or issued to another client | Work through the four causes |
| "The authorization grant type is not supported" | unsupported_grant_type | Typo in grant_type | Must be exactly authorization_code or refresh_token |
| "The requested scope is invalid, unknown, or malformed" | invalid_scope | Requested a scope your app does not hold | Omit scope to receive your app's current grants, or request a subset of them |
| "The authorization server encountered an unexpected condition …" | server_error | Zid-side fault | Retry with backoff; if it persists, open a ticket with the timestamp |
{
"status": "error",
"message": {
"type": "error",
"code": null,
"name": null,
"description": "The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed."
}
}redirect_uri mismatch. The redirect_uri sent to /oauth/token must be/oauth/authorize, and both must match a registeredhttps://x.com/cb and https://x.com/cb/ are different values.client_id exchanging the code is not thegrant_type,client_id, client_secret, redirect_uri, code — must be present in the request⚠️ Reloading the callback page produces this error. The authorization code is spent by the first successful exchange. If you then press
refresh, use the browser's Back button, or reopen the callback URL from your history, the
same code is submitted a second time and the request fails with the generic message above
— even though the first attempt succeeded and the tokens it returned are perfectly
valid.This is a normal refusal, not a broken integration. If you captured the tokens from the
first attempt, keep them. If you did not, start a new authorization from the beginning
to get a fresh code. Never re-submit a code that has already been sent.This is especially easy to trigger while testing with
Zid Bridge, because the callback URL stays in your
browser's address bar.
Why all six look identical. Zid's authorization server does determine a specific
reason internally — "Authorization code has been revoked", "Authorization code has
expired", and so on — but that detail is not currently included in the response. Expect to
diagnose these by elimination rather than by reading the error.
client_id — again, usually a staging/production mix-up.error key:{
"error": "invalid_request",
"error_description": "App installation is not allowed"
}EC4; seeing both for one merchant ishttps://api.zid.sa/v1/… with Authorization + X-Manager-Token.422.message.code | description (en) | Cause | What to do |
|---|---|---|---|
ERROR_SESSION_MISSING | "Please login first." | Neither X-Manager-Token nor Access-Token was sent | Add the X-Manager-Token header with the access_token value from the token response |
ERROR_SESSION_INVALID | "Invalid authentication" | The manager token could not be decrypted — it is corrupted, truncated, re-encoded, or from a different environment | Send the access_token value verbatim. Do not URL-encode, re-encrypt, trim, or pass it through a JSON round-trip that may mangle it. Check your column is wide enough — these tokens are long. |
ERROR_SESSION_INVALID | "Login session expired. Please login again." | The manager token is past its expiry, or its underlying record no longer exists | Refresh. If a successful refresh does not fix it, the merchant uninstalled your app — see the decision tree. |
ERROR_SESSION_INVALID | "The authentication token has been revoked" | The Authorization token was explicitly revoked | Do not retry. Treat as uninstalled until the merchant reinstalls. |
null | "Unauthenticated" | The Authorization bearer token was rejected outright — missing, malformed, expired, or revoked | Check the header is Authorization: Bearer <authorization> (the authorization field, not access_token). Then refresh. |
null | a message mentioning the required scope | Your token is missing a required scope. | Do not refresh. See the warning below. |
🚨 A missing scope returns 401, not 403. This is the single most expensive trap in Zid's auth surface. The natural reaction to a
401 is "token expired → refresh" — but the refreshed token carries the same scopes, so
the next call 401s again, and your client loops until it trips the rate limiter and starts
returning 429 instead.How to tell it apart from a real expiry: a scope 401 has message.code: nulland a
description naming a scope, and it does not go away after a successful refresh.Defensive rule: never retry a 401 more than once per token. Refresh at most once, and
if the same endpoint 401s again with the new token, stop and treat it as a scope problem.Fix: add the scope to your app in the Partner Dashboard. Existing merchants must then
re-approve it — see Scope and consent errors.
message.code | description (en) | Cause | What to do |
|---|---|---|---|
APP_PERMISSIONS_PENDING_APPROVAL | "Permissions pending merchant approval." | You added scopes; this merchant has not approved them yet | Only the merchant can clear it — by deactivating and reactivating your app. Their subscription is not affected. See below. |
APP_PERMISSION_REMOVED | "This app no longer has the permission this request needs." | A scope was removed from your app after the token was issued | Restore the scope in the Partner Dashboard, or stop calling the endpoint. Note: enforcement of this is currently gated — today you are more likely to see the 401 scope error above. |
ERROR_POPUP_OK | "Subscription is not active." | The merchant's own Zid store subscription has lapsed — nothing to do with your app | Not fixable by you. The merchant must renew their Zid plan. Pause syncs for that store and retry later. |
ERROR_POPUP_OK | "This action is not allowed on demo store" | You sent a non-GET request to a demo store | Demo stores are read-only. Only GET requests succeed. |
ERROR_POPUP_BACK | "Store mobile isn't verified" | The merchant has not verified their mobile (or email, for non-KSA stores) | The merchant must verify in their dashboard |
ERROR_POPUP_BACK | "Store readiness isn't completed" | The store has not finished onboarding | The merchant must complete store setup |
ERROR_POPUP_OK | "Permission Denied" | The merchant user your token belongs to lacks the role permission this endpoint requires | Usually means the token was issued to a sub-user, not the store owner. Have the owner authorize, or have them grant the permission. |
The 403s split cleanly into two groups. APP_PERMISSIONS_PENDING_APPROVALandAPP_PERMISSION_REMOVEDare your problem (scope configuration). Everything else is
the merchant's account state and no amount of retrying or re-authorizing will change
it. Surface those to the merchant rather than to your error logs.
| HTTP | message.code | Cause | What to do |
|---|---|---|---|
404 | ERROR_SESSION_INVALID | "Store not found." The user behind the token has no store | Mark the installation broken and open a ticket with the store id — this should not normally happen |
422 | ERROR_POPUP_OK | Validation failure — uses the validation envelope | Read message.validations[] for per-field detail |
429 | MSG_HIDDEN | Rate limit exceeded | Honour the Retry-After response header. See below. |
500 | ERROR_POPUP_OK | "Something went wrong" — server-side fault | Retry with exponential backoff. If persistent, open a ticket with the timestamp and X-Request-Id. |
403 APP_PERMISSIONS_PENDING_APPROVAL.Deactivating does not cancel your subscription. If the store has a paid subscription
to the app, deactivating leaves it untouched — the merchant can reactivate at any time
while the subscription is still valid, at no extra cost.
authorization, access_token and refresh_token for that store rather than rejecting🚨 If your app uses a wildcard token, adding a scope takes the whole integration down
for that merchant. A wildcard token cannot express "everything except these two", so
Zid withholds the app entirely — every endpoint returns403 APP_PERMISSIONS_PENDING_APPROVAL— until the merchant re-approves.Plan wildcard-token scope changes like a release: notify merchants first, expect a
re-consent window, and do not ship on a Friday.
403 APP_PERMISSION_REMOVED with| Signal | Meaning | Right response |
|---|---|---|
403 APP_PERMISSIONS_PENDING_APPROVAL on some endpoints | Partial consent | Degrade that feature; keep the rest running. Ask the merchant to deactivate and reactivate the app. |
403 APP_PERMISSIONS_PENDING_APPROVAL on every endpoint | Wildcard token, consent pending | Pause the integration for that store; ask the merchant to deactivate and reactivate the app |
403 APP_PERMISSION_REMOVED | Scope config drift | Fix your app config; alert your own team, not the merchant |
To finish updating the app's permissions, please open your Zid dashboard, deactivate
the app and then activate it again. Your subscription is not affected — you can
reactivate at any time while it is valid, at no extra cost.
429, message.code = MSG_HIDDEN, plus a Retry-After header givingRetry-After and wait that long. Do not use a fixed sleep.Accept-Language header — or send anything other than ar or en —message.code, never onmessage.description. Codes are stable identifiers; descriptions are localised editorialapp.market.application.uninstall webhook;An uninstalled app and an expired session return the identical error — 401withERROR_SESSION_INVALIDand "Login session expired." The distinguishing test is whether a
refresh fixes it. See the decision tree.
One webhook caveat: app.market.application.authorizedis dispatched during the
authorize step, before the token exchange completes. An install can therefore fail
after you have already receivedauthorized. Useapp.market.application.installas
your signal that an installation is real and usable.
A request failed. Where?
│
├─ The merchant clicked Install and landed on their dashboard
│ └─ Read ?error_code= from the URL they paste → §1 EC table
│ ├─ no error_code at all → merchant was not logged in
│ └─ a Zid-branded error card instead → read the card; it's accurate
│
├─ POST /oauth/token returned an error
│ ├─ body has a top-level "error" key
│ │ └─ "App installation is not allowed" → no active app subscription; merchant must
│ │ subscribe, then reinstall
│ └─ body is Zid's envelope (code: null) → read message.description
│ ├─ "Client authentication failed"
│ │ → client_id / client_secret / wrong environment
│ ├─ "...missing a required parameter..."
│ │ → the generic message. SIX causes share it; work them in order.
│ │ Most often the code was already used — did you reload the
│ │ callback page? — or it expired (10-minute limit).
│ └─ "The refresh token is invalid."
│ → reused / expired (1y) / uninstalled / wrong client.
│ App still installed? Restart the install flow.
│ Not installed? Mark the installation inactive.
│
└─ An API call to api.zid.sa failed
│
├─ 429 → honour Retry-After, back off per store.
│ Low real traffic? Look for a 401 loop underneath.
│
├─ 403 → is message.code APP_PERMISSIONS_PENDING_APPROVAL / APP_PERMISSION_REMOVED?
│ ├─ yes → YOUR scope config. Do NOT retry on a timer.
│ │ PENDING_APPROVAL → merchant deactivates + reactivates the app
│ │ (subscription unaffected). Be ready to store the NEW tokens.
│ │ PERMISSION_REMOVED → restore the scope in the Partner Dashboard.
│ └─ no → MERCHANT account state (subscription, verification, readiness,
│ permissions, demo store). Not fixable by you — surface it to them.
│
├─ 422 → validation envelope; read message.validations[]
│
├─ 404 ERROR_SESSION_INVALID → store missing; open a ticket with the store id
│
└─ 401 → Refresh the token ONCE.
│
├─ Refresh succeeded, call now works
│ → ordinary expiry. You are refreshing too late:
│ move to a scheduled refresh (~every 10 months).
│
├─ Refresh succeeded, call still 401s
│ → MISSING SCOPE. Stop retrying immediately.
│ Add the scope; existing merchants must re-approve.
│
└─ Refresh itself failed
→ the merchant uninstalled (or tokens were revoked).
Mark the installation inactive. Wait for a reinstall.authorization, newaccess_token, new refresh_token, new expiry. The old refresh token is revoked themessage.code, never on message.description. Descriptions are localisedX-Request-Id response header, if you captured it;message.code;error_code;