Pagination & Errors
Pagination
Section titled “Pagination”List endpoints use cursor pagination. A page looks like this:
{ "items": [ /* ... */ ], "nextCursor": "djE6OTIxOTA1"}To fetch the next page, pass nextCursor back as the cursor query parameter:
curl "https://publicapi.shout.com/v1/forms?limit=50&cursor=djE6OTIxOTA1" \ -H "Authorization: Bearer $SHOUT_API_KEY"When nextCursor is null, you’ve reached the last page — stop.
{ "items": [ /* ... */ ], "nextCursor": null }Rules:
- The cursor is opaque. It’s an internal token — don’t parse it, build it, or reason about its contents. Pass it back verbatim. A malformed cursor returns
400 invalid_request. limitcontrols page size: default 25, maximum 100. Values above 100 are capped at 100;0falls back to the default.- Cursors encode a stable sort, so walking pages won’t skip or duplicate rows even as new data arrives. Use a filter like
updated_since/since(not the cursor) to poll for new data.
A few small collections (workspaces, groups, inboxes) are returned unpaginated — they still use
the { "items": [...], "nextCursor": null } shape for consistency, but nextCursor is always null.
Error envelope
Section titled “Error envelope”Errors (except 401 and 429, see below) return a JSON body with a stable machine-readable
error code and a human-readable message:
{ "error": "not_found", "message": "No form with this id exists."}Branch on the error code, not the message (messages may be reworded).
Status codes
Section titled “Status codes”| Status | error code | Meaning |
|---|---|---|
200 OK | — | Success. |
201 Created | — | A resource was created (returns the new resource; a Location header where applicable). |
204 No Content | — | Success with no body (e.g. delete). |
400 Bad Request | invalid_request | Malformed request — bad cursor, unknown filter value, missing required field. |
401 Unauthorized | (empty body) | Missing, invalid, expired or revoked key. |
403 Forbidden | insufficient_scope | Valid key, but it lacks the scope this endpoint needs. |
404 Not Found | not_found | No such resource, or it isn’t visible to your tenant. |
409 Conflict | conflict | The request conflicts with existing state (e.g. a duplicate contact email — the body includes the existing contactId). |
422 Unprocessable | unprocessable / unprocessable_entity | The request was well-formed but can’t be fulfilled (e.g. replying to a non-email ticket, or a form with no resolvable owner). |
429 Too Many Requests | (empty body) | Rate limit exceeded — see below. |
502 Bad Gateway | delivery_failed | A downstream delivery failed (e.g. a ticket reply email couldn’t be sent). Not caused by your request. |
Example: insufficient scope
Section titled “Example: insufficient scope”{ "error": "insufficient_scope", "message": "This endpoint requires the 'responses:read' scope."}Example: duplicate contact
Section titled “Example: duplicate contact”{ "error": "conflict", "message": "A contact with this email already exists.", "contactId": "1dcc380c-8076-2a37-5fb8-3a2265f83a67"}Rate limiting
Section titled “Rate limiting”Each key may make 120 requests per minute, measured as a sliding window. Beyond that:
HTTP/2 429retry-after: 5The Retry-After header gives the number of seconds to wait before retrying. Back off for that
long, then continue. Building a small retry-with-backoff wrapper around your HTTP client is
recommended for any high-volume integration.