Conventions

HTTP methods, status codes, response envelopes, request ids, content types, and time formats.

Updated 2026-08-16

HTTP methods

MethodUse
GETRead a resource or a list
POSTCreate, or run a named action (publish, assign-users, merge, retry)
PATCHPartial update — a merge: only the keys you send change, and a null value clears one
DELETERemove or deactivate

Record updates have no PUT. Updates there are merges by design, so a partial body can never wipe fields you did not send. (Earlier releases accepted PUT on records as an undocumented alias for PATCH; on the record endpoint it now returns 405. Other endpoints that accept PUT treat it the same as PATCH.)

For record updates specifically, PATCH validates the fields you send against the record's own form version by default — publishing a new version of a form does not break an integration mid-flight. Sending "schema": "current" opts the record into the newest version instead: the whole merged record is checked against it, and the record moves to that version when it conforms. Invalid payloads — unknown field ids, wrong value types, list values outside the option set — return 400 with a details array naming each problem.

Status codes

CodeMeaning
200Success
201Created
202Accepted — the work was queued (report merge returns this with a merge_run_id)
400validation_failed — body or query is malformed
401unauthorized — missing, expired, or invalid API key
403forbidden — valid key, wrong scope or cross-org access
404not_found
405method_not_allowed
409conflict — slug/identifier already exists, or idempotency replay with a different body
429rate_limited
500internal_error — unexpected server fault
502bad_gateway — an upstream call failed (e.g. a report merge)
503returned by /health when the database is unreachable; the body is a success envelope with data.status: "degraded", not an error code

Response envelopes

Single resource:

{
  "data": { ... },
  "request_id": "req_a1b2c3d4..."
}

List:

{
  "data": [ ... ],
  "meta": {
    "next_cursor": "eyJpZCI6Ii4uLiJ9" | null,
    "limit": 25
  },
  "request_id": "req_a1b2c3d4..."
}

Error:

{
  "error": {
    "code": "validation_failed",
    "message": "Invalid request body",
    "details": [
      { "path": "config.recipient", "issue": "Required" }
    ]
  },
  "request_id": "req_a1b2c3d4..."
}

details is included only when the server has structured field-level information. For other 4xx codes you'll just see code + message.

Warnings on a successful write

A write can succeed and still have something worth telling you about. When it does, the success envelope carries an optional warnings array:

{
  "data": { ... },
  "warnings": [
    {
      "code": "expression_cycle",
      "path": "fields",
      "message": "Circular visibility rule: field_a → field_a"
    }
  ],
  "request_id": "req_a1b2c3d4..."
}

The key is absent when there is nothing to report, so a client that ignores it sees exactly the envelope it saw before.

Warnings describe something we accepted but you probably did not mean. Read code and not message — the code is the contract, the wording may change.

POST and PATCH on /forms return the same warnings the form builder shows a person in its warnings panel. Until this release an integration building forms through the API got none of them, so a form could save cleanly and then behave in a way nobody meant to ask for. Six codes:

codeWhat it means
duplicate_field_idsTwo fields in the same scope share an id. One of them wins when a record is read.
expression_cycleA Visible when rule or formula refers back to itself in a circle.
icon_unresolvableA form or page icon that will not draw, so the crew sees a blank-looking button.
lookup_filter_unresolvableA lookup filter points at a field that is not there to read.
display_fields_unknownThe fields chosen to label a record in lists do not exist on the form.
unknown_field_referenceA Visible when rule or formula reads a field that is not on the form — usually one that was deleted after the rule was written.

Warnings never block. A warning does not become a 400 in a later release — a form that saves today has to keep saving — so an integration can log them, show them to whoever authors the form, and ship. Two of them are stricter later on. A circular expression, and a rule that reads a field which is not on the form, are both accepted while saving a draft — a form mid-authoring passes through either on its way to something sensible — and both are refused at publish and at ready-for-testing, because there is no reason to send one to a device. A dangling reference is worth the block: the usual shape of it hides the field that carries the rule, and a hidden field is not checked for being required, so a form can go out missing an answer nobody meant to make optional.

The warnings are read from the saved form rather than from your request, so a PATCH that changes one part of a form is judged on the whole thing, the way a field user will meet it.

One warning the builder shows is deliberately left out here: it nudges a person who has marked nothing required, which is a fair prompt in the builder and a false alarm over the API, where plenty of correct forms have no required field. A channel that cries wolf gets ignored, and the six above are worth reading.

The same warnings come back from the form tools on the MCP server.

Request id

Every response includes:

  • X-Request-Id header (e.g. req_a1b2c3d4e5f60718293a4b5c)
  • request_id in the body envelope

These match. Quote the id when reporting issues — we use it to join HTTP logs, audit rows, and observability events.

Content types

  • Requests: application/json for everything except file uploads (forthcoming in a later phase).
  • Responses: application/json. The report merge endpoint is no exception — it queues the render and returns 202 with a merge_run_id; the document is rendered once, then sent to every destination configured on the report.

Time format

All timestamps are ISO-8601 UTC: 2026-05-23T15:00:00Z. Send the same format on writes.

UUIDs

Resource ids are UUIDs in the 8-4-4-4-12 hex format. The API accepts both RFC 4122 v1-v8 UUIDs and lower-case hex with no version/variant bits (handy for fixture data).