Capturia measures its ads and site traffic with the Meta pixel and Google Analytics. You can decline at any time. Learn more

FR

Contacts

10 endpoints

get/v1/contacts

List 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.

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.

Required scopes: contacts:read

Query parameters

NameTypeRequiredDescription
searchstringoptionalWord search across email, first_name, last_name, company (case-insensitive; each word must match a column, words combined with AND).
tagstringoptionalUUID 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").
includestringoptionalIncludes the meta.custom_field_definitions block in the response.

Responses

StatusDescription
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 code: insufficient_scope (the key lacks the required scope — details.required_scope) or feature_not_available (the public API or the targeted module isn't included in the account's plan — details.feature names the feature, e.g. public_api, quotes). A plan denial is final for the key: don't retry; the account must change plans.

429

The per-window request quota has been exceeded for this API key or endpoint.

500

Unexpected server error. Always include the request_id when contacting support.

Example request

curl -X GET 'https://api.capturia.io/v1/contacts' \
  -H 'Authorization: Bearer cap_live_<your-key>' \
  -H 'Content-Type: application/json'
post/v1/contacts

Create 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.

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.

Required scopes: contacts:write

Request body

PropertyTypeRequiredDescription
emailstringoptionalEmail of the contact, stored in lowercase. An uppercase letter never creates a second contact.
first_namestringoptional—
last_namestringoptional—
phonestringoptionalPhone 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.
companystringoptional—
deal_valuenumberoptionalEstimated value of the opportunity in CAD.
custom_fieldsobject | nulloptionalCustom 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_idstringoptionalTarget 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

StatusDescription
201

Contact created.

400

Parameter or body validation failed.

401

Unknown API key or invalid format.

403

Authorization denied. Two causes, distinguished by code: insufficient_scope (the key lacks the required scope — details.required_scope) or feature_not_available (the public API or the targeted module isn't included in the account's plan — details.feature names the feature, e.g. public_api, quotes). A plan denial is final for the key: don't retry; the account must change plans.

409

A resource with a conflicting unique identifier already exists.

415

The Content-Type header is missing or not supported for this endpoint.

429

The per-window request quota has been exceeded for this API key or endpoint.

500

Unexpected server error. Always include the request_id when contacting support.

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
}'
get/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.

Required scopes: contacts:read

Query parameters

NameTypeRequiredDescription
expandstringoptionalIf tags, adds client_contact_tags to the returned contact.

Responses

StatusDescription
200

Contact found.

401

Unknown API key or invalid format.

403

Authorization denied. Two causes, distinguished by code: insufficient_scope (the key lacks the required scope — details.required_scope) or feature_not_available (the public API or the targeted module isn't included in the account's plan — details.feature names the feature, e.g. public_api, quotes). A plan denial is final for the key: don't retry; the account must change plans.

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 request_id when contacting support.

Example request

curl -X GET 'https://api.capturia.io/v1/contacts/{id}' \
  -H 'Authorization: Bearer cap_live_<your-key>' \
  -H 'Content-Type: application/json'
patch/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.

Required scopes: contacts:write

Request body

PropertyTypeRequiredDescription
emailstringoptional—
first_namestring | nulloptional—
last_namestring | nulloptional—
phonestring | nulloptionalE.164 phone number (e.g. +15145551234).
companystring | nulloptional—
deal_valuenumber | nulloptional—
is_prioritybooleanoptionalFlagged as a priority for faster human follow-up.
notesstring | nulloptionalFree-form internal notes about the contact.
custom_fieldsobject | nulloptionalAdditive patch on the existing custom fields (keys you don't provide are kept). Passing null clears all fields.

Responses

StatusDescription
200

Contact updated.

400

Parameter or body validation failed.

401

Unknown API key or invalid format.

403

Authorization denied. Two causes, distinguished by code: insufficient_scope (the key lacks the required scope — details.required_scope) or feature_not_available (the public API or the targeted module isn't included in the account's plan — details.feature names the feature, e.g. public_api, quotes). A plan denial is final for the key: don't retry; the account must change plans.

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 request_id when contacting support.

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
}'
delete/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.

Required scopes: contacts:delete

Responses

StatusDescription
200

Contact deleted.

401

Unknown API key or invalid format.

403

Authorization denied. Two causes, distinguished by code: insufficient_scope (the key lacks the required scope — details.required_scope) or feature_not_available (the public API or the targeted module isn't included in the account's plan — details.feature names the feature, e.g. public_api, quotes). A plan denial is final for the key: don't retry; the account must change plans.

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 request_id when contacting support.

Example request

curl -X DELETE 'https://api.capturia.io/v1/contacts/{id}' \
  -H 'Authorization: Bearer cap_live_<your-key>' \
  -H 'Content-Type: application/json'
get/v1/contacts/{id}/timeline

Contact 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.

Required scopes: contacts:read

Responses

StatusDescription
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 code: insufficient_scope (the key lacks the required scope — details.required_scope) or feature_not_available (the public API or the targeted module isn't included in the account's plan — details.feature names the feature, e.g. public_api, quotes). A plan denial is final for the key: don't retry; the account must change plans.

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 request_id when contacting support.

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'
post/v1/contacts/{id}/tags

Attach 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.

Required scopes: contacts:write

Request body

PropertyTypeRequiredDescription
tag_idsstring[]requiredUUIDs of the tags to attach (all must belong to the same account, at most 100 per request).

Responses

StatusDescription
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 code: insufficient_scope (the key lacks the required scope — details.required_scope) or feature_not_available (the public API or the targeted module isn't included in the account's plan — details.feature names the feature, e.g. public_api, quotes). A plan denial is final for the key: don't retry; the account must change plans.

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 system_key that starts with campaign:): it follows the lead's campaign and can't be attached or detached manually.

429

The per-window request quota has been exceeded for this API key or endpoint.

500

Unexpected server error. Always include the request_id when contacting support.

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"
  ]
}'
delete/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.

Required scopes: contacts:write

Responses

StatusDescription
200

Tag detached.

400

Parameter or body validation failed.

401

Unknown API key or invalid format.

403

Authorization denied. Two causes, distinguished by code: insufficient_scope (the key lacks the required scope — details.required_scope) or feature_not_available (the public API or the targeted module isn't included in the account's plan — details.feature names the feature, e.g. public_api, quotes). A plan denial is final for the key: don't retry; the account must change plans.

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 system_key that starts with campaign:): it follows the lead's campaign and can't be attached or detached manually.

429

The per-window request quota has been exceeded for this API key or endpoint.

500

Unexpected server error. Always include the request_id when contacting support.

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'
post/v1/contacts/{id}/notes

Add 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.

Required scopes: contacts:write

Request body

PropertyTypeRequiredDescription
contentstringrequiredText of the note.

Responses

StatusDescription
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 code: insufficient_scope (the key lacks the required scope — details.required_scope) or feature_not_available (the public API or the targeted module isn't included in the account's plan — details.feature names the feature, e.g. public_api, quotes). A plan denial is final for the key: don't retry; the account must change plans.

404

The requested resource doesn't exist (or isn't within the tenant's scope).

409

A previous request with the same Idempotency-Key used a different payload, or is still being processed.

429

The per-window request quota has been exceeded for this API key or endpoint.

500

Unexpected server error. Always include the request_id when contacting support.

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."
}'
post/v1/contacts/batch

Create 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.

Required scopes: contacts:write

Request body

PropertyTypeRequiredDescription
contactsarrayrequiredList of contacts to upsert (max 100 per request, otherwise 400 batch_too_large).
system_idstringoptionalTarget 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_automationsbooleanoptionalTriggers 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

StatusDescription
200

Batch processed. processed = number of contacts successfully inserted or updated.

400

Parameter or body validation failed.

401

Unknown API key or invalid format.

403

Authorization denied. Two causes, distinguished by code: insufficient_scope (the key lacks the required scope — details.required_scope) or feature_not_available (the public API or the targeted module isn't included in the account's plan — details.feature names the feature, e.g. public_api, quotes). A plan denial is final for the key: don't retry; the account must change plans.

409

A previous request with the same Idempotency-Key used a different payload, or is still being processed.

415

The Content-Type header is missing or not supported for this endpoint.

429

The per-window request quota has been exceeded for this API key or endpoint.

500

Unexpected server error. Always include the request_id when contacting support.

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."
    }
  ]
}'