Errors worth branching on
These five failures cover the custom-field create (POST /api/v1/custom-fields), which every form-builder save flows through. Each block below is a full{ error, meta } envelope as the API returns it, and the matrix at the bottom answers retry vs surface for the same five classes. For the platform-wide decision table these branches plug into, see the error handling guide.
401 — Unauthorized
401 on this page is the bearer key failing before the route ran — it never means the resource is wrong. Rotate the key or re-mint the scoped token; retrying the same request changes nothing.
403 — Forbidden
403 means the key authenticated but the operation is gated by scope — check the key’s scopes on the developer page; a 403 is never a data-not-found shape.
422 — Schema
422 means the payload did not match the request schema — branch on error.details.field and resend with an allowed name (letters/digits/hyphens/underscores) instead of blind-retrying the same body.
429 — Rate
Retry-After header and error.details.retry_after are both set on every 429 — resend the SAME request after the lower of the two.
409 — Conflict
60-second retry matrix
Worked request and response samples
Copy the request as written, substitute your own ids, and compare the response envelope — a success in the same shape the numbered error sections spell out.Define a custom field
POST /api/v1/custom-fieldsWorked sequences — field lifecycle
The blocks below walk one field through its whole life, end to end: define it, list and inspect it in the schema, edit what stays editable, retire it safely, and push values onto contacts. Each sample is a full request or response envelope you can copy; substitute your own ids. The error matrix above stays the branch table for every step.1 — Define a field (with an enum variant)
Theselect create above is the common shape. For a closed vocabulary of values, declare type: enum with the allowed values inline in enum_values — values written to contacts are rejected at the door if they are not in the list.
201
2 — List the schema and diff one field
List every definition the tenant has, then read one field back for the full validator set. The schema-diff check before a PATCH or DELETE is: GET list → GET{id} → GET {id}/dependencies (blast radius) or {id}/explorer (usage + redacted samples).
200
3 — Update what is editable; skip what is immutable
PATCH rewrites the label, description, options, default value, therequired / searchable flags, and validators (validator_regex, min_len, max_len, enum_values). The key and type are frozen — send them in the body and they are ignored; a rename of values already stored on contacts is how you orphan data. Passing null for a validator or default_value clears it.
200
key (vip_band) and type (select) come back unchanged — that is your confirmation the immutable pair survived the PATCH.
4 — Retire a field: dependency gate, then force-delete
DELETE is the archive path. Run it withoutforce first: if any segment, campaign, drip enrollment, or flow still references the field you get a 409 carrying the full dependency snapshot — that is the undo-safe version of the call, nothing was removed. Once the dependents are cleaned up (or deliberately abandoned), re-send with ?force=true; dependents are flagged broken and the definition plus every stored value is stripped from contacts.
204 No Content — the field is archived and its key stripped from every contact.
5 — Backfill a segment, then read it back through Contacts
Values land on a contact throughPUT /api/v1/custom-fields/values, one field per call, validated against the definition. Backfill a segment by iterating contact ids — the same call idempotent per contact — and confirm the write by reading the contact back through GET /api/v1/contacts/{id}.
200 — write accepted
200 — read-back through Contacts
error.details.field set to the failing key, matching the 422 branch in the matrix above.