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

FR

Leads

1 endpoint

post/v1/leads/capture

Capture 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, in customData, or in a block (pipeline.id, utm.utm_source);
  • custom_field_not_defined — a custom_fields key with no definition of the same name in the account;
  • reserved_tag — a tag with a reserved name (tags[1]);
  • system_not_active — a system_id that is not active;
  • existing_contact_kept — a field of the pipeline block 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 of validation_error / business_rule_violation). Simplified body shape specific to this route — see the 422 response below.
  • 429 — rate_limited_ip when the IP limit (100 req/min) is reached (instead of rate_limit_exceeded). The X-RateLimit-Scope: ip header identifies the scope of the triggered limit. Simplified body shape (no standard request_id or details) — to be aligned in a later phase.
Required scopes: leads:capture

Request body

PropertyTypeRequiredDescription
phonestring | integerrequiredPhone (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_consentboolean | stringrequiredExplicit 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_namestringoptional—
last_namestringoptional—
emailstringoptional—
source_labelstringoptionalFree-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.
utmLeadCaptureUtmoptional—
gclidstringoptionalGoogle 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.
notesstringoptionalFree-form notes attached to the lead (visible on the contact).
tagsstring[]optionalTags 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_answersarrayoptionalList of qualification questions/answers captured upstream.
pipelineLeadCapturePipelineConfigoptional—
custom_fieldsobjectoptionalCustom 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.
addressstringoptionalThe contact's postal address (free text, e.g. the property for sale).
postal_codestringoptionalThe contact's postal code.
address_place_idstringoptionalPlace identifier (e.g. a Google Place ID) if the address comes from an autocomplete. Stored as is for later re-resolution.
streetstringoptionalStreet (including the civic number). Completes an existing contact without overwriting a street that is already filled in.
citystringoptionalCity. Completes an existing contact without overwriting a city that is already filled in.
sectorstringoptionalArea or neighborhood. Completes an existing contact without overwriting an area that is already filled in.
provincestringoptionalProvince. Completes an existing contact without overwriting a province that is already filled in.
countrystringoptionalCountry. Completes an existing contact without overwriting a country that is already filled in.
metaLeadCaptureMetaAttributionoptional—
system_idstringoptionalTarget 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

StatusDescription
200

Lead captured (status: created), idempotent replay of a previous capture (status: idempotent_replay), or contact already handled (status: existing_pipeline_contact — already in an open pipeline stage: no new submission is recorded, the SMS bot is not triggered, and activity_id is null). In all cases, the returned contact_id is stable.

400

The request is malformed (missing header, invalid JSON, invalid parameter).

401

Unknown API key or invalid format.

403

Key without the leads:capture scope (insufficient_scope), or an organization that has not yet accepted the API terms of use (terms_acceptance_required). Standard error envelope.

409

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

422

Payload rejected by validation (invalid_phone, missing_consent, invalid_email, invalid_payload) or by pipeline resolution (pipeline_not_found, stage_not_found). Simplified shape specific to this route: no type or doc_url, and details is an array [{ field, reason }] (field is empty when the body is not an object). Only system_not_found (a system_id that matches no System in the account) comes back in the standard error envelope. For an invalid scalar field, the message echoes back the received value (capped at 120 characters) — it is never stored by Capturia; only the names of the received fields are, visible in the API key's Recent errors tab. The request_id is always in the X-Request-ID header.

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