Skip to content

Errors & pagination

Reference

Greenlight’s MCP tools and webhooks use the same conventions for errors and pagination. One page; every surface works the same way.

Every error response — REST, MCP, webhook delivery failure — uses this flat shape:

{
"code": "app.slug_in_use",
"message": "An app with slug 'expense-tracker' already exists in this organization.",
"details": { "slug": "expense-tracker" },
"next_steps": "Pick a different slug, or ask the current owner to add you as an editor.",
"request_id": "req_01HXYZ4M2P"
}
  • code is stable across versions and uses <domain>.<reason> snake_case. Clients should switch on code, not on message.
  • message is human-readable, one sentence. Greenlight reserves the right to improve wording.
  • details (optional) carries structured, code-specific context — which field failed validation, which check failed in the policy gate, and so on.
  • next_steps (optional) is an actionable hint for the caller.
  • request_id is unique per request. Including it in a support ticket gets to the root cause fastest.

Over MCP the same body comes back as JSON text inside content, with isError: true and no structuredContent — a tool’s advertised output schema describes its success payload, so an error body (which doesn’t match that shape) never goes in structuredContent. Parse content[0].text as JSON to get code / message / next_steps / request_id.

HTTP When
400 Input failed validation, or a connected-database query/response shape was rejected. details identifies ownership and retry safety for SQL calls.
401 No valid session, bearer token, or required actor token. details.issuer identifies the configured identity provider when known. Code is auth.token_invalid (missing/malformed) or auth.token_expired (TTL elapsed — re-authenticate).
403 Authenticated, but the caller isn’t authorized for this action.
404 The target resource doesn’t exist or the caller can’t see it.
409 The action conflicts with current state (e.g. a slug in use, or a stale Knowledge base_version).
422 The request was well formed but its content can’t be used: for example a build token usage report whose counts or times cannot be real (token_usage.implausible), or a DNS zone that is not verified yet.
429 The caller’s bucket is exhausted. Retry-After indicates when to retry. details.retry_after_ms carries the same value in milliseconds.
413 The explicit request-body cap was exceeded (validation.body_too_large).
499 The caller disconnected while connected-database work was queued or active. This is a Greenlight response code for observability, not proof that a write rolled back.
500 A platform error. The audit log captures these; report with the request_id.
502 An external dependency or its network/auth path failed. Retry only when the code’s contract says the operation is replay-safe; SQL errors state this explicitly.
503 A bounded platform resource is temporarily full. The SQL gateway returns proxy.query_capacity with Retry-After.

The most common stable codes. Not exhaustive; the full set lives in the platform’s shared error module.

Code Meaning
app.slug_in_use Another app in the org already owns this slug.
app.not_found The app doesn’t exist or the caller can’t see it.
app.not_owned Authenticated, but the caller isn’t an owner or editor of this app.
app_co_owner.group_derived The person is an editor through a group bound to the app, not individually, so there is nothing to remove. Unbind the group, or take them out of it.
app.invalid_slug Slug doesn’t match the required pattern.
app.not_deployed The app has not deployed yet (e.g. a preview URL or diagnostics snapshot was requested before first deploy, or no pod is running).
app.unreachable curlApp could not complete an HTTP response from the deployed app. Check the app’s logs; details.hit_app says whether a response began before the failure.
permission.scope_denied Proxy call to an integration the app isn’t granted.
permission.pending The app used an integration whose grant is still in IT review.
proxy.actor_token_required A user passthrough integration call omitted the actor token. These integrations act as the signed-in user, so a call with nobody signed in cannot run; scheduled work uses a service-identity integration.
proxy.actor_token_invalid The actor token expired, was minted for a different app, or its Greenlight session has since ended. The user signs in again and the app forwards the fresh token.
proxy.actor_token_impersonated The actor token was minted by an administrator requesting the app as another person. An integration that acts as the signed-in user refuses it; Greenlight never reaches an upstream as a user who is not the one making the request.
proxy.user_connection_required The signed-in user has not connected their account for this integration, or the stored connection stopped working because the upstream refused to refresh it. details.connect_url opens Greenlight’s Connect page: show it as a Connect button, never as a redirect the person did not click, keep the rest of the app working, and retry once they return.
proxy.upstream_user_unmapped The upstream has no account Greenlight may act as for the signed-in person. For Box: no single active, non-admin Box user whose login is their email; next_steps names the email looked up. For a Fabric Warehouse: it refused their login, usually because they have no access to it — IT grants them a workspace role or item permission. Either way IT fixes it upstream, and the app cannot.
proxy.query_failed A connected-database /query or inspectIntegrationDb statement was rejected by the database itself (syntax, permission, cast); the message is the database’s own.
proxy.query_timeout A connected-database /query statement exceeded the execution-time cap. Narrow it; do not replay a possible write based on this code alone.
proxy.query_result_unsupported A query returned a second result grid, too many columns, or an unsafe/unbounded native type. Reshape the result or cast to a bounded scalar type.
proxy.query_capacity The SQL execution bulkhead is full. The statement did not start; honor Retry-After.
proxy.query_canceled The caller disconnected while SQL work was queued or active. Inspect execution_outcome before reasoning about a write.
inspect.user_connection_required inspectIntegrationApi or inspectIntegrationDb on a user passthrough integration runs as you, and you have not connected your account for it, or the connection stopped working. Open details.connect_url in a browser, then run the call again.
inspect.transaction_control An inspectIntegrationDb statement began, committed, or rolled back a transaction. Inspection already runs the statement inside a transaction it rolls back; remove the transaction statements.
proxy.upstream_unreachable The connected database or another upstream could not be reached. SQL responses mask the driver message and carry structured ownership/outcome fields.
internal.audit_unavailable Greenlight could not persist the required SQL audit event. The database operation may already have committed.
internal.unexpected Unexpected platform fault; internal detail is masked. Report the request_id.
ai.model_denied The AI request used a model alias not granted to the app.
auth.session_expired The session token expired or was revoked.
auth.token_invalid No valid bearer token or the token is malformed/revoked.
auth.token_expired The bearer token’s TTL elapsed; re-authenticate via the agent plugin.
rate_limit.exceeded The caller’s bucket is exhausted.
env.reserved_name envSet tried to set a platform-owned name (e.g. DATABASE_URL).
env.policy_denied Org policy blocked the env operation.
validation.body_invalid Request body failed validation; details carries the issue path.

Every connected-database gateway error adds a stable decision record under details:

Field Meaning
fault_origin The responsible boundary: request (caller SQL/shape), policy (grant or database-role denial), connected_database (the upstream database), platform (Greenlight), or unknown when the boundary cannot prove responsibility.
retryable The same logical operation could plausibly succeed later. This is not permission to replay it.
safe_to_retry Replaying the identical request cannot duplicate a side effect. Automatically retry only when this and retryable are both true.
execution_outcome not_started or unknown. Once execution may have begun, Greenlight does not claim commit or rollback certainty.
database_code The SQL Server error number when available. Switch on this field, not database-message prose.

This is how a client distinguishes platform fault from its own fault: fault_origin: "platform" means Greenlight failed; "request" means the submitted query or result shape needs correction; "policy" means the operation is not allowed; and "connected_database" identifies the external database. "unknown" is intentional when a transport boundary cannot assign blame safely. The same request_id appears in the error body and X-Request-Id response header and correlates to Greenlight logs and audit.

List endpoints — REST and MCP — use opaque cursor pagination, not offset/limit. Offsets are unsafe under concurrent inserts (they double-count or skip) and get expensive at large offsets. They accept limit (default 20, max 100) and cursor; the response includes next_cursor, or null on the last page.

{
"items": [ /* … */ ],
"next_cursor": "eyJrIjoiMjAyNi0wMy0xMlQxNDowODoxMVoiLCJpZCI6IjhhM2IxYzRmIn0="
}

To paginate forward, pass next_cursor back as cursor. To start from the beginning, omit cursor. There is no offset parameter.

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.

Where a list endpoint accepts filters, they are query params applied by the server, so a filtered read covers the whole collection rather than the page you happened to fetch. Several filters combine as AND. Multi-value filters accept either repeated (?tag=a&tag=b) or comma-separated (?tag=a,b) form, and substring search is case-insensitive with %/_ treated literally. A boolean filter takes the literal strings true or false and rejects anything else; leaving it out means “do not narrow on this”, so omitting one always gives you the wider result set.

A cursor belongs to the filter set that produced it. When you change a filter, start again without a cursor. Some endpoints bind the filter into the cursor and reject a mismatched one as validation.cursor_invalid rather than returning a page from a set you did not ask for, so carrying a cursor across a filter change surfaces as an error rather than a silent reshuffle.

Each list has a defined sort key — documented per endpoint in the platform API conventions — with a stable tiebreaker so an item appears exactly once during a sustained pagination even as others are added concurrently. Many lists are newest-first by creation time; the apps catalog is newest-deploy first (never-deployed apps last). The cursor encodes that position; you never sort client-side.

Some endpoints also accept an explicit ordering (sort and order) over a small set of fields. A cursor belongs to the ordering that produced it, so pass the same sort and order on every page of a walk; changing either means starting again without a cursor. Sending a cursor with a different ordering is rejected as validation.cursor_invalid rather than silently returning a page from the wrong sequence.