Webhooks

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.json

The 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 motion

Variables

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.

VariableValue
{{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, aboutThe 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 if in 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.

It reaches your tool like a real event

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