Skip to main content

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.

RecipeKindPlan
Newsletter preference centreDynamicScale+
Appointment requestDynamic, webhookScale+
Lead qualificationStaticAny plan with WhatsApp
Profile self-serviceStaticAny 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 lists resolver 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.

  1. 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 }'
  1. Build the flow. The service screen submits with data_exchange; SendSeven calls your webhook for the slots binding of PICK_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" }] }
}
]
}
  1. Your webhook verifies the signature, reads data.service and 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.

  1. Book the slot when the flow is completed, not in the webhook: listen for whatsapp_flow.completed and read answers.slot, answers.service and answers.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 budget answer "Über 20.000 €" adds the tag tag_hot_lead. The contact_tag_added trigger can then notify sales.
  • notes has 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.