Leads
1 endpoint
/v1/leads/captureCapture a lead from an external funnel
Captures a new lead in Capturia from a third-party funnel, form, or tool (Make, Zapier, n8n, custom landing page). Once captured, the lead shows up immediately in My Prospects in the dashboard, and the SMS bot takes over within 30 seconds if consent and configuration are in order.
Contact already known (email or phone)
The contact is resolved by email first (case-insensitive),
then by phone, including secondary contact details already
attached to a contact. Capturing a lead whose email or phone
already exists reuses and enriches the existing contact — the
returned contact_id is that contact's, never a duplicate and
never an error. Contact details and core fields that are already filled
in (name, email, phone, pipeline, value, address...) are not
overwritten; qualification answers (custom_fields), on the other
hand, are refreshed with the latest values sent — a key that is
already present is replaced, a new key is added. A new
phone number is kept as a secondary number when the contact already
has one. If the email and the phone point to two different
contacts, the email takes priority and no contact detail is removed
from the other contact.
The response exposes contact_was_new to tell the two outcomes apart:
on an existing contact (false), pipeline and assignment_status
describe the resolved request, not necessarily the persisted state — the
contact keeps the pipeline, stage, and sales rep it already has, and
ignored_fields names the fields of the pipeline block it did
not take (existing_contact_kept).
Received fields not applied (ignored_fields)
A capture that responds 200 tells you what it did not apply. The response
carries ignored_fields, a [{ field, reason }] list that is always
present and empty when everything was applied:
unknown_field— a name the API does not know, at the root, incustomData, or in a block (pipeline.id,utm.utm_source);custom_field_not_defined— acustom_fieldskey with no definition of the same name in the account;reserved_tag— a tag with a reserved name (tags[1]);system_not_active— asystem_idthat is not active;existing_contact_kept— a field of thepipelineblock that the existing contact kept;sales_rep_not_found— a sales rep email that matches no active member of the account;submission_not_recorded— what only applies with a recorded submission, when none is recorded (existing_pipeline_contact,idempotent_replay);duplicate_field— a copy of a field received from several sources, set aside in favor of another value.
Only limit: on an existing contact, identity and address fields that
are already filled in, and ad attribution that is already set
(first-touch rule), are kept without being named one by one
(contact_was_new: false signals it).
The lead is captured in all of these cases: this is a setup warning
to read on the first test of an integration. The list is
capped at 50 entries (ignored_fields_truncated), with fields you mapped
by hand listed before the ones a connector adds automatically.
Pipeline and stage
The pipeline block designates the pipeline and the stage by their text
identifier (pipeline_slug, stage_slug), which you copy from the dashboard:
Administration, API Keys, "Your pipeline and stage identifiers"
section. A connector that only sends key/value pairs
(GoHighLevel workflow, Zapier) sends them flat:
pipeline.pipeline_slug, pipeline.stage_slug. Without
pipeline_slug, the stage is looked up in the default pipeline.
Contact already in an open pipeline
A lead whose contact (resolved by email or phone) is already in
an open pipeline stage (already handled by a sales rep — not
archived, stage neither won nor lost)
is not resubmitted: the response returns status: existing_pipeline_contact,
no new submission is recorded, and the SMS bot is not
triggered again, so an ongoing conversation is not overwritten. Newly
provided core fields still enrich the existing contact.
Retry deduplication takes precedence: a re-push received within the
5-minute window is treated as a retry (status: idempotent_replay, see
Idempotency) and keeps the original activity_id and
original_created_at — existing_pipeline_contact only applies after this
window.
Idempotency
The Idempotency-Key header is optional but recommended for client-side
retries. A second request with the same key and the same payload returns the
original response without duplicating the lead. The key is stored for 24h. A second
request with the same key but a different payload returns 409
idempotency_conflict. The underlying RPC also keeps its own
5-minute dedup window. This window is per contact and
independent of content: any submission for a contact already captured
within the last 5 minutes is treated as a retry
(idempotent_replay) even if the payload differs — the contact is still
enriched (including custom_fields, which are refreshed), but no
new activity, no tag, and no SMS bot follow-up. Updates to the
contact remain real: an automation that watches a
custom field reacts to the value change, as it does for any
change to the contact. This is also what prevents a double
bot follow-up when a lead resubmits their form twice in a row.
A different Idempotency-Key does not bypass this window: it
applies in the database after any cache miss. Two intentionally
distinct submissions from the same contact only produce two activities
if they are more than 5 minutes apart.
Custom error codes
For historical reasons, some rejections return codes outside the standard catalog:
- 422 —
invalid_phone,missing_consent,invalid_email,invalid_payload,pipeline_not_found,stage_not_found(instead ofvalidation_error/business_rule_violation). Simplified body shape specific to this route — see the 422 response below. - 429 —
rate_limited_ipwhen the IP limit (100 req/min) is reached (instead ofrate_limit_exceeded). TheX-RateLimit-Scope: ipheader identifies the scope of the triggered limit. Simplified body shape (no standardrequest_idordetails) — to be aligned in a later phase.
leads:captureRequest body
| Property | Type | Required | Description |
|---|---|---|---|
phone | string | integer | required | Phone (normalized to E.164). Expected final format:
^\+[1-9]\d{10,14}$ (between 11 and 15 digits after the +).
Also accepted: a positive JSON integer within the JavaScript safe range
(at most 9007199254740991; every E.164 number fits), converted to a
string before normalization, for no-code connectors whose source field
is numeric. Prefer the string: it is the only form that preserves an
explicit international +. Any other number (negative, decimal, zero)
is rejected with a 422 invalid_phone that restates the accepted formats. |
sms_consent | boolean | string | required | Explicit SMS consent. Must be true (boolean) or a
whitelisted truthy string (see the schema description).
Ambiguous values (false, "no", 0, etc.) are rejected with
422 missing_consent. Accepted at the root of the body or in
the customData object (GoHighLevel webhooks). |
first_name | string | optional | — |
last_name | string | optional | — |
email | string | optional | — |
source_label | string | optional | Free-form label identifying the lead's source (e.g. funnel-fb-jan,
landing-blueprint). Shown in the pipeline and in the contact's
activity log. |
utm | LeadCaptureUtm | optional | — |
gclid | string | optional | Google Ads click identifier (the gclid parameter of the landing
URL, auto-tagging). Kept with the lead's ad
attribution (first-touch). Without utm.source, the lead is
attributed to the google source and the cpc medium, which
links it to the campaign whose web origin is google / cpc.
When meta.ads_consent is false, the identifier is never
kept (neither in the attribution nor in the received copy): only the
google / cpc origin remains. |
notes | string | optional | Free-form notes attached to the lead (visible on the contact). |
tags | string[] | optional | Tags to attach to the created contact. If a tag does not exist for this
client, it is created on the fly. Any name that starts with
"Campagne :" (any case and spacing) is ignored: this prefix
is reserved for campaign tags, which Capturia sets on its own based on the
lead's campaign. The response names it by its position in
ignored_fields (tags[1], reason reserved_tag). |
qualification_answers | array | optional | List of qualification questions/answers captured upstream. |
pipeline | LeadCapturePipelineConfig | optional | — |
custom_fields | object | optional | Custom fields of the contact, keyed by the NAME of the definition
configured on the client side (e.g. type_propriete), not by its internal
UUID: a site's integrator does not know the ids. The capture resolves
name → definition per client, then fills the contact and the kanban.
A key with no matching definition is ignored (no error) and
named in the response's ignored_fields
(custom_fields.ma_cle, reason custom_field_not_defined). For a
select field, provide the option's value (e.g. maison_isolee), not its
label. |
address | string | optional | The contact's postal address (free text, e.g. the property for sale). |
postal_code | string | optional | The contact's postal code. |
address_place_id | string | optional | Place identifier (e.g. a Google Place ID) if the address comes from an autocomplete. Stored as is for later re-resolution. |
street | string | optional | Street (including the civic number). Completes an existing contact without overwriting a street that is already filled in. |
city | string | optional | City. Completes an existing contact without overwriting a city that is already filled in. |
sector | string | optional | Area or neighborhood. Completes an existing contact without overwriting an area that is already filled in. |
province | string | optional | Province. Completes an existing contact without overwriting a province that is already filled in. |
country | string | optional | Country. Completes an existing contact without overwriting a country that is already filled in. |
meta | LeadCaptureMetaAttribution | optional | — |
system_id | string | optional | Target System of the capture (UUID of one of the account's Systems, visible in
the Capturia dashboard). The captured lead is enrolled in this System.
Omitted → the main System (historical behavior). A UUID that matches
none of the account's Systems → system_not_found error (422),
and no contact is created. A System of the account that is not active (archived,
for example) → the lead goes to the main System, and the response names
system_id in ignored_fields (reason system_not_active). |
Responses
| Status | Description |
|---|---|
200 | Lead captured ( |
400 | The request is malformed (missing header, invalid JSON, invalid parameter). |
401 | Unknown API key or invalid format. |
403 | Key without the |
409 | A previous request with the same |
422 | Payload rejected by validation ( |
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/leads/capture' \
-H 'Authorization: Bearer cap_live_<your-key>' \
-H 'Content-Type: application/json' \
-d '{
"phone": "+15145551234",
"sms_consent": true,
"first_name": "Alex",
"last_name": "Tremblay",
"email": "[email protected]",
"source_label": "funnel-fb-jan",
"utm": {
"source": "facebook",
"medium": "cpc",
"campaign": "spring-2026"
},
"tags": [
"lead-chaud",
"vu-webinaire"
],
"pipeline": {
"pipeline_slug": "ventes-b2b",
"stage_slug": "nouveau-lead",
"deal_value": 2500,
"expected_close_date": "2026-06-30",
"assigned_sales_rep_email": "[email protected]"
},
"custom_fields": {
"type_propriete": "maison_isolee",
"delai_vente": "0_3_mois"
},
"address": "123 rue Principale, Montréal",
"postal_code": "H2X 1Y4"
}'