Two shapes, always the same.
A successful call returns data. A failed call returns
error. You can tell them apart by the status code and by the one key at the top.
Success
A 2xx response with a JSON body wraps its payload as { "data": … }. A list puts an array in
data and adds pagination; what that object means depends on the
endpoint, see pagination.
curl -s https://app.upfour.io/v1/monitors/$MONITOR_ID \
-H "Authorization: Bearer $UP4_API_KEY"{ "data": { "id": "…", "name": "Example", … } }
{
"data": [ … ],
"pagination": {
"cursor": null,
"next_cursor": null,
"prev_cursor": null,
"limit": 50,
"has_more": false
}
}- The envelope applies to every call made with an API key. A response that already carries a top-level
dataorerroris passed through unchanged. 204 No Contenthas no body. File downloads such as report exports are not JSON and are not wrapped.- New fields can appear in a response; the changelog lists them as added. Ignore fields you do not know.
Errors
Every 4xx and 5xx response uses one object, with Content-Type: application/json.
Errors are not wrapped in data.
{
"error": {
"code": "insufficient_scope",
"message": "API token does not have the required scope",
"request_id": "req_…",
"details": { "required_scope": "monitor.write" }
}
}| Field | Meaning |
|---|---|
error.code | A stable identifier in snake_case. Branch on this. |
error.message | A sentence for people. It can change; do not match on it. |
error.request_id | The id of this request, the same as the X-Request-Id response header. |
error.details | Only present when there is more to say: field names the invalid field, required_scope the missing scope, original_request_at the first use of an idempotency key. |
Every code, with its status and cause, is in the error catalogue.
Status codes
A status without a more specific code gets the default code below. Many errors carry a specific code instead, such as
cursor_expired or idempotency_key_conflict.
| Status | Default error.code | Usually means |
|---|---|---|
| 400 | bad_request | The request is malformed or a field is invalid. A field error adds details.field. |
| 401 | unauthorized | No valid key. See API keys and scopes. |
| 402 | payment_required | The Company’s plan or billing state does not allow the action. |
| 403 | permission_denied | Refused. Scope errors use their own codes, see API keys and scopes. |
| 404 | not_found | The resource does not exist, or your key cannot see it. |
| 409 | conflict | The request conflicts with the current state. Idempotency conflicts use their own codes. |
| 429 | rate_limit_exceeded | Too many requests. Wait for Retry-After. |
| 503 | service_unavailable | A deliberate notice, for example a feature that is not available right now. The message is meant to be read. |
| other 5xx | internal_error | Our fault. The message is always “Internal server error”; send us the request_id. |
Any other status falls back to request_failed.
Request ids
Every response, success or failure, carries X-Request-Id. If you send the header,
up4 uses your value; otherwise it generates req_ followed by a UUID. Log it, and quote it when you contact support.
curl -si https://app.upfour.io/v1/me \
-H "Authorization: Bearer $UP4_API_KEY" \
-H "X-Request-Id: nightly-sync-2026-10-27" | grep -i '^x-request-id'Common mistakes
- Matching on
error.message. Match onerror.codeand the status. - Reading
dataon an error response. There is none. - Expecting a body on
204. - Treating every
403as “not allowed for good”. A missing scope or a read-only key can be fixed with a new key. - Retrying a
5xxor timed-out POST without anIdempotency-Key. With one, a retry returns the stored answer when the first request finished below 500; a5xxitself is not stored. See idempotency.
Was this page helpful?