API error codes
The unified MeerPartners API error format, HTTP codes and a table of domain codes with explanations. How a client should react to 401, 403, 409, 422, 429 and 5xx during integration.
The MeerPartners API returns errors in a unified format with a stable string code. Route your logic by the code field, not by the message text: the text may change and be localized, the code will not.
Error format
{
"error": {
"code": "POSTBACK_BAD_SIGNATURE",
"message": "Signature is invalid",
"details": {}
}
}| Field | Value |
|---|---|
code | A stable constant in SCREAMING_SNAKE_CASE — the client makes its decision based on it |
message | A human-readable explanation (may change) |
details | Extra context: for validation — a field map; sometimes limit, tenants, etc. |
The main rule
Never parse message for logic. Branch on code and the HTTP status. message is only for logs and for showing to a human.
HTTP codes
| HTTP | Meaning | What the client should do |
|---|---|---|
400 | Bad request (syntax/semantics, a stale timestamp) | Fix the request, do not retry blindly |
401 | Not authenticated: missing/broken token or signature | Check the key/signature/token |
403 | No rights: scope, role, another tenant | Check the key scope / role / tenant |
404 | Resource not found or not visible | Check the identifier |
409 | State/idempotency conflict (including replay) | Rebuild the request (new timestamp/idempotency key) |
410 | The resource is "stale" or disabled (the link/offer is inactive) | Do not retry |
422 | Field validation error | Fix the body per details |
429 | Rate limit exceeded | Retry with backoff, look at Retry-After |
500 | Internal error | Retry with backoff; if it persists — escalate |
503 | A dependency is unavailable (fail-closed) | Retry later with backoff |
Domain codes
Authentication and keys
| Code | HTTP | When |
|---|---|---|
TENANT_API_KEY_INVALID | 401 | Key is unknown, inactive, revoked or expired; X-Api-Key-Id is missing |
TENANT_API_KEY_SCOPE_DENIED | 403 | The key lacks the required scope (postback / sso) |
TENANT_SUSPENDED | 403 | The business (tenant) is inactive |
TOKEN_EXPIRED | 401 | The JWT has expired |
TOKEN_INVALID | 401 | The JWT did not parse / is invalid |
TOKEN_WRONG_TYPE | 401 | A refresh was expected but another type arrived (or vice versa) |
TOKEN_REVOKED | 401 | The refresh token has already been used/revoked |
ACCESS_REVOKED | 401 | Access revoked on the MeerID side (ban/deletion) |
EMBED_SCOPE_DENIED | 403 | The action requires a full token, but an embed-scoped one arrived |
Postback
| Code | HTTP | When |
|---|---|---|
POSTBACK_BAD_SIGNATURE | 401 | The HMAC signature did not match |
POSTBACK_STALE_TIMESTAMP | 400 | X-Timestamp outside the ±300 s window |
POSTBACK_REPLAY | 409 | The same signature replayed within the memory window |
POSTBACK_TENANT_MISMATCH | 403 | X-Tenant-Id did not match the key's tenant |
POSTBACK_MISSING_FIELD | 422 | A required field for the event_type is missing (or an invalid body) |
POSTBACK_UNKNOWN_REF | 200 | No attributable click — accepted:false (not an error) |
POSTBACK_REFUND_NO_CONVERSION | 200 | Refund for a non-existent conversion — accepted:false |
POSTBACK_RATE_LIMITED | 429 | Too many postbacks per key |
Codes on 200 are not failures
POSTBACK_UNKNOWN_REF and POSTBACK_REFUND_NO_CONVERSION arrive with HTTP 200 and accepted:false. These are normal outcomes (organic / a non-applicable refund), not connectivity errors — do not retry them.
Token-exchange (SSO)
| Code | HTTP | When |
|---|---|---|
EXCHANGE_DENIED | 401 | An invalid signature, X-Timestamp missing/expired, the tenant is inactive |
EXCHANGE_REPLAY | 401 | A replay of the same signature |
USER_BANNED | 403 | The user is banned |
Webhooks
| Code | HTTP | When |
|---|---|---|
DEPOSIT_WEBHOOK_UNCONFIGURED | 503 | The deposit webhook secret is not set (fail-closed) |
DEPOSIT_WEBHOOK_SIGNATURE_INVALID | 401 | Missing headers / invalid signature / timestamp outside the window / replay |
DEPOSIT_WEBHOOK_REPLAY_STORE_UNAVAILABLE | 503 | The replay-protection store is unavailable |
INVALID_PAYLOAD | 400 | Invalid JSON in the body |
LOGOUT_TOKEN_INVALID | 401 | An invalid logout_token (MeerID back-channel logout) |
WEBHOOK_TIMESTAMP_STALE | 401 | A MeerID identity webhook outside the freshness window |
Tracking
| Code | HTTP | When |
|---|---|---|
TRACKING_LINK_NOT_FOUND | 404 | An unknown link {code} |
LINK_INACTIVE | 410 | The link is disabled (no fallback) |
OFFER_INACTIVE | 410 | The offer is inactive (no fallback) |
TRACKING_RATE_LIMITED | 429 | Explicit flooding of a tracking endpoint |
General
| Code | HTTP | When |
|---|---|---|
VALIDATION_ERROR | 422 | A body validation error (field map in details) |
FORBIDDEN | 403 | Insufficient rights/role |
TENANT_HEADER_REQUIRED | 400 | Several businesses and no X-Tenant-Id specified (details.tenants) |
TOO_MANY_REQUESTS | 429 | Limit exceeded (the Retry-After header) |
SERVICE_UNAVAILABLE | 503 | A dependency is unavailable (fail-closed) |
INTERNAL_SERVER_ERROR | 500 | An unhandled error |
Cabinet user-facing errors
The codes a human sees in the cabinet interface (e.g. BELOW_MIN_PAYOUT, KYC_REQUIRED, INSUFFICIENT_BALANCE, OFFER_PAUSED, SELF_REFERRAL_FORBIDDEN) are described in the user reference. The full list with human-readable wording is on the Error reference page.