GoHighLevel
Capture leads from GHL forms, opportunities, and calendar events via a workflow webhook — step-by-step with screenshots.
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 with Make HTTP Request (more restricted action).

Configure URL and method
URL: https://app.capturia.io/api/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_workflow

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 Leads 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.phone}} | 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.first_name}} | Lead's first name |
| last_name | No | {{contact.last_name}} | Last name |
| No | {{contact.email}} | RFC 5322 valid email | |
| source_label | No | "ghl_workflow" | Free-form tag (e.g. 'webflow_form', 'fb_lead_ads') |
| utm.source | No | (not natively supported) | Nested utm object (not flat) |
| utm.medium | No | (not natively supported) | Nested utm object |
| utm.campaign | No | (not natively supported) | Nested utm object |
| tags | No | "tag1, tag2" or [] | Array of strings |
| pipeline.id | No | (optional) | Target pipeline UUID |
| pipeline.stage_id | No | (optional) | Target stage UUID |
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 SMB owner.
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 Leads 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 |
|---|---|---|---|
| 401 | missing_api_key | Authorization header absent or malformed | Add the header Authorization: Bearer cap_live_... |
| 401 | invalid_api_key | Key revoked, expired, or typo | Check the key in the dashboard, generate a new one if needed |
| 403 | terms_acceptance_required | ToS not accepted (or version bump forced re-consent) | The SMB owner must re-accept ToS in the dashboard |
| 403 | insufficient_scope | Key lacks the leads:capture scope | Create a new key with the 'Lead capture' preset |
| 422 | invalid_phone | Phone format not recognized | Use E.164 (+15145551234) or NANP 10 digits (5145551234) |
| 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 | Other field malformed (email, utm object vs flat, etc.) | Check the body format — see the leads:capture API doc |
| 429 | rate_limited_key | Per-key quota exceeded (often 100 req/min) | Throttle at source, or request a tier upgrade from support |
| 429 | rate_limited_ip | Per-IP quota exceeded | Reduce the rate or contact support |
| 400 | invalid_payload | Body is not valid JSON (Form or form-urlencoded sent instead) | Configure Payload Type = JSON / Content-Type = application/json |
FAQ — common pitfalls
| Symptom | Cause |
|---|---|
| Platform test green (200 OK) but lead doesn't show up in pipeline | Phone matches an existing contact — response status: idempotent_replay. The lead exists but isn't duplicated. Test sandboxes often reuse the same number. |
| 400 "Body is not valid JSON" | Payload Type set to Form instead of JSON, or body sent as x-www-form-urlencoded instead of application/json. |
| 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. |
| Lead created without UTM tracking | utm field sent as flat (utm_source) instead of nested object (utm.source). Server expects an object. |
| 403 terms_acceptance_required after an update that used to work | ToS version bump on Capturia side forces the SMB owner to re-accept in the dashboard before 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 always gives precedence to the field you mapped yourself (sms_consent in Custom Data) over those automatic fields — exactly the setup prescribed in step 5, no field renaming required. |