Body templates
Shape the JSON a webhook sends to match the tool that receives it, with Contact, Account and Motion values filled in. No code on your side.
By default a webhook sends Curviate's standard payload. Set body_template and it sends your JSON instead: fixed values as you wrote them, with {{variables}} replaced by the event's values. The body is signed like any delivery (Verifying signatures) and always sent as application/json. null is the standard payload.
A template and the data field remapping on the messaging and user sources exclude each other: sending both is 400 INVALID_REQUEST. A template names its fields with variables instead.
curviate webhook create --source motion --events motion.custom_event \
--request-url https://api.example.com/leads \
--template-file ./lead.jsonThe REST form sends the template text as body_template:
curl -X POST https://api.curviate.com/v1/webhooks \
-H "Authorization: Bearer $CURVIATE_API_KEY" -H "Content-Type: application/json" \
-d '{
"source": "motion",
"request_url": "https://api.example.com/leads",
"events": ["motion.custom_event"],
"body_template": "{\"email\": \"{{contact.email}}\", \"name\": \"{{contact.full_name}}\"}"
}'--template-file - reads the template from standard input. curviate webhook update <id> --template-file <path|-> replaces it. To check a template without saving anything, lint it offline with the same rules the API applies; it exits 2 when there is an error:
curviate webhook lint --template-file ./lead.json --source motionVariables
A variable sits inside a JSON string, {{name}}, with optional spaces inside the braces. GET /v1/webhooks/variables?source=<source> (CLI: curviate webhook variables --source <source>) lists exactly what a source carries, with an example for each; any role may read it.
| Variable | Value |
|---|---|
{{event}}, {{occurred_at}} | The event type and when it happened. |
{{account.first_name}}, {{account.last_name}}, {{account.full_name}} | The LinkedIn Account the event belongs to. Short forms: {{my_first_name}}, {{my_last_name}}, {{my_full_name}}. |
{{contact.full_name}}, first_name, last_name, headline, job_title, company, email, location, linkedin_url, about | The Contact. Short forms: {{first_name}}, {{company}}, and so on. |
{{contact.extra.<key>}} | One of the Contact's custom fields. |
{{motion.id}}, {{motion.name}}, {{enrollment_id}} | The event's Motion and Enrollment. |
{{step_id}} | The Step the event came from. motion source only. |
{{enrollment.<key>}} | An Enrollment value saved by an until event.<name> Step with save. motion source only. |
The motion source carries all of them. The messaging and user sources carry the event, Account, Contact and Motion variables when the event belongs to a Motion's Enrollment. The account_status and post sources carry {{event}}, {{occurred_at}} and the Account variables. A variable the source never carries is an unknown variable (T6).
A bare name that is neither a standard Contact field nor a my_ form is refused: write {{contact.extra.<name>}} for a custom field. {{enrollment.id}} is reserved: use {{enrollment_id}}.
A template that uses {{contact.email}} or {{email}} is not sent for an event whose Contact has no email. The delivery is recorded as skipped_no_email: no request, no retry, neither a success nor a failure.
How a template renders
- The template must be valid JSON as written. Curviate fills the parsed value, so every value is escaped for you.
- A string that is exactly one variable keeps the value's type:
"{{enrollment.score}}"is a number when the saved value is one. - A missing value (absent,
null, blank, an object or an array) drops its key, or its element in an array. Inside other text, a missing value renders as empty text. \{{writes a literal{{.- There are no defaults, filters or logic. To branch, use
ifin the Sequence. - The body is rendered once, when the delivery first runs. A retry or a redelivery sends those same bytes, freshly signed (Delivery & retries).
- At most 16,384 bytes and 200 variables per template.
Lint codes
A create or an edit checks the template. When anything is wrong the answer is 422 WEBHOOK_TEMPLATE_INVALID, nothing is saved, and errors[] lists every finding at once. Each carries code, severity, message, pointer, line, col (into the template), hint, docs_url (this page and the code's anchor) and, for an unknown variable, candidates[]. Warnings come back in warnings[] on a successful write.
T1: Empty template
The template is an empty string. Send null for the standard payload, or write the JSON body your tool expects.
T2: Not valid JSON
The text does not parse as JSON. Use double-quoted keys and strings, commas between members and nothing after the last value. A trailing comma is the usual cause, and the hint names it.
T3: Variable never closed
A {{ has no closing }}. Write it as {{contact.email}}.
T4: Variable outside a string
A variable stands where JSON expects a bare value. Put it inside quotes: "{{enrollment.score}}" keeps the value's type.
T5: Variable in a key
A variable is used as an object key. Use a fixed key and put the variable in the value: "email": "{{contact.email}}".
T6: Unknown variable
The name is not a variable, or the webhook's source never carries it. The hint and candidates offer the nearest names. GET /v1/webhooks/variables?source=<source> lists what the source carries.
T7: enrollment.id is reserved
Use {{enrollment_id}} for the Enrollment's id.
T8: No defaults or filters
A | follows the variable. Templates have no defaults or filters, and a missing value drops its key. Remove the part after the bar.
T9: Single-brace placeholder
{name} is written with single braces. Variables use double braces: {{name}}.
T10: Template size
The template is over 16,384 bytes or holds more than 200 variables. Shorten it.
TW1: Enrollment value no Sequence saves
A warning, not an error. {{enrollment.<key>}} names a value that no current Sequence version in your Team saves, so it is always missing. Save it with save on an until event.<name> Step (Sequence rules); until one does, its key is dropped. The Sequence may be written after the webhook, so this does not block the write.
Test it
POST /v1/webhooks/{id}/test (CLI: curviate webhook test <id> [--event <type>]) sends one delivery to your URL. With a template it is your real template rendered on a sample event of the type you pick (event, default the first you subscribed), filled with a made-up Contact at example.com, never a real person. It carries Curviate-Test: true, makes one attempt and never changes the webhook's health.
Your tool receives a test like a real event, and it may create a record. The
Curviate-Test header is for your own receiver; a third-party API ignores it.
Next steps
- Webhook recipes: templates for Instantly, Smartlead, HubSpot and n8n.
- Incoming events: the other direction, a tool's event moves an Enrollment on.
- Delivery & retries: how a delivery is retried and when a webhook is switched off.