Errors

Response Formats

Errors raised by the partner endpoints use a flat format:

{
  "error": "ERROR_CODE",
  "message": "Human-readable description of the error."
}

The error field is a stable machine-readable code. The message field provides a human-readable explanation and may change between versions.

Two groups of errors use a nested format instead: everything from POST /api/v1/auth/exchange, and request-body validation and feature-toggle errors on every endpoint.

{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Validation failed",
    "details": { "amount": ["Must be a valid positive decimal with at most 2 decimal places"] }
  }
}

details appears only on VALIDATION_ERROR. Check for both shapes in your client: const code = typeof body.error === 'string' ? body.error : body.error?.code;

Unexpected failures return 500 with the code INTERNAL_ERROR in the nested format.

Error Summary Table

HTTP StatusError CodeCategoryDescription
401MISSING_HEADERSAuthenticationRequired authentication headers are missing
401INVALID_TIMESTAMPAuthenticationX-Timestamp header is not a valid Unix millisecond value
401TIMESTAMP_EXPIREDAuthenticationRequest timestamp is older than 5 minutes
401TIMESTAMP_FUTUREAuthenticationRequest timestamp is more than 30 seconds in the future
401INVALID_REQUEST_IDAuthenticationX-Request-Id header is not in the allowed format
401INVALID_API_KEYAuthenticationAPI key is invalid, inactive, expired, or partner is not active/approved
401INVALID_SIGNATUREAuthenticationHMAC-SHA256 signature does not match
401UNSIGNED_BODYAuthenticationThe request has a body whose raw bytes could not be captured for verification
401INVALID_CODESSOAuth code does not exist
401CODE_EXPIREDSSOAuth code has expired (60-second lifetime)
401CODE_ALREADY_USEDSSOAuth code has already been exchanged
403INSUFFICIENT_SCOPESAuthorizationAPI key does not have the required scopes
403IP_NOT_ALLOWEDAuthorizationRequest IP is not in the API key’s allowlist
403SELF_REGISTRATION_DISABLEDRegistrationSelf-registration is turned off by the admin
403ACCOUNT_BLOCKEDSSO, BalanceThe user’s account is blocked
403ACCOUNT_FROZENSSO, BalanceThe user’s account is frozen
403FORBIDDENWebhooksThe subscription belongs to another partner
403AUTHORIZATION_ERRORFeature toggleThe developerApi feature toggle is disabled
403FEATURE_TIER_RESTRICTEDFeature toggleThe platform’s license tier does not include the Developer API
400VALIDATION_ERRORAllA body field failed validation
400MISSING_EMAILRegistrationGET /developer/status was called without email
400MISSING_USERNAMEUser ProvisioningUsername is required by the username policy but was not provided
400MODE_NOT_ALLOWEDSSOThe requested auth mode (cookie or token) is disabled in settings
400INVALID_AMOUNTBalanceAmount is zero
400INSUFFICIENT_BALANCEBalanceUser’s balance is less than the requested debit amount
400INVALID_EVENT_TYPEWebhooksEvent type is not in the list of supported event types
400INVALID_URLWebhooksWebhook URL is not allowed
404USER_NOT_FOUNDUser ProvisioningNo external user mapping exists for this partner + external user ID
404SUBSCRIPTION_NOT_FOUNDWebhooksThe subscription does not exist
404PAGE_NOT_FOUNDDocumentationUnknown documentation page
404DELIVERY_NOT_FOUNDWebhooksWebhook delivery record not found (admin retry)
409DUPLICATE_REQUESTAuthenticationX-Request-Id has already been processed within the last 5 minutes
409EMAIL_CONFLICTUser ProvisioningEmail address is already in use by another user not linked to this partner
409NAME_CONFLICTRegistrationPartner name is already taken
409USERNAME_CONFLICTUser ProvisioningRequested username is already taken
409IDEMPOTENCY_CONFLICTBalanceThe reference was already used for a different user, amount, or operation
415UNSUPPORTED_CONTENT_TYPEAuthenticationThe request has a body that is not application/json
429RATE_LIMIT_EXCEEDEDRate LimitingAPI key rate limit or endpoint rate limit exceeded
500INTERNAL_ERRORAllUnexpected server error
500DOCS_RENDER_FAILEDDocumentationA documentation page or the OpenAPI spec could not be served
503REPLAY_STORAGE_UNAVAILABLEAuthenticationReplay protection storage is unavailable

Detailed Error Descriptions

MISSING_HEADERS

HTTP Status: 401 Unauthorized

Cause: One or more required authentication headers are missing from the request. The Developer API requires X-API-Key, X-Signature, X-Timestamp, and X-Request-Id headers on all authenticated endpoints.

Response example:

{
  "error": "MISSING_HEADERS",
  "message": "X-API-Key, X-Signature, X-Timestamp, and X-Request-Id headers are required"
}

Fix: Include all required authentication headers in every request. See the Authentication guide for details.

INVALID_TIMESTAMP

HTTP Status: 401 Unauthorized

Cause: The X-Timestamp header is not a valid Unix timestamp in milliseconds. It must be a numeric value representing the current time.

Response example:

{
  "error": "INVALID_TIMESTAMP",
  "message": "X-Timestamp must be a valid Unix millisecond timestamp"
}

Fix: Use Date.now() (JavaScript), int(time.time() * 1000) (Python), or round(microtime(true) * 1000) (PHP) to generate the timestamp.

TIMESTAMP_EXPIRED

HTTP Status: 401 Unauthorized

Cause: The request timestamp is older than 5 minutes (300,000 milliseconds). This prevents replay attacks by rejecting stale requests.

Response example:

{
  "error": "TIMESTAMP_EXPIRED",
  "message": "Request timestamp is too old (>5 minutes)"
}

Fix: Ensure your server clock is synchronized using NTP. Generate the timestamp immediately before sending each request. If your requests take a long time to reach the server, check for network latency or proxy buffering. A timestamp in seconds instead of milliseconds also lands here.

TIMESTAMP_FUTURE

HTTP Status: 401 Unauthorized

Cause: The request timestamp is more than 30 seconds in the future. This prevents clock-skew exploitation.

Response example:

{
  "error": "TIMESTAMP_FUTURE",
  "message": "Request timestamp is too far in the future (>30 seconds)"
}

Fix: Synchronize your server clock using NTP.

INVALID_REQUEST_ID

HTTP Status: 401 Unauthorized

Cause: The X-Request-Id header is too long or contains disallowed characters. (A missing header returns MISSING_HEADERS.) For replay protection and deduplication, X-Request-Id must be:

  • 1–128 characters
  • match ^[A-Za-z0-9._:-]+$ (UUID v4 recommended)

Response example:

{
  "error": "INVALID_REQUEST_ID",
  "message": "X-Request-Id must be 1-128 chars and match /^[A-Za-z0-9._:-]+$/"
}

Fix: Generate a fresh request ID for every authenticated request. Prefer UUID v4 (e.g., crypto.randomUUID() in Node.js).

INVALID_API_KEY

HTTP Status: 401 Unauthorized

Cause: The API key provided in the X-API-Key header is invalid. This can happen for several reasons:

  • The key does not exist in the system
  • The key has been revoked
  • The key has expired (past its expiresAt date)
  • The key is deactivated
  • The partner account is suspended (isActive: false)
  • The partner account is not yet approved (isApproved: false)

Response example:

{
  "error": "INVALID_API_KEY",
  "message": "API key is invalid, inactive, expired, or partner is not active/approved"
}

Fix: Verify you are using the correct API key. Check with the platform admin to confirm your partner account is approved and active. If the key expired, request a new one. Rotation changes the secret but not the key, so a rotated key stays valid.

INVALID_SIGNATURE

HTTP Status: 401 Unauthorized

Cause: The HMAC-SHA256 signature in the X-Signature header does not match the expected value, or it is not 64 hex characters. The server computes the signature over the canonical request string (v1, HTTP method, canonical path/query, timestamp, request ID, and raw body) and compares it to your provided signature.

Response example:

{
  "error": "INVALID_SIGNATURE",
  "message": "HMAC signature verification failed"
}

Common causes and fixes:

  1. Wrong secret: You’re signing with a different secret than the one associated with the API key. Use the secret that was returned when the key was generated.
  2. Wrong signing string: The signing string must be exactly newline-separated v1, uppercase HTTP method, canonical path/query, X-Timestamp, X-Request-Id, and the exact raw request body.
  3. Body mismatch: The JSON body you signed is not byte-for-byte identical to what was sent. Ensure you serialize JSON once and use that same string for both signing and sending.
  4. Encoding issue: The HMAC output must be hex-encoded (not base64).
  5. Key rotation: If the secret was recently rotated, the old secret may have expired. Use the new secret.
  6. Trailing newline: For a request with no body, the signing string ends with a newline. Shell command substitution ($(...)) strips it.

See the Authentication guide for step-by-step signing instructions and examples.

INVALID_CODE

HTTP Status: 401 Unauthorized

Cause: The auth code provided to the /api/v1/auth/exchange endpoint does not exist. The code may have been mistyped, truncated, or was never generated.

Response example:

{
  "error": "INVALID_CODE",
  "message": "Invalid auth code"
}

Fix: Ensure you are passing the full 64-character hex code exactly as returned by the POST /api/v1/developer/auth/code endpoint. Auth codes are lowercase hex; anything else fails validation with VALIDATION_ERROR.

CODE_EXPIRED

HTTP Status: 401 Unauthorized

Cause: The auth code has expired. Auth codes are valid for 60 seconds from generation.

Response example:

{
  "error": "CODE_EXPIRED",
  "message": "Auth code has expired"
}

Fix: Generate a new auth code and exchange it within 60 seconds. Design your flow to exchange the code immediately after receiving it — do not store codes for later use.

CODE_ALREADY_USED

HTTP Status: 401 Unauthorized

Cause: The auth code has already been exchanged for a session. Auth codes are single-use.

Response example:

{
  "error": "CODE_ALREADY_USED",
  "message": "Auth code has already been used"
}

Fix: Generate a new auth code for each login attempt. Never reuse codes.

INSUFFICIENT_SCOPES

HTTP Status: 403 Forbidden

Cause: The API key does not have the required scopes for this endpoint. The response includes the missing scopes.

Response example:

{
  "error": "INSUFFICIENT_SCOPES",
  "message": "API key is missing required scopes: balance:write",
  "missingScopes": ["balance:write"]
}

Fix: Request that the platform admin adds the missing scopes to your API key. Available scopes: user:create, user:read, auth:code, balance:read, balance:write, webhooks:manage.

IP_NOT_ALLOWED

HTTP Status: 403 Forbidden

Cause: The request originated from an IP address that is not in the API key’s IP allowlist.

Response example:

{
  "error": "IP_NOT_ALLOWED",
  "message": "Request IP is not in the API key allowlist"
}

Fix: Contact the platform admin to add your server’s IP address to the API key’s allowlist. If you use dynamic IPs, consider removing the IP restriction or using a CIDR range. Note: if the API key has no IPs in the allowlist, all IPs are allowed.

SELF_REGISTRATION_DISABLED

HTTP Status: 403 Forbidden

Cause: The POST /api/v1/developer/register endpoint was called but self-registration is disabled in the platform’s developer settings.

Response example:

{
  "error": "SELF_REGISTRATION_DISABLED",
  "message": "Self-registration is currently disabled. Contact the platform administrator."
}

Fix: Contact the platform administrator to either enable self-registration or create a partner account for you manually.

MISSING_USERNAME

HTTP Status: 400 Bad Request

Cause: The username policy on your partner account needs a value that was not sent. Under require, the username field is needed. Under generate_from_email, username or email is needed.

Response example:

{
  "error": "MISSING_USERNAME",
  "message": "Username is required by partner policy"
}

Fix: Include a username field in the request body. The username must be 3-30 characters, alphanumeric plus underscores only. Alternatively, ask the platform admin to change your partner’s username policy.

MODE_NOT_ALLOWED

HTTP Status: 400 Bad Request

Cause: The requested auth code mode (cookie or token) is disabled in the platform’s developer settings.

Response example:

{
  "error": "MODE_NOT_ALLOWED",
  "message": "Token mode is not allowed"
}

Fix: Use the other mode, or contact the platform admin to enable the mode you need.

INVALID_AMOUNT

HTTP Status: 400 Bad Request

Cause: The amount in a credit or debit request is zero. Other malformed amounts ("-5", "10.123", "abc", or a number instead of a string) fail earlier with VALIDATION_ERROR.

Response example:

{
  "error": "INVALID_AMOUNT",
  "message": "Amount must be greater than zero"
}

Fix: Send a string containing a positive decimal number with at most 2 decimal places: "10", "10.5" and "10.50" are all valid.

INSUFFICIENT_BALANCE

HTTP Status: 400 Bad Request

Cause: A debit was requested but the user’s balance is less than the requested amount.

Response example:

{
  "error": "INSUFFICIENT_BALANCE",
  "message": "Insufficient balance. Current: 50, requested: 100"
}

Fix: Check the user’s balance before attempting a debit using GET /api/v1/developer/users/:externalUserId/balance. Alternatively, handle this error gracefully in your application by informing the user they have insufficient funds.

INVALID_EVENT_TYPE

HTTP Status: 400 Bad Request

Cause: The eventType in a webhook subscription request is not one of the supported event types.

Response example:

{
  "error": "INVALID_EVENT_TYPE",
  "message": "Invalid event type 'custom.event'. Supported types: user.created, user.updated, deposit.confirmed, deposit.failed, withdrawal.completed, withdrawal.failed, balance.credited, balance.debited"
}

Fix: Use one of the supported event types. See the Webhooks guide for the complete list.

USER_NOT_FOUND

HTTP Status: 404 Not Found

Cause: No external user mapping exists for the given externalUserId and your partner account. The user has not been provisioned through the Developer API for your partner.

Response example:

{
  "error": "USER_NOT_FOUND",
  "message": "No user found with this external user ID"
}

Fix: Provision the user first using POST /api/v1/developer/users. Verify that the externalUserId is correct and URL-encoded in the path.

ACCOUNT_BLOCKED / ACCOUNT_FROZEN

HTTP Status: 403 Forbidden

Cause: The user’s account is blocked or frozen. Credits, debits and code exchange are refused. Balance errors use the flat format and exchange errors the nested one. On an exchange, the code is used up.

Fix: Contact the platform admin.

IDEMPOTENCY_CONFLICT

HTTP Status: 409 Conflict

Cause: The reference on a credit or debit was already used for a different user, a different amount, or the other operation. Nothing changes.

{
  "error": "IDEMPOTENCY_CONFLICT",
  "message": "Idempotency reference was already used for a different amount"
}

Fix: Use a new reference for each distinct operation, and the same one only when retrying that exact operation.

INVALID_URL

HTTP Status: 400 Bad Request

Cause: The webhook URL broke a rule, for example it uses HTTP in production, contains credentials, or targets a local hostname or private IP. See URL rules.

Fix: Use a public HTTPS URL.

SUBSCRIPTION_NOT_FOUND / FORBIDDEN

HTTP Status: 404 Not Found / 403 Forbidden

Cause: On DELETE /api/v1/developer/webhooks/:subscriptionId, the subscription does not exist, or it belongs to another partner.

Fix: List your subscriptions with GET /api/v1/developer/webhooks and use an ID from there.

DELIVERY_NOT_FOUND

HTTP Status: 404 Not Found

Cause: The webhook delivery ID provided for a manual retry does not exist (admin endpoint).

Response example:

{
  "error": "DELIVERY_NOT_FOUND",
  "message": "Delivery not found"
}

Fix: Verify the delivery ID is correct. Check the webhook deliveries list in the admin panel.

DUPLICATE_REQUEST

HTTP Status: 409 Conflict

Cause: The required X-Request-Id header has already been seen within the last 5 minutes. This prevents accidental duplicate processing.

Response example:

{
  "error": "DUPLICATE_REQUEST",
  "message": "This request ID has already been processed"
}

Fix: Generate a unique X-Request-Id (UUID) for each request. The ID is used up once the signature verifies, even if the request then fails, so every retry needs a new request ID. If you need idempotency for balance operations, use the reference field instead.

EMAIL_CONFLICT

HTTP Status: 409 Conflict

Cause: The email address provided during user provisioning is already in use by another poker account that is not linked to your partner.

Response example:

{
  "error": "EMAIL_CONFLICT",
  "message": "A user with this email already exists but is not linked to this partner"
}

Fix: Either use a different email address, or omit the email field (the system will generate a placeholder email).

NAME_CONFLICT

HTTP Status: 409 Conflict

Cause: The partner name provided during registration is already taken by another partner (case-insensitive match).

Response example:

{
  "error": "NAME_CONFLICT",
  "message": "A partner with this name already exists"
}

Fix: Choose a different partner name.

USERNAME_CONFLICT

HTTP Status: 409 Conflict

Cause: The requested username is already taken by another user account.

Response example:

{
  "error": "USERNAME_CONFLICT",
  "message": "Username 'john_doe' is already taken"
}

Fix: Choose a different username, or switch to the generate_from_email or generate_random username policy to have the system generate unique usernames automatically.

VALIDATION_ERROR

HTTP Status: 400 Bad Request

Cause: A request body field failed validation. This runs after authentication on signed endpoints. It uses the nested format, with the failures per field in error.details.

Fix: Correct the listed fields.

UNSUPPORTED_CONTENT_TYPE

HTTP Status: 415 Unsupported Media Type

Cause: A request with a body does not use a JSON content type.

{
  "error": "UNSUPPORTED_CONTENT_TYPE",
  "message": "Developer API requests with bodies must use application/json"
}

Fix: Send Content-Type: application/json.

UNSIGNED_BODY

HTTP Status: 401 Unauthorized

Cause: The request has a body, but its raw bytes could not be captured to verify the signature.

Fix: Send the body as application/json.

REPLAY_STORAGE_UNAVAILABLE

HTTP Status: 503 Service Unavailable

Cause: The platform uses Redis for request-ID replay protection and it is unavailable. The request is refused rather than risk a replay.

Fix: Retry with a new X-Request-Id after a short delay.

AUTHORIZATION_ERROR / FEATURE_TIER_RESTRICTED

HTTP Status: 403 Forbidden

Cause: The Developer API is switched off (AUTHORIZATION_ERROR) or isn’t included in the platform’s license tier (FEATURE_TIER_RESTRICTED). These use the nested format and apply to everything except the documentation pages.

Fix: Contact the platform admin.

RATE_LIMIT_EXCEEDED

HTTP Status: 429 Too Many Requests

Cause: You’ve exceeded the rate limit for this API key or endpoint. Responses to the per-key limit include a Retry-After header indicating how many seconds to wait. Every request made with a valid key counts, including rejected ones.

Response example:

{
  "error": "RATE_LIMIT_EXCEEDED",
  "message": "API key rate limit exceeded"
}

Response headers:

Retry-After: 30

Fix: Implement exponential backoff in your client. Respect the Retry-After header. If you consistently hit rate limits, contact the platform admin to increase your API key’s rateLimitPerMinute setting.

Rate limits by endpoint type:

  • Authenticated endpoints: Configurable per API key (default: 100 requests/minute)
  • POST /developer/register: 5 requests per hour per IP
  • GET /developer/status: 3 requests per minute per IP
  • POST /auth/exchange: 10 requests per minute per IP (nested error format)

The per-IP endpoints use their own messages, such as Too many registration attempts, try again in 1 hour.

Try the live demo Pricing

Have questions about Poker Engine? We reply fast.