Skip to content

Reference

What holds across every endpoint, gathered so no resource page has to repeat it: the shape a request and a response take, the status codes and error bodies they carry, and the rate limiting, pagination, and conditional requests that apply to all of them. The data types every parameter is described in terms of are at the end.

A resource's own object — a table, a frame, a position — belongs on that resource's page rather than here.

Response objects

A successful response is the requested resource itself: a bare JSON object with snake_case keys, or a bare array for list endpoints. There is no envelope — the payload is not wrapped in type, resource, method, or data fields.

json
{
  "id": "984c03a9-7a64-4046-a26f-57cc773c7ec5",
  "title": "Ice Cream Scoop Showdown",
  "purpose": "Rank freezer-case pints across flavor, texture, and value"
}

Request objects

A request body is a bare JSON object with snake_case keys, sent with Content-Type: application/json. There is no envelope here either — the payload is not wrapped in a field naming the resource.

Each endpoint lists the parameters it takes, in its own section on that resource's page. A key the endpoint doesn't define is ignored rather than refused, id among them — the API assigns every id and never reads one out of a request body.

Status codes

Every response carries a status code. A 2xx means the request succeeded. Anything else is an error, reported with the status code itself and never nested inside a 2xx response.

StatusMeaning
200 OKThe request succeeded. The body is the requested resource.
201 CreatedThe resource was created. The body is the new resource.
204 No ContentThe request succeeded with nothing to return. Delete endpoints use this.
304 Not ModifiedWhat you already hold is current. See Conditional requests.
401 UnauthorizedMissing or invalid token.
403 ForbiddenValid token, but the wrong tier or an ownership mismatch.
404 Not FoundThe resource does not exist, or is not visible to your token.
409 ConflictThe request conflicts with the current state — deleting a record another record still depends on.
422 Unprocessable ContentValidation failed.
429 Too Many RequestsA rate limit was exceeded. See Rate limiting.

Your token is authenticated before it is authorized, so a request carrying no token at all is a 401, never a 403.

Each endpoint lists the codes it can return, with two exceptions no endpoint repeats: the 429, which any request can meet, and the 304, which any conditional read can.

Errors

An error body carries the reason it failed. A 422 lists the offending fields, each with an array of messages:

json
{
  "errors": {
    "name": ["has already been taken"]
  }
}

Every other error carries a single message:

json
{
  "error": "..."
}

Rate limiting

Requests are counted over a rolling five-minute window. A request carrying a token is counted against that token rather than against the address it came from, so one integration's traffic never competes with another's for the same allowance.

What is countedLimit per five minutes
Requests carrying a token3,000 per token
Requests carrying no token300 per address
Every request5,000 per address

The last row is a ceiling sitting above the other two rather than a fourth allowance. A single caller presenting the same token reaches its own limit long before it.

Repeated authentication failures are counted separately. Fifty rejected tokens from one address within five minutes bar that address for fifteen minutes. A request carrying no token at all is not a failure and never counts toward this — only a token that was presented and refused.

Every refusal is a 429, in the same shape as any other error, carrying a Retry-After header giving whole seconds:

json
{
  "error": "Rate limit exceeded. Retry in 42 seconds."
}

Nothing reports how much of an allowance is left. There is no header to read ahead of being refused, so treat Retry-After as the signal and wait it out rather than retrying straight away.

Pagination

List endpoints are paginated. Pass page and limit as query parameters:

ParameterTypeDescription
pageinteger (optional)The page of records to return. 0 or 1 returns the first page.
limitinteger (optional)The number of records per page. Defaults to 20, with a maximum of 100.

An unusable limit resolves rather than failing, with one exception. Above the maximum it is capped at 100. Absent, 0, or not a number at all, it falls back to the default 20. A negative limit is the exception, and comes back as a 422:

json
{
  "errors": {
    "limit": ["must be at least 1"]
  }
}

The list itself is returned as a bare array. Pagination metadata is carried in the response headers, following RFC 8288:

HeaderDescription
linkRFC-8288 links to the first, prev, next, and last pages.
total-countThe total number of records across all pages.
current-pageThe page number of this response.
page-limitThe number of records per page.
total-pagesThe total number of pages.

HTTP header names are case-insensitive, so your client may present these with different capitalization than shown here.

Conditional requests

Every 200 and 201 carries an etag header naming that exact response body:

etag: W/"7c1e4b90d2af6538e0b47cd91a2f6e83"

Hand it back on a later read as If-None-Match. If the response would be what you already hold, it comes back as a 304 Not Modified and the body is not sent again:

bash
curl -i "https://api.cxipgroup.com/tables/984c03a9-7a64-4046-a26f-57cc773c7ec5" \
  -H "Authorization: Bearer ustk_b3d9f0a2-1c47-4e8a-9f65-0d2b8c7e3a41" \
  -H 'If-None-Match: W/"7c1e4b90d2af6538e0b47cd91a2f6e83"'

Send the value back exactly as it arrived, W/ prefix and quotes included. The comparison is a string match against the whole header, so a variation that looks equivalent is not one — and it fails quietly, answering 200 with the full body rather than reporting anything:

What you sendWhat happens
W/"7c1e4b90…"304, as intended.
"7c1e4b90…" — the same tag without its W/200. The prefix is part of the string being matched.
*200. A wildcard is not recognized.
W/"7c1e4b90…", W/"a9c4…"200. Only one tag per request, so a list matches nothing.

Only GET and HEAD are conditional. An If-None-Match sent with a PUT or a DELETE is ignored, and there is no If-Match precondition — an etag cannot be used to hold an update against a concurrent write. If-Modified-Since is understood but never matches, because no response carries a last-modified to compare it against.

The tag is derived from the response body and nothing else, so it changes when the body changes and at no other time. It is not a version number and carries no ordering: two tags tell you whether the body differs, never which came first. Sort on created_at or updated_at for that.

A 304 saves you the body, not your allowance. The request is counted against your rate limit before the tag is ever compared, so revalidating on a tight loop is as expensive as fetching.

A 204 No Content carries no etag, and neither does any error. There is nothing to revalidate against a response with no body.

Data types

Every parameter and attribute on this site is described with one of these tokens. They are listed alphabetically.

Array values array

Arrays are ordered lists of values — for example, a table's positions, or the bare array a list endpoint returns.

Datetime values datetime

Timestamps are ISO-8601 strings in UTC (for example, 2026-08-03T11:21:45Z).

Decimal values decimal

Decimals are JSON numbers that may carry a fractional part. They appear where a value is computed rather than stored — the positions average is the one that does — and are rounded to two decimal places. A range constraint, where there is one, is noted on the field.

Integer values integer

Integers are whole numbers. Some fields constrain the range — for example, position coordinates accept 0–100 — and that constraint is noted on the field.

Object values object

An object is a JSON object whose keys Shapecaster does not define. It appears where the contents are yours rather than ours: a table document's data is the one that is, and it comes back exactly as it was saved. The top level must be an object — not an array, string, or number — and any nesting below that is up to you.

String values string

Strings are series of UTF-8 encoded characters. Length limits vary by field rather than sharing a single global cap:

FieldCap
Frame name, val1val450 characters
Frame valN_min_label, valN_max_label50 characters
Frame valN_question255 characters
Table title50 characters
Table purpose255 characters
Position headline120 characters
Table link label, table document label100 characters
Table link url2048 characters

Token values token

A token is the string a request authenticates with — a UUID behind a prefix naming which tier it carries.

An admin token, issued to a client:

json
"adtk_c1f4e2a7-9b3d-4e6a-8f21-0a5c7d9e1b34"

A user token, returned when a session is created:

json
"ustk_b3d9f0a2-1c47-4e8a-9f65-0d2b8c7e3a41"

UUID values uuid

A UUID is how Shapecaster identifies a resource. Ours are version 4 and are returned in lowercase.

json
"984c03a9-7a64-4046-a26f-57cc773c7ec5"

Every id is a UUID, and so is every field naming another resource's id — a table's frame_id and parent_id, a link's or document's table_id. A user tag is one as well, and a token is one behind its adtk_ or ustk_ prefix.

A version 4 UUID is random, so nothing about a resource can be read out of its id — sorting a set of them gives an arbitrary order rather than a chronological one. Sort on created_at when you want creation order.

Treat a UUID as opaque, and let the API mint them. You never send one for a resource you are creating — the id is assigned on create, and an id in a request body is ignored.

Shapecaster API Documentation