| # | 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_client, for example, is 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. |
| "authorization grant β¦ is invalid, expired, revoked, does not match the redirection URI β¦, or was issued to another client" | invalid_grant | The big one. See the breakdown below. | See below |
| "The refresh token is invalid" | β | Refresh token expired (1 year), already used, or revoked by an uninstall | If the merchant still has your app installed, restart the install flow. If not, mark the installation inactive. |
| "The request is missing a required parameter β¦" | invalid_request | A required form field was absent or empty | Verify all five fields are present in the body, not the query string |
| "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 |
invalid_grant β the five causes, in order of likelihoodinvalid_grant is one message for five different problems. Work through them in this order: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 than theerror 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
β ββ "...grant...is invalid, expired..." β work the 5 invalid_grant causes in order
β ββ "The refresh token is invalid" β refresh token dead (1y expiry, reused, or
β uninstalled) β restart install flow
β
ββ 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;