GoHighLevel
Send Capturia the leads of a GoHighLevel form, opportunity or appointment through a workflow's Webhook action, step by step.
Setup
Create a GHL workflow with a trigger (Form Submitted, Contact Created, Calendar Booked) and the native Webhook action.
Create a GHL workflow
In GoHighLevel, go to Automation > Workflows and click Create Workflow. Pick the trigger (Form Submitted, Contact Created, Calendar Booked — whichever should push the lead to Capturia).
Add the Webhook action
As action, pick Webhook. It's the native action that calls an external URL with a custom payload. Don't confuse it with Custom Webhook, a HighLevel premium action: the Webhook action is enough here.
Configure URL and method
URL: https://api.capturia.io/v1/leads/capture
Method: POST
Encoding: JSON (option at the top of the modal)
Custom Headers: add a row Authorization with value Bearer cap_live_xxxxxxxx.
Build the payload
In Custom Data, add key/value pairs (GHL serializes to JSON automatically when Encoding=JSON). Use GHL Custom Values ({{contact.phone}}, {{contact.first_name}}, etc.) for values.
Example:
phone→{{contact.phone}}first_name→{{contact.first_name}}last_name→{{contact.last_name}}email→{{contact.email}}sms_consent→true(fixed value — see step 5)source_label→ghl_workflowpipeline.pipeline_slug→ the identifier of the target pipeline, copied from Capturia (optional)pipeline.stage_slug→ the identifier of the target stage, copied from the same place (optional; withoutpipeline.pipeline_slug, the stage is looked up in your default pipeline)
Capture SMS consent
Recommended: create a GHL Custom Field of type "Checkbox" named SMS Consent, add it to the capture form, then add an If/Else condition in the workflow ("SMS Consent is checked"). In the checked branch only, place the Webhook action with sms_consent → true (fixed value).
If the client already has a different Custom Field (e.g. Marketing Opt-in), use it in the condition. Hardcoding sms_consent: true without a condition is only acceptable if the upstream form has documented explicit attestation.
Test and publish
First select a test contact carrying your own cell number and a test email, then click Test Webhook in the action modal: GHL sends the payload to Capturia with that contact's values. A 200 response = a real lead created in Capturia — there is no trial mode: the record shows up under Prospects and the SMS agent texts the submitted number if consent is in order. Archive the test record once verified, then save the workflow and toggle it to Publish.
Field mapping
The mapping below uses GHL Custom Values for dynamic values. Adapt according to the Custom Fields available on the client side.
| Capturia field | Required | Source field | Notes |
|---|---|---|---|
| phone | Yes | {{contact. | E.164 (+15145551234), NANP 10 or 11 digits, or formatted (514) 555-1234 / 514-555-1234. 10-digit numbers auto-prefixed with +1. |
| sms_consent | Yes | true (fixed value, behind the step-5 condition) | Boolean true OR string 'true' / 'yes' / 'oui' / 'vrai' / '1' / 'on' / 'checked' (case-insensitive). Accepted at the body root or inside the customData object (GoHighLevel webhooks). |
| first_name | No | {{contact. | Lead's first name |
| last_name | No | {{contact. | Last name |
| No | {{contact. | RFC 5322 valid email | |
| source_label | No | "ghl_workflow" | Free-form tag (e.g. 'webflow_form', 'fb_lead_ads') |
| utm. | No | your custom field, or a fixed value | Nested utm object, or the flat key utm.source |
| utm. | No | your custom field, or a fixed value | Nested utm object, or the flat key utm.medium |
| utm. | No | your custom field, or a fixed value | Nested utm object, or the flat key utm.campaign |
| tags | No | "tag1, tag2" (added to the contact tags GHL already sends) | Array of strings, or a comma-separated string |
| pipeline. | No | identifier copied from Capturia (fixed value) | Text identifier of the target pipeline (not its UUID). Copy it in Capturia: Administration, API Keys, "Your pipeline and stage identifiers" section. Without it, the default pipeline is targeted. Nested pipeline object, or flat key. |
| pipeline. | No | identifier copied from Capturia (fixed value) | Text identifier of the target stage, copied from the same place. Without it, the stage marked "Default stage" next to the identifiers. Sent without pipeline_slug, it is looked up in your default pipeline. Nested pipeline object, or flat key. |
SMS Consent — CASL / Quebec Law 25
The sms_consent field is required. Canada's Anti-Spam Law (CASL) and Quebec's Law 25 require explicit consent before any commercial SMS. Capturia stores a timestamped record of this attestation for your legal protection.
Recommended pattern — consent captured at source
Add a custom consent question directly in the source form (FB Lead Form custom question, Typeform field, GHL form checkbox). Map this field to sms_consent. Consent is explicit, dated, and tied to the lead who gave it.
Alternative pattern — checkbox on the landing page
If the source form doesn't allow custom questions, add a consent checkbox on the landing page before the ad. Keep a copy of the page (Wayback Machine, dated screenshot) as evidence.
Legal grey area
Hardcoding sms_consent: true without real lead attestation = legal risk. In case of complaint, Capturia may be required to provide proof of consent. Without proof, liability falls on the business that sends the lead.
Step-by-step test
First select a test contact carrying your own cell number and a test email, then click Test Webhook in the action modal: GHL sends the payload to Capturia with that contact's values. A 200 response = a real lead created in Capturia (no trial mode): the record shows up under Prospects and the SMS agent texts the submitted number if consent is in order. Never test with a real customer's record. Archive the test record, then save the workflow and toggle it to Publish.
Error catalog
| Code | Error | Cause | Fix |
|---|---|---|---|
| 400 | invalid_payload | Empty body or malformed JSON (extra comma, missing quote) | Validate the JSON you send, then send again |
| 401 | missing_api_key | Authorization header absent or malformed | Add the header Authorization: Bearer cap_live_... |
| 401 | invalid_api_key | Key revoked, expired, or mistyped | Check the key in Capturia, generate a new one if needed |
| 403 | terms_acceptance_required | API terms of use not accepted (or a new version to accept) | The account owner accepts the terms in Capturia, on the API Keys page |
| 403 | insufficient_scope | Key lacks the leads:capture scope | Create a new key with the "Lead capture" preset |
| 415 | invalid_content_type | Body sent as a form (Form, x-www-form-urlencoded) instead of JSON | Send as JSON (Payload Type = JSON, Encoding = JSON, Body content type = application/JSON) |
| 422 | invalid_phone | Phone missing or format not recognized | Use E.164 (+15145551234) or NANP 10 digits (5145551234) |
| 422 | invalid_email | Malformed email | Send a valid address, or leave the field out |
| 422 | missing_consent | sms_consent absent, false, or unrecognized value — the field is looked up at the body root AND inside the customData object (GHL) | Map the consent field to sms_consent (true / yes / oui). From GHL: fixed value true behind a condition on the consent checkbox |
| 422 | invalid_payload | Another field refused (text too long, malformed date, negative value, etc.) — details names the field | Fix the field named in details, then send again |
| 422 | pipeline_not_found | pipeline.pipeline_slug matches no pipeline of the account | Copy the pipeline's text identifier again from the API Keys page (it is not its UUID) |
| 422 | stage_not_found | pipeline.stage_slug matches no stage of the targeted pipeline | Copy the stage's text identifier again from the API Keys page, under the right pipeline |
| 429 | rate_limit_exceeded | The key's request quota is exceeded | Space out sends at the source (the Retry-After header gives the delay) |
| 429 | rate_limited_ip | More than 100 requests per minute from the same IP address | Reduce the rate or contact support |
FAQ — common pitfalls
| Symptom | Cause |
|---|---|
| Platform test green (200 OK) but lead doesn't show up in pipeline | The same contact was already sent in the last 5 minutes — response status: idempotent_replay, no duplicate (tests often reuse the same number). If the contact already existed, it keeps its stage: see the "not in the right pipeline" entry below. |
| 415 "Unsupported Content-Type" or 400 "Body is not valid JSON" | 415: the tool sends a form (Payload Type set to Form, form-encoded Encoding) instead of JSON. 400: the JSON is malformed, often an extra comma or a missing quote around a variable. |
| 200 OK response but no SMS sent to lead | sms_consent: true but AI agent not configured, or SMS AI agent disabled, or no Twilio number provisioned for the client. |
| 200 OK response but the lead is not in the right pipeline or stage | Read ignored_fields in the response: it names every received field that was not applied. unknown_field: the field carries a name the API does not know (pipeline.id instead of pipeline.pipeline_slug, for instance). existing_contact_kept: the contact already existed and keeps its pipeline and stage. duplicate_field: the same field arrived twice (root and Custom Data, for instance) and the other value was kept. |
| Lead created without UTM tracking | Field sent under a name the API does not know (utm_source). Send the nested utm object or the flat key utm.source; ignored_fields names the field that was not applied. |
| 403 terms_acceptance_required after an update that used to work | A new version of the API terms of use must be accepted: the account owner accepts it in Capturia, on the API Keys page, and the key works again. |
| 422 missing_consent even though sms_consent is in Custom Data | GHL wraps Custom Data in a customData object, which Capturia unwraps automatically (no configuration needed). If the error still occurs, check that the value sent is a fixed true, not a {{contact...}} checkbox interpolation. |
422 missing_consent with sms_consent → {{contact.my_checkbox}} | GHL interpolates the label of the checked option (e.g. "SMS Consent"), never true. Use an If/Else condition on the checkbox and send the fixed value true in the checked branch (step 5). |
The contact has a custom field named "SMS Consent" — does it conflict with the sms_consent in Custom Data? | No. The GHL webhook sends every custom field of the contact under its display name on its own, in addition to your Custom Data pairs. Capturia gives your Custom Data sms_consent precedence over those automatic fields — exactly the setup prescribed in step 5, no field renaming required. A standard contact field (email, first name, phone) that you remap in Custom Data keeps the contact's value; if the two differ, the response names your copy in ignored_fields (duplicate_field). |
| My sub-account is linked through the connector and I also send this webhook: the lead does not change stage | With the connector, the contact often already exists in Capturia when the webhook arrives. An existing contact keeps its pipeline and stage: the webhook's pipeline block is not applied, and the response names it in ignored_fields (existing_contact_kept). Placement then follows your connector's pipeline matches. |