Webhook recipes
Each recipe is three pieces that work together: an outgoing webhook with a body template that hands a Contact to the tool, an incoming event that takes the tool's answer back, and a Sequence fragment that waits for it. Replace every YOUR_... value with your own. Every recipe passes the template lint and curviate motion lint.
No recipe sends the text of a reply to another tool: the templates carry the Contact's profile fields and ids only. A tool's endpoint and field names can change, so check each against the tool's own documentation, linked in every recipe, before you rely on it.
Instantly
A LinkedIn invitation that is not accepted hands the Contact to an Instantly campaign; Instantly's reply_received moves the Enrollment on. The same incoming URL also takes a lead_scored post from your scoring step.
Checked against: developer.instantly.ai/api-reference/lead/create-lead, developer.instantly.ai/guides/webhook-events
1. The webhook
{
"source": "motion",
"events": [
"motion.custom_event"
],
"custom_event_names": [
"start_email_sequence"
],
"request_url": "https://api.instantly.ai/api/v2/leads",
"headers": [
{
"key": "Authorization",
"value": "Bearer YOUR_INSTANTLY_API_KEY"
}
]
}The body_template, as sent to the tool:
{
"campaign": "YOUR_INSTANTLY_CAMPAIGN_ID",
"email": "{{contact.email}}",
"first_name": "{{first_name}}",
"last_name": "{{last_name}}",
"company_name": "{{company}}",
"job_title": "{{job_title}}",
"personalization": "Saw your profile ({{headline}}) on LinkedIn.",
"skip_if_in_campaign": true,
"custom_variables": {
"linkedin_url": "{{linkedin_url}}",
"curviate_enrollment_id": "{{enrollment_id}}",
"score": "{{enrollment.score}}",
"phone": "{{contact.extra.phone}}"
}
}2. The incoming event
{
"event_name_field": "event_type",
"match": {
"email": "lead_email"
}
}3. The Sequence
{
"on_reply": "end",
"steps": [
{
"id": "invite",
"do": "invite.send",
"with": {
"withdraw_after": "21d"
}
},
{
"id": "accepted",
"until": "invite.accepted",
"timeout": "21d",
"then": "opener",
"else": "scored"
},
{
"id": "opener",
"do": "message.send",
"with": {
"text": "Hi {{first_name}}, thanks for connecting.",
"fallback": "Thanks for connecting."
},
"next": "end"
},
{
"id": "scored",
"until": "event.lead_scored",
"timeout": "7d",
"save": {
"score": "score"
},
"then": "is_hot",
"else": "end"
},
{
"id": "is_hot",
"if": {
"enrollment.score": {
"gte": 50
}
},
"then": "start_email",
"else": "end"
},
{
"id": "start_email",
"emit": "start_email_sequence",
"next": "replied"
},
{
"id": "replied",
"until": "event.reply_received",
"timeout": "14d",
"then": "end",
"else": "end"
}
]
}Smartlead
A Contact who has not accepted after 21 days joins a Smartlead campaign; Smartlead's EMAIL_REPLY moves the Enrollment on.
Checked against: api.smartlead.ai/reference/add-leads-to-a-campaign-by-id, api.smartlead.ai/guides/webhook-integration
1. The webhook
{
"source": "motion",
"events": [
"motion.custom_event"
],
"custom_event_names": [
"start_email_sequence"
],
"request_url": "https://server.smartlead.ai/api/v1/campaigns/YOUR_CAMPAIGN_ID/leads?api_key=YOUR_SMARTLEAD_API_KEY",
"headers": []
}The body_template, as sent to the tool:
{
"lead_list": [
{
"email": "{{contact.email}}",
"first_name": "{{first_name}}",
"last_name": "{{last_name}}",
"company_name": "{{company}}",
"location": "{{location}}",
"linkedin_profile": "{{linkedin_url}}",
"custom_fields": {
"job_title": "{{job_title}}",
"curviate_enrollment_id": "{{enrollment_id}}"
}
}
]
}2. The incoming event
{
"event_name_field": "event_type",
"match": {
"email": "to_email"
}
}3. The Sequence
{
"on_reply": "end",
"steps": [
{
"id": "invite",
"do": "invite.send",
"with": {
"withdraw_after": "21d"
}
},
{
"id": "accepted",
"until": "invite.accepted",
"timeout": "21d",
"then": "end",
"else": "start_email"
},
{
"id": "start_email",
"emit": "start_email_sequence",
"next": "replied"
},
{
"id": "replied",
"until": "event.email_reply",
"timeout": "14d",
"save": {
"campaign": "campaign_name"
},
"then": "thanks",
"else": "end"
},
{
"id": "thanks",
"if": {
"enrollment.campaign": {
"exists": true
}
},
"then": "end",
"else": "end"
}
]
}HubSpot
An accepted invitation creates the HubSpot contact; a HubSpot workflow's Send a webhook action reports a booked meeting back, which ends the Enrollment and keeps the deal stage on the Contact.
Checked against: developers.hubspot.com/docs/api-reference/crm-contacts-v3/basic/post-crm-v3-objects-contacts
1. The webhook
{
"source": "motion",
"events": [
"enrollment.accepted"
],
"custom_event_names": null,
"request_url": "https://api.hubapi.com/crm/v3/objects/contacts",
"headers": [
{
"key": "Authorization",
"value": "Bearer YOUR_HUBSPOT_PRIVATE_APP_TOKEN"
}
]
}The body_template, as sent to the tool:
{
"properties": {
"email": "{{contact.email}}",
"firstname": "{{first_name}}",
"lastname": "{{last_name}}",
"company": "{{company}}",
"jobtitle": "{{job_title}}"
}
}2. The incoming event
{
"event_name": "meeting_booked",
"match": {
"email": "email"
}
}3. The Sequence
{
"on_reply": "end",
"steps": [
{
"id": "invite",
"do": "invite.send",
"with": {
"withdraw_after": "21d"
}
},
{
"id": "accepted",
"until": "invite.accepted",
"timeout": "21d",
"then": "booked",
"else": "end"
},
{
"id": "booked",
"until": "event.meeting_booked",
"timeout": "30d",
"save_to_contact": {
"hubspot_stage": "dealstage"
},
"then": "end",
"else": "nudge"
},
{
"id": "nudge",
"do": "message.send",
"with": {
"text": "Hi {{first_name}}, is a short call still useful?",
"fallback": "Is a short call still useful?"
},
"next": "end"
}
]
}n8n
Every custom event reaches an n8n Webhook node with the Enrollment id; the workflow posts its verdict back with that id, so the match never depends on an email.
Checked against: docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.webhook/, docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.httprequest/
1. The webhook
{
"source": "motion",
"events": [
"motion.custom_event"
],
"custom_event_names": [
"qualify"
],
"request_url": "https://YOUR_N8N_HOST/webhook/YOUR_WEBHOOK_PATH",
"headers": [
{
"key": "X-N8n-Token",
"value": "YOUR_HEADER_AUTH_VALUE"
}
]
}The body_template, as sent to the tool:
{
"event": "{{event}}",
"enrollment_id": "{{enrollment_id}}",
"motion": "{{motion.name}}",
"step": "{{step_id}}",
"contact": {
"name": "{{contact.full_name}}",
"linkedin_url": "{{linkedin_url}}",
"company": "{{company}}"
}
}2. The incoming event
{
"event_name": "qualified",
"match": {
"enrollment_id": "enrollment_id"
}
}3. The Sequence
{
"on_reply": "end",
"steps": [
{
"id": "invite",
"do": "invite.send",
"with": {
"withdraw_after": "21d"
}
},
{
"id": "accepted",
"until": "invite.accepted",
"timeout": "21d",
"then": "qualify",
"else": "end"
},
{
"id": "qualify",
"emit": "qualify",
"next": "verdict"
},
{
"id": "verdict",
"until": "event.qualified",
"timeout": "3d",
"save": {
"tier": "tier"
},
"then": "is_a",
"else": "end"
},
{
"id": "is_a",
"if": {
"enrollment.tier": {
"eq": "a"
}
},
"then": "pitch",
"else": "end"
},
{
"id": "pitch",
"do": "message.send",
"with": {
"text": "Hi {{first_name}}, worth a short call this week?",
"fallback": "Worth a short call this week?"
},
"next": "end"
}
]
}Before you switch it on
- Send a test from the webhook, then post a real example to the incoming URL so it captures a sample ( Incoming events). A test reaches the tool like a real event and may create a record.
- A request that timed out after the tool had already created its record is retried, so a template that creates a record can create it twice. Use the tool's own skip-if-exists option where it has one, and see Delivery & retries.
- A template that uses the Contact's email is not sent for a Contact without one; the delivery shows as
skipped_no_email.