Reference
Errors
Failures are part of the API surface, so they have a documented shape. Every error carries a status you can branch on, a machine-readable code you can switch on, a message meant for a human, and a request id that makes a support conversation short.
Error shape
Branch on code, not on message. Codes are part of the contract and will not change under you; messages are written for people and get rewritten when they are unclear.
1HTTP 4092{3 "code": "email_already_exists",4 "message": "A user with the email alan@foo-corp.example already exists.",5 "request_id": "req_01HQ8ZK3M4N5P6R7S8T9V0W1X2",6 "documentation_url": "https://paycux.com/docs/errors#error-codes"7}
Status codes
Standard HTTP semantics. A 4xx means the request will not succeed unchanged; a 5xx means it might succeed on a retry.
| Status | Meaning | When you see it |
|---|---|---|
| 400 | Bad request | The body could not be parsed, or a parameter is the wrong type |
| 401 | Unauthorized | The API key is missing, malformed or has been revoked |
| 403 | Forbidden | The key is valid but not permitted to touch this resource or environment |
| 404 | Not found | No object with that id exists in the current environment |
| 409 | Conflict | The change collides with an existing object — a duplicate email or slug |
| 422 | Unprocessable entity | The request is well formed but fails a business rule |
| 429 | Too many requests | You are over the rate limit for this environment |
| 500 | Server error | Something failed on our side; the request id is worth keeping |
| 503 | Service unavailable | A dependency is degraded — retry with backoff |
Error codes
The codes you are most likely to handle explicitly. New codes are added over time and announced in the changelog, so treat an unknown code as a generic failure of its status class rather than a crash.
| Code | Status | Meaning |
|---|---|---|
| authentication_failed | 401 | The credentials presented could not be verified |
| api_key_revoked | 401 | The key was rotated or deleted from the dashboard |
| environment_mismatch | 403 | A staging key was used against a production object, or the reverse |
| insufficient_permissions | 403 | The key lacks the scope this endpoint requires |
| entity_not_found | 404 | No object with that id exists in this environment |
| email_already_exists | 409 | Another user in this environment already claims that address |
| organization_domain_conflict | 409 | The domain is already routed to a different organization |
| role_slug_taken | 409 | A role with that slug already exists |
| invalid_parameters | 422 | One or more fields failed validation — see the errors array |
| connection_not_active | 422 | The connection has not completed setup and cannot be used to sign in |
| directory_not_linked | 422 | The organization has no directory connected |
| mfa_enrollment_required | 422 | The policy requires a second factor before this action |
| rate_limit_exceeded | 429 | Too many requests — read the Retry-After header |
| internal_error | 500 | An unexpected failure; report the request id if it repeats |
Validation errors
A 422 with invalid_parameters lists every field that failed, not just the first one, so a form can be corrected in a single pass.
1HTTP 4222{3 "code": "invalid_parameters",4 "message": "The request could not be processed.",5 "request_id": "req_01HQ8ZK3M4N5P6R7S8T9V0W1X2",6 "errors": [7 { "field": "email", "code": "format", "message": "Must be a valid email address." },8 { "field": "domain_data[0].domain", "code": "format", "message": "Must be a bare domain." },9 { "field": "permissions", "code": "too_long", "message": "At most 100 permissions per role." }10 ]11}
Rate limits
Limits are applied per environment, which means your staging load testing cannot throttle production. The current budget is on every response, so you can back off before you are refused rather than afterwards.
| Scope | Limit | Window |
|---|---|---|
| Read endpoints (GET) | 600 requests | per minute, per environment |
| Write endpoints (POST, PUT, DELETE) | 180 requests | per minute, per environment |
| Authentication endpoints | 120 requests | per minute, per IP address |
| Audit log exports | 10 requests | per hour, per environment |
| Bulk directory reads | 60 requests | per minute, per directory |
Higher budgets are available on annual plans — talk to us before you build a queue you do not need.
1HTTP 4292Paycux-RateLimit-Limit: 6003Paycux-RateLimit-Remaining: 04Paycux-RateLimit-Reset: 17816832605Retry-After: 1767{8 "code": "rate_limit_exceeded",9 "message": "Too many requests. Retry after 17 seconds.",10 "request_id": "req_01HQ8ZK3M4N5P6R7S8T9V0W1X2"11}
Retrying safely
The official SDKs already retry 429 and 5xx responses with exponential backoff and jitter. If you are calling the API directly, four rules keep a retry from becoming an outage of your own making.
- Never retry a 4xx other than 429 — the same request will fail the same way
- Honour Retry-After when it is present instead of guessing an interval
- Add jitter, so a shared failure does not turn into a synchronised stampede
- Cap total attempts and surface the failure; an infinite retry hides a real problem
1async function withRetry(call, attempts = 5) {2 for (let attempt = 1; attempt <= attempts; attempt++) {3 try {4 return await call();5 } catch (error) {6 const retryable = error.status === 429 || error.status >= 500;7 if (!retryable || attempt === attempts) throw error;89 const hinted = Number(error.headers['retry-after']) * 1000;10 const backoff = Math.min(2 ** attempt * 250, 20000);11 const jitter = Math.random() * 250;1213 await sleep(hinted || backoff + jitter);14 }15 }16}
Request ids
Every response, successful or not, includes Paycux-Request-Id. Log it next to your own correlation id. When something goes wrong at three in the morning, that pair is the difference between a diagnosis and a guess.
Still stuck?
Send the request id, the endpoint and the timestamp. We can trace the call end to end from those three things alone.