Docs
Paging, retries and errors
How every list pages, how to retry a write safely, the headers every answer carries, and the error envelope with its codes.
These rules hold for every route in the Reference, whatever the product. A client written against them once works for every list and every write.
Paging a list
A list operation takes two query parameters and answers two fields beside its items:
| Field | Where | Meaning |
|---|---|---|
page_size | request | how many items at most; 0 or absent is the route's default, more than its maximum is the maximum |
page_token | request | the previous answer's next_page_token; absent for the first page |
next_page_token | answer | the token for the next page; "" on the last page, always present |
total_size | answer | how many items match, only on routes that can count them exactly; a route without it does not count |
curl -H "Authorization: Bearer $INORBIT_TOKEN" \
"https://api.inorbit.hr/v1/webhooks/endpoints/$ENDPOINT/deliveries?page_size=50"{ "deliveries": [{ "id": "…" }], "next_page_token": "AXQ…" }Ask again with page_token=AXQ… and the same other parameters, until
next_page_token is "".
- The token is opaque: send it back as it came, never build or change one.
- A token belongs to the filters it came from. Sent with any other parameter changed,
it is
400 bad_requestnamingpage_token; to change a filter, start again from the first page. - A page can be shorter than
page_sizewithout being the last. Only an emptynext_page_tokensays the list is done. - The order is newest first unless the route documents an
order_by. - Where a route had a
limitparameter before, it still works as an older name forpage_size;page_sizewins when both are sent.
A list answer with another page also carries a Link header (RFC 8288)
to it, so a client that follows links pages without reading the field:
Link: </v1/webhooks/endpoints/ep_1/deliveries?page_size=50&page_token=AXQ…>; rel="next"The OpenAPI document marks every list operation with x-iohr-list, naming its items
and its page fields. A client generated with iohr uses it: beside each
list method it adds an iterator over every item, which follows the token for you, stops
fetching when you stop reading, and stops at the first error.
| Language | Iterator |
|---|---|
| Go | for d, err := range api.Radar().AllListDigests(ctx, p) (iter.Seq2) |
| TypeScript | for await (const d of api.radar.allListDigests()) |
| Python | for d in api.radar.all_list_digests():, or async for on the asyncio client |
| Rust | while let Some(d) = pages.next().await on api.radar().all_list_digests(&p) |
| Java | for (Digest d : api.radar().allListDigests()), or .stream() |
| C# | await foreach (var d in client.Radar().AllListDigestsAsync()) |
Retrying a write
A write that creates or triggers something takes an optional Idempotency-Key header
on the operations whose reference lists it, for example creating a webhook endpoint,
sending a test event, retrying a delivery, creating a connection or a monitor, and
adding or checking a domain. Use a fresh UUID per logical write, and send the same one
again when you retry it:
curl -X POST https://api.inorbit.hr/v1/webhooks/endpoints \
-H "Authorization: Bearer $INORBIT_TOKEN" \
-H "Idempotency-Key: 6f1c2a9e-5b0d-4c7a-9e3f-2d8b1a4c7e10" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/hooks", "event_types": ["webhook.test"]}'| What happens | Answer |
|---|---|
| the first call with a key | runs, and its answer (success or refusal) is kept for a day |
| a repeat with the same key and the same body | the kept answer, with Idempotency-Replayed: true; nothing runs again |
| a repeat while the first call still runs | 409 conflict: wait and send it again |
| the same key with a different body | 422 unprocessable: use a new key for a new write |
| a key that is not 1 to 255 visible characters | 400 bad_request |
Without the header a write runs every time it is sent, so a write without a key should
not be retried blindly. Reads (GET) and PUT/DELETE are safe to repeat as they are.
What every answer carries
| Header | Meaning |
|---|---|
x-request-id | the call's id, given by the edge. Keep it with your logs and quote it when you ask us about a call |
Link: <…>; rel="next" | on a list with another page (above) |
X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset | the per-key rate limit: the size of the window, what is left of it, and the seconds until it refills |
Retry-After | on a 429 and on any refusal with a retry detail: the seconds to wait |
Idempotency-Replayed: true | on an answer repeated for an Idempotency-Key (above) |
Errors
Every error, from the API or from the edge in front of it (a missing token, a scope the token lacks, the rate limit, an unknown route, a service that is down), is the same JSON envelope, never plain text:
{
"code": "bad_request",
"error": "page_token does not belong to these filters",
"details": [{ "type": "field", "field": "page_token", "description": "belongs to other filters" }],
"request_id": "0199a1f2-7c3e-7d41-9b2a-5e8f0c6d1a23"
}code is a stable slug and the status follows it; error is a sentence for a person;
details are typed entries (a field, an info with a machine-readable reason, a
retry with after_seconds); request_id is the call's x-request-id. The codes,
their statuses and what to do with each are on Errors. Which to retry:
| Retry | How |
|---|---|
429 rate_limited | after Retry-After seconds |
503 unavailable, 504 timeout | with backoff (a growing, randomised wait), a few times |
409 conflict on a write with a key | send the same key again after a moment |
| anything else | do not retry: fix the request, or read the error |
A write without an Idempotency-Key is retried only when you know it did not run.