Error Handling
ZopNight uses standard HTTP status codes and returns structured JSON error responses. Reference for status codes, retry strategy, and common error scenarios.
ZopNight uses standard HTTP status codes and returns structured error responses.
Error Response Format
Every error body has an error field. Its shape depends on where the error was raised.
Gateway errors (permissions, rate limits, routing, PAT checks) carry a string error. Some also carry a machine-readable code:
{ "error": "forbidden", "code": "rbac_denied"}Service errors (validation, not found, conflicts, server faults) carry an error object with a message. Some also carry a code:
{ "error": { "message": "'1' missing parameter(s): name" }}OAuth endpoints (/oauth/register, /oauth/token, /oauth/revoke) answer errors in the RFC 6749 form, with a string error code and an error_description.
HTTP Status Codes
| Code | Meaning | Common Causes |
|---|---|---|
200 | OK | Request succeeded |
201 | Created | Resource created successfully |
400 | Bad Request | Invalid request body, missing required fields, validation errors |
401 | Unauthorized | Missing, invalid, or expired JWT token |
403 | Forbidden | Insufficient permissions or accessing another organization's resources |
404 | Not Found | Resource, schedule, or endpoint does not exist |
409 | Conflict | Duplicate resource, schedule name already exists |
429 | Too Many Requests | Rate limit exceeded — see Rate Limiting; or BUSY: another change to the same data held it — see Busy below |
500 | Internal Server Error | Unexpected server error — retry with backoff |
502 | Bad Gateway | Gateway could not reach the backend service |
503 | Service Unavailable | Service is temporarily unavailable |
Retry Strategy
| Status | Action |
|---|---|
| 4xx (except 429) | Do not retry. Fix the request and try again. |
| 429 | If the response has a Retry-After header, wait that many seconds. The per-org REST limit and BUSY send none: wait for the next clock minute (REST limit) or retry after a short backoff of 1s, 2s, 4s (BUSY). |
| 500 | Retry with exponential backoff (1s, 2s, 4s, 8s). |
| 502/503 | Retry with exponential backoff. The service may be restarting. |
Common Error Scenarios
Expired Token
An expired or invalid JWT access token is refused at the gateway, before it reaches any service:
HTTP 401{ "error": { "message": "invalid auth header in key 'Authorization'" }}Use the /auth/refresh endpoint to get a new access token.
Invalid Request Body
Creating a schedule without a name:
HTTP 400{ "error": { "message": "'1' missing parameter(s): name" }}Resource Not Found
HTTP 404{ "error": { "message": "No entity found with dashboard: 3f2b8c1e-6d4a-4b7e-9a51-2c8e7f0d1a93" }}A 404 with route not found usually means the path is wrong, for example because it includes
/orgs/{orgId}. Call the path without the org; the gateway adds it.
Rate Limited
HTTP 429{ "error": "rate limit exceeded"}The MCP server adds a code so an agent can tell a retryable limit from a permission error. It also sends a Retry-After header:
HTTP 429Retry-After: 42
{ "error": "rate limit exceeded", "code": "rate_limited"}The /oauth/* endpoints use the RFC 6749 form, also with Retry-After:
HTTP 429Retry-After: 42
{ "error": "slow_down", "error_description": "Rate limit exceeded on /oauth. Retry after the cool-down window."}Busy
HTTP 429{ "error": { "code": "BUSY", "message": "the service is busy with another change to the same data; retry the request" }}Another request was changing the same data and the database gave up waiting for it.
Nothing was changed. The response carries no wait period (no Retry-After
header): retry the same request after a short backoff. Match on
error.code = BUSY, not on the message text. Routes that can answer it
include POST /users, PATCH /users/{email}, DELETE /users/{email},
POST /teams/{teamID}/members, the preference writes under /me/preferences, and
DELETE /cloud-accounts/{caID}/iam-import/disconnect.