Recipes
Four complete flows you can copy, adapt and save with POST /whatsapp-flows (see Build a flow via the API). Replace the ids (a9c4…, tag_…) with ids from your workspace.
| Recipe | Kind | Plan |
|---|---|---|
| Newsletter preference centre | Dynamic | Scale+ |
| Appointment request | Dynamic, webhook | Scale+ |
| Lead qualification | Static | Any plan with WhatsApp |
| Profile self-service | Static | Any plan with WhatsApp |
Newsletter preference centre
The contact sees every list of your preference centre, with their current lists already ticked. Unticking a list unsubscribes them.
- The
listsresolver loads the lists and the contact's subscriptions live when the flow opens. mode: "sync"subscribes ticked lists and unsubscribes unticked ones (field mapping).
{
"schema_version": 1,
"meta": { "name": "Newsletter-Einstellungen", "categories": ["SIGN_UP"], "kind": "dynamic", "language": "de", "cta": "Einstellungen öffnen" },
"screens": [
{
"id": "PREFERENCES",
"title": "Ihre Themen",
"terminal": true,
"success": true,
"data": {
"lists": { "type": "array", "example": [{ "id": "a9c4…", "title": "Angebote" }] },
"subscribed": { "type": "array", "items": { "type": "string" }, "example": [] }
},
"components": [
{ "type": "TextBody", "text": "Wählen Sie, worüber wir Sie per WhatsApp informieren dürfen." },
{
"type": "CheckboxGroup",
"name": "topics",
"label": "Themen",
"options": "${data.lists}",
"init_value": "${data.subscribed}",
"mapping": {
"target": "list_subscription",
"mode": "sync",
"consent": true,
"consent_text": "Ich möchte zu den gewählten Themen Nachrichten per WhatsApp erhalten. Abmeldung jederzeit möglich."
}
},
{ "type": "Footer", "label": "Speichern", "on_click_action": { "type": "complete" } }
]
}
],
"data_sources": [
{
"id": "prefs",
"kind": "internal",
"screen_id": "PREFERENCES",
"resolver": "lists",
"params": { "only_preference_center": true },
"outputs": { "lists": "lists", "subscribed": "subscribed_ids" }
}
]
}
Send it from a "Newsletter" button or keyword automation, or by API. Because the flow is dynamic, the lists are always current, even if the contact opens the message days later. The CheckboxGroup allows at most 20 lists (limits).
Appointment request with your booking system
The contact picks a service, then sees free slots from your own booking system, then confirms. The slots come from a custom webhook data source.
- Create the data source and store its signing secret:
curl -X POST https://api.sendseven.com/api/v1/whatsapp-flow-data-sources \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-H "Content-Type: application/json" \
-d '{ "name": "Terminkalender", "url": "https://termine.example.de/sendseven/slots", "timeout_ms": 3000 }'
- Build the flow. The service screen submits with
data_exchange; SendSeven calls your webhook for theslotsbinding ofPICK_SLOT:
{
"schema_version": 1,
"meta": { "name": "Terminanfrage", "categories": ["APPOINTMENT_BOOKING"], "kind": "dynamic", "language": "de", "cta": "Termin wählen" },
"screens": [
{
"id": "PICK_SERVICE",
"title": "Leistung",
"components": [
{
"type": "RadioButtonsGroup",
"name": "service",
"label": "Welche Leistung?",
"required": true,
"options": [
{ "id": "inspection", "title": "Inspektion" },
{ "id": "tyres", "title": "Reifenwechsel" }
]
},
{
"type": "Footer",
"label": "Weiter",
"on_click_action": {
"type": "data_exchange",
"payload": { "service": "${form.service}" },
"next_screens": ["PICK_SLOT"]
}
}
]
},
{
"id": "PICK_SLOT",
"title": "Termin",
"terminal": true,
"success": true,
"data": {
"slots": { "type": "array", "example": [{ "id": "2026-10-08T09:00", "title": "Do, 08.10. 09:00" }] }
},
"components": [
{ "type": "RadioButtonsGroup", "name": "slot", "label": "Freie Termine", "required": true, "options": "${data.slots}" },
{ "type": "TextInput", "name": "plate", "label": "Kennzeichen", "required": true },
{ "type": "Footer", "label": "Termin anfragen", "on_click_action": { "type": "complete" } }
]
}
],
"data_sources": [
{
"id": "slots",
"kind": "webhook",
"screen_id": "PICK_SLOT",
"data_source_id": "e4b1a0c2-…",
"outputs": { "slots": "available_slots" },
"fallback": { "slots": [{ "id": "call_me", "title": "Bitte rufen Sie mich an" }] }
}
]
}
- Your webhook verifies the signature, reads
data.serviceand answers with at most 20 slots:
{ "data": { "available_slots": [{ "id": "2026-10-08T09:00", "title": "Do, 08.10. 09:00" }, { "id": "2026-10-08T11:30", "title": "Do, 08.10. 11:30" }] } }
If nothing is free, answer { "error_message": "Für diese Leistung ist diese Woche nichts mehr frei." }; the contact stays on the service screen.
- Book the slot when the flow is completed, not in the webhook: listen for
whatsapp_flow.completedand readanswers.slot,answers.serviceandanswers.plate. Or use the Send WhatsApp Flow node and an API Call node to book it from an automation.
Lead qualification
A two-screen static flow that tags hot leads by budget and timing, and tags every lead that completes it.
{
"schema_version": 1,
"meta": { "name": "Projektanfrage", "categories": ["LEAD_GENERATION"], "kind": "static", "language": "de", "cta": "Anfrage starten" },
"screens": [
{
"id": "PROJECT",
"title": "Ihr Projekt",
"components": [
{
"type": "RadioButtonsGroup",
"name": "budget",
"label": "Budget",
"required": true,
"options": [
{ "id": "lt5k", "title": "Unter 5.000 €" },
{ "id": "5to20k", "title": "5.000–20.000 €" },
{ "id": "gt20k", "title": "Über 20.000 €" }
],
"mapping": { "target": "tag", "option_tags": { "gt20k": "tag_hot_lead" } }
},
{
"type": "Dropdown",
"name": "timing",
"label": "Start",
"required": true,
"options": [
{ "id": "now", "title": "Sofort" },
{ "id": "quarter", "title": "In 3 Monaten" },
{ "id": "later", "title": "Später" }
],
"mapping": { "target": "custom_field", "custom_field_id": "3f1c…" }
},
{ "type": "Footer", "label": "Weiter", "on_click_action": { "type": "navigate", "next": "CONTACT" } }
]
},
{
"id": "CONTACT",
"title": "Kontakt",
"terminal": true,
"success": true,
"components": [
{ "type": "TextInput", "name": "company", "label": "Firma", "required": true, "mapping": { "target": "custom_field", "custom_field_id": "7d2e…" } },
{ "type": "TextInput", "name": "email", "label": "E-Mail", "input_type": "email", "required": true, "mapping": { "target": "contact_field", "field": "email" } },
{ "type": "TextArea", "name": "notes", "label": "Ihr Anliegen" },
{ "type": "Footer", "label": "Anfrage senden", "on_click_action": { "type": "complete" } }
]
}
],
"on_complete": { "tag_ids": ["tag_lead_qualified"] }
}
- The
budgetanswer "Über 20.000 €" adds the tagtag_hot_lead. Thecontact_tag_addedtrigger can then notify sales. noteshas no mapping; it is in the webhook and the sessions API only.- An e-mail address that already belongs to another contact is not copied (write-back rules).
Profile self-service
Contacts check and correct their own data. The entry screen is pre-filled from their profile when the flow is sent, using the contact_profile resolver. Because the binding sits on the entry screen, this works in a static flow.
{
"schema_version": 1,
"meta": { "name": "Meine Daten", "categories": ["OTHER"], "kind": "static", "language": "de", "cta": "Daten prüfen" },
"screens": [
{
"id": "PROFILE",
"title": "Meine Daten",
"terminal": true,
"success": true,
"data": {
"first_name": { "type": "string", "example": "Anna" },
"last_name": { "type": "string", "example": "Schmidt" },
"email": { "type": "string", "example": "[email protected]" }
},
"components": [
{ "type": "TextInput", "name": "first_name", "label": "Vorname", "init_value": "${data.first_name}", "mapping": { "target": "contact_field", "field": "first_name" } },
{ "type": "TextInput", "name": "last_name", "label": "Nachname", "init_value": "${data.last_name}", "mapping": { "target": "contact_field", "field": "last_name" } },
{ "type": "TextInput", "name": "email", "label": "E-Mail", "input_type": "email", "init_value": "${data.email}", "mapping": { "target": "contact_field", "field": "email" } },
{ "type": "DatePicker", "name": "birthday", "label": "Geburtstag (optional)", "mapping": { "target": "contact_field", "field": "birthday" } },
{ "type": "Footer", "label": "Speichern", "on_click_action": { "type": "complete" } }
]
}
],
"data_sources": [
{
"id": "me",
"kind": "internal",
"screen_id": "PROFILE",
"resolver": "contact_profile",
"outputs": { "first_name": "", "last_name": "", "email": "" }
}
]
}
Empty answers never clear a stored value, so a contact who skips the birthday keeps the one on file. Add "sensitive": ["birthday"] to the screen if the birthday should not appear in the response summary on the phone.