Skip to main content

The contact resource

A contact is one record in your directory — phone, email, name, tags, consent, plus your own data layered on top. Custom-field values ride on the contact as the custom_fields object: set them on a contact and they come back inline on every contact response — create, read, list — with no separate lookup. Custom-field keys and their types are defined under /api/v1/custom-fields. Write a value with PUT /api/v1/custom-fields/values and read it back on the contact’s custom_fields object. Every response on this page follows the same envelope — data plus a meta block carrying request_id, timestamp, and, on list responses, pagination — so the examples below hold for every endpoint here. Errors follow Devotel Orbit’s { error, meta } envelope, shown once under Error envelope.

Worked sequence: create, set custom fields, read back

The run-through every integration does first: create the contact, attach your own data, then pull the contact back and see the values inline.

1. Create the contact

POST /api/v1/contacts/
At least one of phone or email is required. Send phone numbers in E.164 (+14155551234); display formats are normalized to E.164 before insert. Request
The new contact returns an empty custom_fields object until you set values.

2. Set custom-field values

PUT /api/v1/custom-fields/values
One call per field. key must match a custom-field definition you already created, and the value must match that definition’s type — values of the wrong type are rejected. Request

3. Read back — values inline

GET /api/v1/contacts/{id}
Request

Worked sequence: list with filters, then paginate

GET /api/v1/contacts/
Filters combine on the query string — tags=vip, country_code=US — search narrows by name, phone, or email. The response is cursor-paginated: while meta.pagination.has_more is true, pass meta.pagination.cursor back as the cursor query parameter to fetch the next page. Here the list is filtered to one tag. Request
has_more is true, so request the next page:
Treat the cursor as opaque. Keep the same filters across pages and stop when has_more comes back false.

Error envelope

Validation failures return 422 with a VALIDATION_ERROR code. The classic case is a phone value that fails the format and length checks — below the E.164 floor or not recognizable:
422
When you hit this on create or update, normalize the number to E.164 (country code, digits only, leading +) and retry. A missing-or-unknown custom-field key on PUT /custom-fields/values surfaces the same envelope with a four-hundred status, so handle both before you code a special case.

GDPR erasure — from request to confirmed status

Erasure is a tenant-owned control: you file it, Orbit executes it and keeps the audit chain. The generated reference section for DELETE /api/v1/contacts/{id}/gdpr/delete documents the synchronous cascade but stops at 204 No Content — nothing there tells you how to confirm the deletion you just ran, or how to read the pending/completed status on the cancellable flow. This section closes that gap.

The immediate delete answers 204; the status lives one call away

DELETE /api/v1/contacts/{id}/gdpr/delete
The immediate right-to-erasure returns 204 with an intentionally empty body. On success the contact is gone — any read of the same id returns 404. The probe pair: mint the re-auth challenge, delete, then confirm. Step 1 — mint the re-auth challenge
200
Step 2 — delete (204, empty body by design)
Without a valid challenge token in X-Reauth-Challenge, the delete returns 401 and erases nothing:
401 Error

The cancellable erasure request returns a full status envelope

POST /api/v1/contacts/{id}/gdpr/erasure-request
Unlike the immediate cascade, this flow starts a cooling-off window (per-tenant, seven days by default) and answers 202 with the full request — so you always know whether the deletion is still pending, executing, completed, or cancellable:
status moves pending → executing → executed once the scheduled worker hard-deletes the contact past cooling_off_ends_at (a failed run leaves failure_reason and retries up to its attempt cap; a cancelled request records cancelled_at and never executes). To cancel while it is still pending, pass the request id to POST /:id/gdpr/erasure-request/:requestId/cancel. Poll the status on the per-contact list endpoint — the object above under data.items:
200

Duplicate detection and merge review

Two endpoints handle duplicate detection: a cheap aggregate count for banner surfaces and pre-merge checks, and the full group list you fetch when you open the merge workflow.
GET /api/v1/contacts/duplicates/count
Returns extras — the number of contacts that would be merged away — plus hasMore indicating whether duplicates exist beyond the slice the count scanned. Use it for banner headlines and pre-merge checks.
string
exact (default) or fuzzy. exact is the right choice for banner counts; fuzzy mirrors the detection pass the merge endpoint uses.
integer
Per-strategy group cap, default 100, range 1–200. Raise it when hasMore returns true and you need to scan deeper before reporting “0 duplicates.”
Use GET /duplicates to fetch the full group list (with per-group contact records) when you actually render the merge review UI — fetching it just for a banner paints the contacts page with data you don’t read.