Skip to main content

API recipes: contact lifecycle

The contact-lifecycle cookbook. The canonical pages — Opt-outs API, Segments API, Frequency Caps API, Analytics API, Contacts API — document each endpoint; these recipes chain them into runnable loops: create, refresh, enrich, consent, read, decide. Every request sample ships as a six-language group — cURL, Node.js (TypeScript), Python, Go, Ruby, PHP, in that order, each dependency-free so it runs without an SDK installed — followed by the complete response envelope. All five loops stay on tenant-owned controls — the contact record, opt-outs, segments, and caps are your rules, set and enforced by your key. Base URL is https://api.orbit.devotel.io/api/v1 throughout; authenticate with X-API-Key and a test key (dv_test_sk_…) for the sandbox. The base lifecycle chain, run against the contact record itself: pin your source-of-truth id on create, refresh idempotently by that pin, write attributes and consent as recorded state, read back to verify, and scan for duplicates the pin could not prevent. This is the recipe form of Import, dedupe, and upsert contacts; the counters and endpoints agree with that page. Create with external_id — the pin that keeps one live row per customer:
201
A resend of the same external_id resolves to this row and refreshes it — it never mints a second. Phone numbers normalize to E.164 server-side, and a contact needs at least one of phone or email at creation. Upsert refresh — the refresh path is the same write at scale: POST /contacts/bulk keys every row on external_id, so an existing pin updates the row instead of inserting a new one. Up to 10 000 rows per call; the response carries the same inserted / updated / skipped counters as the bulk import loop:
200
Sum inserted + updated + skipped per chunk and reconcile against your source row count; skipped rows download per job from GET /contacts/import-jobs/{id}/skipped.csv. A rerun of an identical batch moves rows into updated or skipped — never into a second live contact. Custom-field write — attribute and custom-field writes ride the contact update; import the scalar facts you personalize with, never derived values (segments, scores), which the CDP recomputes from events:
200
Attribute writes merge per key; keys you omit keep their stored value. Field definitions (types, display names) live in custom fields. Consent write — consent rides the contact as recorded state, written exactly as your source holds it (grant as grant, suppression as suppression; consent management):
200
Writing consent never re-triggers a welcome or double opt-in for an already-consented contact; rows whose source cannot state a basis land unconsented and go through your capture flow once. Read-back — verify the row as stored before you cut over:
200
Phone came back as E.164, the pin matches your source, attributes and consent carried over — the row is cut-over ready. Duplicate scan — the pin prevents new duplicates; scan for the estate you already have:
200
Page GET /contacts/duplicates for the full cluster list; identity resolution walkthrough covers reading them, and POST /contacts/merge combines a cluster into one survivor. Merge after import, never to hide a missing pin.

1. Opt-outs: bulk suppress → export → re-opt-in one channel

A suppression file arrives from support and you need the round trip: opt the contacts out in one batch, export the suppression list as compliance evidence, then restore the one contact who re-consents by phone or email. contact identifies the address-book row by phone (E.164) or email — pick the id off GET /contacts first if your file lists raw addresses. Bulk opt-out — up to 500 rows per call, per-row outcomes isolated:
207 Multi-Status — the full-envelope batch result. succeeded / skipped / failed count independently, so one unknown phone number never rolls the rest back:
207
A failed fifth row would show ok: false, status: "failed", and an actionable error string — clean the named rows and resend only them; skipped rows (already opted out) never double-stamp the consent trail. Export the suppression list — compliance evidence, up to 10 000 contacts:
200
Omit channel to export every opt-out across channels. Past 10 000, page GET /contacts/optouts on its cursor instead — same rows, no cap. Re-opt-in the one contact who re-consents — delete the channel’s opt-out:
200 returns the updated contact row. The contact is eligible again on that channel subject to consent records — the delete clears the suppression, and any affirmative-consent requirement (a WhatsApp 24-hour window, RCS marketing consent) still has to be satisfied by your own capture. The round trip closes: every enlisted row, plus one restored. Labelled error envelope — a channel outside the canonical nine (case-insensitive, whitespace-trimmed) fails the whole request before any row is processed:
422
Nothing was written on a validation failure, so retrying the corrected body is always safe; an already-opted-out single-row write resolves as 200 with already_opted_out: true, not an error. Canonical page: Opt-outs API. Envelope reference: API error handling by example.

2. Segments: autopilot → evaluate → materialize-lookalike → members

The AI-assisted audience loop: describe the audience, persist the suggestion as a real segment, force a re-evaluation when data moves, then grow it — preview a lookalike expansion and materialize it as a static segment you can page members out of. Autopilot from a description — suggest rules + live preview in one round-trip, nothing persisted:
200
Persist the winner with POST /contacts/segments (the main cookbook’s task 15); membership materializes on create. Labelled error envelope — the model emits a rule the segmentation engine cannot validate, and the error names the field:
422
Retry with a tighter description; a 503 means no AI provider is configured on the deployment — fall back to the manual builder. Evaluate on demand — after data (not rules) changed, rebuild membership now instead of waiting for the periodic refresh:
200 returns the evaluation result — the matched count, duration, and the membership diff — in the standard envelope. Materialize a lookalike audience — promote a preview into a new static segment (no rules, auto_refresh off — the k-NN snapshot is a point-in-time cohort the refresh scheduler never wipes):
201 returns materialized: true plus the new segment — its id is a first-class segment you activate like any other. A preview with no candidates returns 200 with materialized: false and a reason, persisting nothing. Read members — page the materialized snapshot with the same cursor contract as any list:
Pass meta.pagination.cursor while has_more is true. The members list reads the last-materialized snapshot — call evaluate (above) first if you need the very latest audience. Canonical pages: Segments API (AI surfaces), Contacts API (saved-segment CRUD + membership).

3. Frequency caps: read the rules behind a send decision

Caps are the tenant’s rolling-window limits on how often one contact can be messaged. Two reads serve the “what applies to this contact?” question: the per-contact rules read, then the live per-recipient usage you branch a send decision on. Rule writes ride PATCH /frequency-caps/:id; reads ride GET /contacts/:id/frequency-caps (rules) and GET /contacts/:id/cap-status (live usage) — the decision loop below uses the rule IDs from the first read to target back-off per category. Read the rules that apply to the contact — flat plus grouped by channel, globals separated:
200
Read live usage for the “can I send?” decision — snapshot across every channel, with the consent state folded in:
Per cap that applies, the response carries max_count / current_count / sends_remaining and (once slots are exhausted) the next_slot_at ISO timestamp, plus the channel’s consent state — one read answers both “how many left” and “may we send at all”. Update a rule — widen the marketing SMS cap; rule writes are owner/admin/developer with frequency-caps:write:
Decide in your send gate:
Labelled error envelope — a send that would exceed the cap:
429
Defer that recipient and continue the batch (the main cookbook’s error table has the retriable-vs-terminal split). Caps are exclusion enforcement, not a block on your whole send plan — suppression lists and opt-outs gate in parallel (Opt-outs round trip above). Canonical pages: Frequency Caps API, Contacts API.

4. Usage: fetch a day of metering for guardrails

The metering guardrail read: the home-overview usage chart’s same daily timeline, pulled for a one-day window so your own guardrails (budget dashboards, spend alerts, anomaly heuristics) sit on the numbers the platform charts. Read-only; an explicit start_date / end_date pair takes precedence over the days trailing window.
200
Feed totals into a budget check before you queue a batch, or row-by-row into a daily anomaly heuristic. For spend-shaped guardrails instead of volume, the billing surface’s spend series and burn rate (the main cookbook’s task 22) price the same window. Labelled error envelope — an explicit pair with a missing end:
400
A lone start_date (or end_date) is a 400 — the pair refines together, and the details issues list names the fix instead of silently falling back to the trailing days window. Canonical page: Analytics API → per-day usage timeline. Finer-grained metering (rate a batch, credit notes): Usage metering.

See also