Errors & pagination
Reference
Greenlight’s REST API, MCP tools, and webhooks all use the same conventions for errors and pagination. Two pages worth of reading; both surfaces work the same way.
Error envelope
Every error response — REST, MCP, webhook delivery failure — uses this shape:
{ "error": { "code": "FORBIDDEN", "message": "User does not have role 'admin'.", "request_id": "req_01HQX5MWGRTYBJ7C7C7C7C7C", "details": { /* optional, error-specific */ } }}codeis stable across versions. Clients should switch oncode, not onmessage.messageis human-readable. Greenlight reserves the right to improve wording.request_idis unique per request. Including it in a support ticket gets to the root cause fastest.detailsis present on errors that carry structured context (which field failed validation, which check failed in the policy check, etc.).
HTTP status mapping
| HTTP | When |
|---|---|
400 BAD_REQUEST | Input failed validation. details.field names the offending field. |
401 UNAUTHENTICATED | No valid token. |
403 FORBIDDEN | Authenticated, but the caller’s role doesn’t permit this action. |
404 NOT_FOUND | The target resource doesn’t exist or the caller can’t see it. |
409 CONFLICT | The action conflicts with current state (e.g., a stale Knowledge proposal). |
412 PRECONDITION_FAILED | An If-Match or base_version check rejected the request. |
429 RATE_LIMITED | The caller’s token has hit its budget. Retry-After indicates when to retry. |
500 INTERNAL | A platform error. The audit log captures these; report with the request_id. |
503 UNAVAILABLE | Transient platform unavailability. Safe to retry with backoff. |
Error code catalog
The most common stable codes. Not exhaustive; the full set is in the OpenAPI spec.
| Code | Meaning |
|---|---|
UNAUTHENTICATED | Missing or invalid token. |
FORBIDDEN | Authenticated but not authorized. |
NOT_FOUND | Resource missing or invisible. |
INVALID_INPUT | Validation failed. See details.field. |
CONFLICT | State conflict; retry with current state. |
STALE_PROPOSAL | Knowledge proposal’s base_version is out of date. |
POLICY_CHECK_FAILED | The policy check rejected a change. See details.checks. |
INTEGRATION_NOT_GRANTED | The app doesn’t have access to the requested integration. |
INTEGRATION_REVOKED | The integration was revoked since the request started. |
BUDGET_EXCEEDED | The org or app exceeded its configured budget. |
RATE_LIMITED | Too many requests. |
INTERNAL | Platform error. |
Idempotency
Mutating endpoints accept an Idempotency-Key header (REST) or idempotency_key field (MCP). Requests with the same key and the same payload return the original response. Idempotency keys are scoped per token and stored for 24 hours.
Use a UUID v4. Generate a new one for each logical operation; reuse the same one for retries of the same operation.
Pagination
List endpoints accept limit (default 50, max 200) and cursor. The response includes next_cursor if more results exist.
{ "items": [ /* … */ ], "next_cursor": "eyJsYXN0X2lkIjoiYXBwX2s5eDJtM3AifQ=="}To paginate forward, pass next_cursor back as cursor. To start from the beginning, omit cursor. There is no offset parameter; cursor pagination is the only mode supported.
Cursors are opaque. They encode the position needed to resume the query, but the encoding is not part of the API contract. Do not parse a cursor; do not generate one.
Stable sort
Results within a paginated list are sorted by id (which is monotonic), descending — newest first. The sort is stable across pages; an item that exists at request time will appear exactly once during a sustained pagination, even if other items are added concurrently.