Response format & pagination
Every /v1 response — success or error — shares one envelope. Once you have parsed it once, you have parsed all of them.
The envelope
{
"statusCode": 200,
"errorCode": null,
"message": null,
"data": { }
}
| Field | Type | Meaning |
| --- | --- | --- |
| statusCode | integer | Same value as the HTTP status code. |
| errorCode | string or null | null on success. A stable, machine-readable code on failure — see Errors. |
| message | string or null | null on success. A human-readable description on failure. Safe to log, not safe to pattern-match on — match errorCode instead. |
| data | object, array, or null | The payload on success. Always null on failure. |
A success response always has errorCode: null and message: null; a failure response always has data: null. The shape never mixes.
// A single resource
{ "statusCode": 200, "errorCode": null, "message": null, "data": { "id": "...", "name": "..." } }
// A failure
{ "statusCode": 404, "errorCode": "not_found", "message": "Resource not found", "data": null }
Pagination
Every list endpoint accepts:
| Parameter | Default | Meaning |
| --- | --- | --- |
| limit | 50 | Items per page. Must be between 1 and 100; anything else is a 400 bad_request. |
| cursor | none | An opaque string from a previous response's next_cursor. Pass it back exactly as received. |
A paginated response adds one key next to data, never inside it:
{
"statusCode": 200,
"errorCode": null,
"message": null,
"data": [ { "id": "..." }, { "id": "..." } ],
"next_cursor": "eyJ0IjoxNzM3..."
}
next_cursor is a string when another page exists, and null on the last page (the key is still present — never omitted). Walking every page looks like:
cursor=""
while :; do
res=$(curl -s "https://api.oneguard.one/v1/vaults?limit=100${cursor:+&cursor=$cursor}" \
-H "Authorization: Bearer $TOKEN")
echo "$res" | jq -c '.data[]'
cursor=$(echo "$res" | jq -r '.next_cursor // empty')
[ -z "$cursor" ] && break
done
A malformed or expired cursor is a 400 with errorCode: "invalid_cursor" — construct it only by round-tripping a next_cursor you were given, never by hand.
Idempotent writes
Any POST that creates a resource accepts an Idempotency-Key header (any string, 1–255 printable ASCII characters). Send the same key on a retry of the same request and you get back the exact original result instead of creating a second resource:
curl -s https://api.oneguard.one/v1/vaults \
-X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: 8f14e45f-...-a3b2" \
-H "Content-Type: application/json" \
-d '{"name": "Marketing site"}'
| Situation | Result |
| --- | --- |
| Same key, same request body | The stored response is replayed, with an added Idempotent-Replayed: true header. |
| Same key, a different request body | 422 unprocessable_entity with errorCode: "idempotency_key_reuse". |
| Same key, while the first request is still being processed | 409 conflict with errorCode: "idempotency_in_progress". |
| No Idempotency-Key header | The request always runs; no dedup happens. |
Keys are scoped per organization, per API key, per route and method, and are remembered for 24 hours. A request that fails (any 4xx or 5xx) stores nothing, so retrying after a failure with the same key simply runs the request again.
Request bodies
POST/PATCH bodies are JSON objects, Content-Type: application/json, capped at 1 MB. An empty, missing, or non-object body on a route that requires one is a 400 bad_request.
Headers on every response
| Header | Meaning |
| --- | --- |
| X-Request-Id | req_<uuid>. Quote this when contacting support about a specific request — it is never duplicated inside the JSON body. |
| Cache-Control: no-store | Nothing from /v1 is ever cacheable. |
CORS is closed: there is no Access-Control-* header on any /v1 response, including preflight. Call the API from a server, not from a browser.