Contacts
10 endpoints
/v1/contactsList contacts
Returns the paginated list of the account's contacts (cursor v2).
Stable sort created_at DESC, id DESC. Supports case-insensitive
search on email, first_name, last_name, company, filtering
by tag, and optional inclusion of the custom field definitions in
meta.
Merged contacts
A contact absorbed by a merge no longer appears in the list: its history lives on the surviving contact, which is the only one listed.
Search
The ?search parameter is split into words (separated by spaces).
Each word must appear, case-insensitively, in at least one of the
email, first_name, last_name, or company columns, and the
words are combined with AND, in any order:
?search=Jean Bouchard finds the contact whose first name is "Jean"
and whose last name is "Bouchard". Each word is sent as a quoted
literal value: commas, parentheses, and periods don't alter the
filter.
Including custom fields
Pass ?include=custom_field_definitions to add the
meta.custom_field_definitions field to the response — useful for
rendering an edit form on your side without a second round-trip.
contacts:readQuery parameters
| Name | Type | Required | Description |
|---|---|---|---|
search | string | optional | Word search across email, first_name, last_name, company (case-insensitive; each word must match a column, words combined with AND). |
tag | string | optional | UUID of a tag: returns only the contacts that carry this tag,
paginated like the full list. To
count a campaign's leads, pass the id of its
"Campagne : <name>" tag (system_key starting with campaign:
in GET /v1/tags) and walk the pages (next_cursor)
until has_more: false: pagination.total_count is only an
estimate. A value that isn't a UUID is
rejected (400 invalid_request, details.field = "tag"). |
include | string | optional | Includes the meta.custom_field_definitions block in the response. |
Responses
| Status | Description |
|---|---|
200 | Paginated list of contacts. |
400 | The request is malformed (missing header, invalid JSON, invalid parameter). |
401 | Unknown API key or invalid format. |
403 | Authorization denied. Two causes, distinguished by |
429 | The per-window request quota has been exceeded for this API key or endpoint. |
500 | Unexpected server error. Always include the |
Example request
curl -X GET 'https://api.capturia.io/v1/contacts' \
-H 'Authorization: Bearer cap_live_<your-key>' \
-H 'Content-Type: application/json'/v1/contactsCreate a contact
Creates a contact under the API key's account, through the same path as the dashboard. An email or a phone number is required (either one is enough). The email is stored in lowercase; the phone number is normalized to E.164 when it's a valid North American number.
The person is looked up across all of the account's identities
(primary and secondary emails and phone numbers, case-insensitive).
If they already exist, nothing is created: the response is
409 duplicate_contact, with the existing contact in data and the
matching fields in details.conflicting_fields. If the email and
the phone number belong to two different contacts, the response is
409 duplicate_resource, with no contact.
Source and consent
The created contact has the source api. An integration doesn't name
anyone who actually met this contact: no implied
consent (SMS or email) is recorded when it's created. Capturia's SMS
follow-ups and prospecting emails are only sent to it
once a real basis for consent exists
(form, booking, inbound message, attestation in
Capturia). To capture a lead who just filled out your form,
with their consent, use POST /v1/leads/capture.
Idempotency
The Idempotency-Key header is optional but recommended.
A second request with the same key and the same payload returns the
original response without repeating the operation. A second request with
the same key and a different payload returns 409 idempotency_conflict.
Custom fields
The keys of custom_fields are the field's name as defined
in Capturia (or, equivalently, the id of its definition) —
the same contract as /v1/events and /v1/leads/capture. The values
are validated against the account's definitions
(client_custom_field_definitions). An unknown field or an
incompatible type returns 400 invalid_custom_fields with details
listing the violations.
contacts:writeRequest body
| Property | Type | Required | Description |
|---|---|---|---|
email | string | optional | Email of the contact, stored in lowercase. An uppercase letter never creates a second contact. |
first_name | string | optional | — |
last_name | string | optional | — |
phone | string | optional | Phone number of the contact (at most 50 characters): a reachable number
that can be normalized to E.164 (e.g. +15145551234). A number that's too
short, incomplete, or followed by an extension is rejected (400,
validation_error, reason invalid_phone on the phone field),
just like when updating a contact. |
company | string | optional | — |
deal_value | number | optional | Estimated value of the opportunity in CAD. |
custom_fields | object | null | optional | Custom fields, keyed by the field's name (or the id of its
definition). Validated against client_custom_field_definitions on
write — an unknown field, an incompatible type, or a value
that's too long returns 400 invalid_custom_fields, under the key
you sent. Maximum lengths: text 1,000 characters, long text
4,000, URL 2,048 (http(s)://), email 320, phone 30; a
list only accepts one of its options. On read, the keys
returned are the definition ids (storage form). |
system_id | string | optional | Target System of the created contact (UUID of one of the account's Systems). The contact
is engaged in this System. Omitted → main System (historical
behavior). UUID unknown for this account → system_not_found error
(422), no contact created; archived System → silent fallback to the
main System. |
Responses
| Status | Description |
|---|---|
201 | Contact created. |
400 | Parameter or body validation failed. |
401 | Unknown API key or invalid format. |
403 | Authorization denied. Two causes, distinguished by |
409 | A resource with a conflicting unique identifier already exists. |
415 | The |
429 | The per-window request quota has been exceeded for this API key or endpoint. |
500 | Unexpected server error. Always include the |
Example request
curl -X POST 'https://api.capturia.io/v1/contacts' \
-H 'Authorization: Bearer cap_live_<your-key>' \
-H 'Content-Type: application/json' \
-d '{
"email": "[email protected]",
"first_name": "Alex",
"last_name": "Tremblay",
"phone": "+15145551234",
"company": "Acme Inc.",
"deal_value": 2500
}'/v1/contacts/{id}Retrieve a contact
Returns a contact by its ID. The ?expand=tags parameter joins the
client_contact_tags table to include the attached tags (id, name,
color, system_key) without an extra round-trip.
The ID of a contact absorbed by a merge returns 404: reads, updates,
and deletes go through the surviving contact. If the merge is undone
from the dashboard, the contact becomes reachable again under its ID.
contacts:readQuery parameters
| Name | Type | Required | Description |
|---|---|---|---|
expand | string | optional | If tags, adds client_contact_tags to the returned contact. |
Responses
| Status | Description |
|---|---|
200 | Contact found. |
401 | Unknown API key or invalid format. |
403 | Authorization denied. Two causes, distinguished by |
404 | The requested resource doesn't exist (or isn't within the tenant's scope). |
429 | The per-window request quota has been exceeded for this API key or endpoint. |
500 | Unexpected server error. Always include the |
Example request
curl -X GET 'https://api.capturia.io/v1/contacts/{id}' \
-H 'Authorization: Bearer cap_live_<your-key>' \
-H 'Content-Type: application/json'/v1/contacts/{id}Update a contact
Updates the provided fields of the contact (omitted fields are kept).
You must provide at least one field, otherwise 400 no_fields.
The custom_fields field is merged with the existing values
(keys you don't provide are kept). The keys are the field's name
(or the id of its definition); an unknown key returns
400 invalid_custom_fields. Passing null clears all custom
fields.
Phone number and email are unique per account: a value already
held by another contact (as a primary or secondary
contact detail) returns 409 duplicate_resource
(details.conflicting_field names the field) and the contact isn't
modified. The phone number is compared once normalized to E.164.
The update goes through the same service as the contact in the
dashboard: same validations (email, phone number, positive amounts,
each custom field's value according to its type), same entry in
the account's timeline and activity log, and the same
lead_updated webhook. An invalid value returns
400 validation_error (details[].field names the field); a
phone number that can't be dialed (that doesn't normalize to
E.164) is rejected with details[].reason = invalid_phone.
A contact always keeps at least a first name, a last name, an email, or
a phone number: a request that would clear the last one returns
400 validation_error (details[].reason = identity_required), and
the contact isn't modified.
The value (deal_value) of an archived contact can't be changed:
422 business_rule_violation (details.reason =
contact_archived), and no field from the request is written.
Restore the contact from the dashboard first.
The notes field is the contact's free-form text, replaced on every
write. To add a note to the contact's timeline, use
POST /v1/contacts/{id}/notes.
contacts:writeRequest body
| Property | Type | Required | Description |
|---|---|---|---|
email | string | optional | — |
first_name | string | null | optional | — |
last_name | string | null | optional | — |
phone | string | null | optional | E.164 phone number (e.g. +15145551234). |
company | string | null | optional | — |
deal_value | number | null | optional | — |
is_priority | boolean | optional | Flagged as a priority for faster human follow-up. |
notes | string | null | optional | Free-form internal notes about the contact. |
custom_fields | object | null | optional | Additive patch on the existing custom fields (keys you don't
provide are kept). Passing null clears all fields. |
Responses
| Status | Description |
|---|---|
200 | Contact updated. |
400 | Parameter or body validation failed. |
401 | Unknown API key or invalid format. |
403 | Authorization denied. Two causes, distinguished by |
404 | The requested resource doesn't exist (or isn't within the tenant's scope). |
409 | A resource with a conflicting unique identifier already exists. |
422 | The request is syntactically valid but violates a business rule. |
429 | The per-window request quota has been exceeded for this API key or endpoint. |
500 | Unexpected server error. Always include the |
Example request
curl -X PATCH 'https://api.capturia.io/v1/contacts/{id}' \
-H 'Authorization: Bearer cap_live_<your-key>' \
-H 'Content-Type: application/json' \
-d '{
"first_name": "Alexandre",
"deal_value": 5000,
"is_priority": true
}'/v1/contacts/{id}Delete a contact
Permanently deletes the contact, exactly as from the dashboard: its inbox conversation, its exchanges (SMS, emails, chat transcripts), its bookings, and its files go with it. Its voicemails and media received by SMS go too, as do the audio of its calls and meetings and the word-for-word transcripts of its calls, including at our telephony provider: this removal is durable and retried automatically if the provider is temporarily unreachable. Its call log (summary, notes, outcome) remains, detached from the contact, as do sales, orders, and payments.
Removal outside the platform (bookings in the connected Google calendar,
stored files) happens right after, on a best-effort basis: the
contact's deletion from the database succeeds even if an external service is
temporarily unreachable. The response's cleanup object reports it —
failed > 0 means some Google bookings couldn't be
removed from the calendar (account disconnected, outage) and remain there.
Requires the contacts:delete scope, separate from writing: as in
the dashboard, deleting a contact is a separate permission.
Only someone who can delete leads in Capturia
can grant it to an API key.
contacts:deleteResponses
| Status | Description |
|---|---|
200 | Contact deleted. |
401 | Unknown API key or invalid format. |
403 | Authorization denied. Two causes, distinguished by |
404 | The requested resource doesn't exist (or isn't within the tenant's scope). |
429 | The per-window request quota has been exceeded for this API key or endpoint. |
500 | Unexpected server error. Always include the |
Example request
curl -X DELETE 'https://api.capturia.io/v1/contacts/{id}' \
-H 'Authorization: Bearer cap_live_<your-key>' \
-H 'Content-Type: application/json'/v1/contacts/{id}/timelineContact timeline (chronological activity)
Returns the contact's activity timeline (stage changes, SMS/email sends, bookings, etc.) in reverse chronological order.
Current limitation
The implementation is a stub: the pipeline_history table references
the other, internal contacts table (not client_contacts), so
the returned list is always empty today. Pagination
is validated but not applied. To be implemented in a later
phase once the history is unified on client_contacts.
contacts:readResponses
| Status | Description |
|---|---|
200 | Empty list (stub implementation) with valid pagination. |
400 | The request is malformed (missing header, invalid JSON, invalid parameter). |
401 | Unknown API key or invalid format. |
403 | Authorization denied. Two causes, distinguished by |
404 | The requested resource doesn't exist (or isn't within the tenant's scope). |
429 | The per-window request quota has been exceeded for this API key or endpoint. |
500 | Unexpected server error. Always include the |
Example request
curl -X GET 'https://api.capturia.io/v1/contacts/{id}/timeline' \
-H 'Authorization: Bearer cap_live_<your-key>' \
-H 'Content-Type: application/json'/v1/contacts/{id}/tagsAttach one or more tags to a contact
Attaches one or more tags to the contact through the
client_contact_tags join table. The tags must belong to the same account
as the contact — otherwise 400 validation_error with the list of
tags not found. An identifier that isn't a UUID also returns
400 validation_error.
The operation is idempotent at the database level: calling it twice with the
same tag_ids doesn't create duplicates (upsert
ON CONFLICT (contact_id, tag_id) DO NOTHING).
Campaign tags
A tag whose system_key starts with campaign: is managed by
Capturia: it follows the lead's campaign. If the contact already carries it, it
is ignored (sending back a lead's list of tags stays idempotent).
If the contact doesn't carry it, it returns 409 tag_managed_by_capturia and
none of the request's tags are attached.
Listing a contact's tags
There's no dedicated GET — use
GET /v1/contacts/{id}?expand=tags to retrieve the tags
currently attached.
contacts:writeRequest body
| Property | Type | Required | Description |
|---|---|---|---|
tag_ids | string[] | required | UUIDs of the tags to attach (all must belong to the same account, at most 100 per request). |
Responses
| Status | Description |
|---|---|
200 | Tags attached. Returns the contact's up-to-date list of tags. |
400 | Parameter or body validation failed. |
401 | Unknown API key or invalid format. |
403 | Authorization denied. Two causes, distinguished by |
404 | The requested resource doesn't exist (or isn't within the tenant's scope). |
409 | The targeted tag is a campaign tag managed by Capturia (a |
429 | The per-window request quota has been exceeded for this API key or endpoint. |
500 | Unexpected server error. Always include the |
Example request
curl -X POST 'https://api.capturia.io/v1/contacts/{id}/tags' \
-H 'Authorization: Bearer cap_live_<your-key>' \
-H 'Content-Type: application/json' \
-d '{
"tag_ids": [
"8f3b1a2c-9d4e-4f5a-b6c7-d8e9f0a1b2c3",
"9a4c2b3d-0e5f-5a6b-c7d8-e9f0a1b2c3d4"
]
}'/v1/contacts/{id}/tags/{tagId}Detach a tag from a contact
Removes the association between a contact and a tag. The tag itself isn't deleted — it remains available for other contacts.
If the tag isn't attached to the contact (or if the contact belongs
to another account), returns 404 resource_not_found.
A campaign tag (system_key starting with campaign:) follows
the lead's campaign: detaching it returns
409 tag_managed_by_capturia.
contacts:writeResponses
| Status | Description |
|---|---|
200 | Tag detached. |
400 | Parameter or body validation failed. |
401 | Unknown API key or invalid format. |
403 | Authorization denied. Two causes, distinguished by |
404 | The requested resource doesn't exist (or isn't within the tenant's scope). |
409 | The targeted tag is a campaign tag managed by Capturia (a |
429 | The per-window request quota has been exceeded for this API key or endpoint. |
500 | Unexpected server error. Always include the |
Example request
curl -X DELETE 'https://api.capturia.io/v1/contacts/{id}/tags/{tagId}' \
-H 'Authorization: Bearer cap_live_<your-key>' \
-H 'Content-Type: application/json'/v1/contacts/{id}/notesAdd a note to a contact's timeline
Adds a note to the contact's timeline, exactly as from the contact in the dashboard: it's added to the previous notes, appears in the contact's Notes tab, and feeds the lead's qualification. The note is signed by the API key that wrote it.
Not to be confused with the contact's free-form text field notes
(PATCH /v1/contacts/{id}), replaced on every write.
The content is required and is at most 20,000 characters; otherwise
400 validation_error. A contact that's unknown, belongs to another account, or
was merged into another contact returns 404.
Idempotency
The Idempotency-Key header is optional but recommended: a
second request with the same key, the same contact, and the same content
returns the original note without writing a second one. The same key
with another contact or other content returns
409 idempotency_conflict.
contacts:writeRequest body
| Property | Type | Required | Description |
|---|---|---|---|
content | string | required | Text of the note. |
Responses
| Status | Description |
|---|---|
201 | Note added to the contact's timeline. |
400 | Parameter or body validation failed. |
401 | Unknown API key or invalid format. |
403 | Authorization denied. Two causes, distinguished by |
404 | The requested resource doesn't exist (or isn't within the tenant's scope). |
409 | A previous request with the same |
429 | The per-window request quota has been exceeded for this API key or endpoint. |
500 | Unexpected server error. Always include the |
Example request
curl -X POST 'https://api.capturia.io/v1/contacts/{id}/notes' \
-H 'Authorization: Bearer cap_live_<your-key>' \
-H 'Content-Type: application/json' \
-d '{
"content": "Appel de suivi : le client veut une soumission pour 3 succursales."
}'/v1/contacts/batchCreate or update multiple contacts in one request
Atomic upsert of up to 100 contacts in a single request. Contacts whose email already exists under the account are updated (the other fields you provide replace the existing values), the rest are inserted.
Internal deduplication
The payload is deduplicated internally by lower(email) (last entry
wins) before the upsert. The order of the contacts in the array and the
case of the email don't affect the idempotency hash.
Idempotency
The Idempotency-Key header is optional but recommended. The
idempotency hash covers the payload after internal deduplication —
a network retry stays idempotent even if you resend the contacts in
a different order or with different email casing.
Response
The endpoint returns 200 with {data: {processed, contacts: []}}
— no per-contact multi-status. A validation error on a
single contact rejects the whole batch (400 validation_error
with details listing each faulty row). A database error
during the upsert returns 500 — contacts already persisted
before the error remain (no transactional rollback
across statements).
When system_id is provided and resolves to an active System, the
response also includes system_routing: the summary of routing the
created contacts to that System. Routing is best-effort
(the upsert is already committed when it runs) — refused + errors =
created contacts that stayed in the main System.
contacts:writeRequest body
| Property | Type | Required | Description |
|---|---|---|---|
contacts | array | required | List of contacts to upsert (max 100 per request, otherwise 400 batch_too_large). |
system_id | string | optional | Target System of the batch (UUID of one of the account's Systems). The
contacts created by this batch are engaged in this
System; updated contacts keep their engagement.
Omitted → main System (historical behavior). UUID
unknown for this account → system_not_found (422), no
contact written; archived System → silent fallback to the
main System. |
trigger_automations | boolean | optional | Triggers the account's active automations and sequences
for the contacts in this batch (contact created, tags
added). Default false: nothing is triggered —
no automation or sequence enrollment, no
email or SMS caused by this import. When true, the response
returns automations_enrolled and, if the engine's hourly
cap (200 enrollments/account/hour) is reached,
automations_skipped_rate_limit — never a silent
drop. |
Responses
| Status | Description |
|---|---|
200 | Batch processed. |
400 | Parameter or body validation failed. |
401 | Unknown API key or invalid format. |
403 | Authorization denied. Two causes, distinguished by |
409 | A previous request with the same |
415 | The |
429 | The per-window request quota has been exceeded for this API key or endpoint. |
500 | Unexpected server error. Always include the |
Example request
curl -X POST 'https://api.capturia.io/v1/contacts/batch' \
-H 'Authorization: Bearer cap_live_<your-key>' \
-H 'Content-Type: application/json' \
-d '{
"contacts": [
{
"email": "[email protected]",
"first_name": "Alex",
"last_name": "Tremblay",
"phone": "+15145551234",
"company": "Acme Inc.",
"deal_value": 2500
},
{
"email": "[email protected]",
"first_name": "Morgan",
"last_name": "Bélanger",
"company": "Example Inc."
}
]
}'