Route prospect replies and add leads with Make
Two Make scenarios for Victoria AI, a reply hub that verifies the webhook signature with sha256 and routes replies by sentiment, and a scenario that adds leads.
Last updated
- Make
- Slack
- Google Sheets
Make (formerly Integromat) talks to Victoria AI through its Webhooks and HTTP apps; no Victoria app is needed. This recipe is two scenarios. The reply hub receives every reply to a campaign, checks the signature with Make's built-in sha256 function, and routes each reply to Slack, a sheet or an email. The leads scenario adds a person to a campaign when something happens in another app.
Before you start
- A Make account. The scenarios use the Webhooks, Router, Slack, Google Sheets and HTTP apps.
- An API key with
campaigns:write(to register the webhook) andleads:write(for the leads scenario). See Create an API key. - A signing secret of your own, at least 16 characters. One way to make one is
openssl rand -hex 32. - The campaign's id, from its URL in the app or from
GET /v1/campaigns.
Scenario 1: the reply hub
A campaign has one webhook, so this scenario is the only receiver for the campaign's replies. Branch with a Router rather than registering a second scenario.
Module 1: Webhooks, Custom webhook
Add a Custom webhook and create a new hook. Open Show advanced settings and set two things before saving:
- Get request headers: on. The signature arrives in a header.
- JSON pass-through: on. Make then passes the body on as one text string instead of splitting it into fields. The signature was computed over those exact bytes, so the string is what has to be hashed; a body Make has parsed and rebuilt won't verify.
Copy the webhook's address. Make determines the hook's output structure from the first request it receives, so send the signed example from "Register and test" below before building the rest, then come back: the webhook's output shows one text field with the body and a headers collection.
Module 2: Router, with a verification filter
Add a Router after the webhook. Every route out of it starts with the same filter, which only lets a correctly signed request through. In the filter, compare:
- The header value:
{{get(map(1.headers; "value"; "name"; "x-signature-256"); 1)}}, which reads thex-signature-256entry from the headers collection. - With:
sha256=followed by{{sha256(1.<body field>; "hex"; "my-signing-secret")}}, where<body field>is the text field the webhook output shows for the body and the third argument is your secret.sha256(text; encoding; key)returns the HMAC when a key is given.
Use the Text operators: Equal to condition. A request with a wrong signature matches no route and the scenario run ends there, which is what should happen.
To use the body's fields in later modules, add a JSON, Parse JSON module after the filter on each route (or once, before the Router, if you prefer to verify first and route second), with the body field as its input. Its output is the full prospect_response event: lead.first_name, ai_response.sentiment, prospect_message and so on.
Routes
Add a filter condition to each route, after the signature check:
- Positive replies to Slack:
ai_response.sentimentequal topositive→ Slack, Create a Message with text such as*{{first_name}} {{last_name}}* ({{company}}) replied by {{channel}} to *{{campaign}}*: {{prospect_message}}. - Every reply to a sheet: no extra condition → Google Sheets, Add a Row, one column per field. Put
idempotency_keyin its own column; a retried delivery carries the same key, so a duplicate row is easy to spot. - Hand-offs to a person:
ai_response.agent_actionequal toescalate→ Email, Send an Email withai_response.sdr_briefand the message.
Make answers the webhook with 200 Accepted as soon as the scenario starts, so Victoria AI treats the delivery as done; a module that fails later shows in the scenario's history, and the run can be re-executed from there.
Register and test
- Turn the scenario on, or leave it in the editor with Run once waiting on the webhook for the first test.
- Send a signed example from
GET /v1/webhooks/examplesto the webhook's address, so Make learns the output structure and you can see the body and headers fields:
BODY=$(curl -s https://api.versionseven.ai/v1/webhooks/examples -H "Authorization: Bearer $VICTORIA_API_KEY" | python3 -c 'import json,sys; print(json.dumps(json.load(sys.stdin)["examples"][0]))')
SIGNATURE=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$VICTORIA_WEBHOOK_SECRET" | sed 's/^.* //')
curl -X POST "https://hook.eu1.make.com/abcdef123456" \
-H "Content-Type: application/json" \
-H "X-Signature-256: sha256=$SIGNATURE" \
--data "$BODY"- Register the address on the campaign with
POST /v1/campaigns/{campaign_id}/webhooks, sending the same secret.replace: truerepoints the campaign's one webhook here if something else held it:
curl -X POST https://api.versionseven.ai/v1/campaigns/$CAMPAIGN_ID/webhooks \
-H "Authorization: Bearer $VICTORIA_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"webhook_url\": \"https://hook.eu1.make.com/abcdef123456\", \"secret\": \"$VICTORIA_WEBHOOK_SECRET\", \"replace\": true}"- Send the example again with the scenario running: the filter passes and the routes fire. Then change one character of
BODY, send it without re-signing, and no route runs.
Scenario 2: add leads to a campaign
Any trigger works: Google Sheets, Watch New Rows, Typeform, Watch Responses, Airtable, Watch Records. The action is HTTP, Make a request:
| Field | Value |
|---|---|
| URL | https://api.versionseven.ai/v1/leads |
| Method | POST |
| Headers | Authorization → Bearer vk_…; Idempotency-Key → {{sha256(lower("550e8400-… " + 1.email))}} or another value unique to the person and campaign |
| Body type | Raw |
| Content type | JSON (application/json) |
| Request content | the JSON below, with the trigger's fields mapped in |
| Parse response | Yes |
{
"campaign_id": "550e8400-e29b-41d4-a716-446655440000",
"lead": {
"first_name": "{{1.first_name}}",
"last_name": "{{1.last_name}}",
"email": "{{1.email}}",
"company": "{{1.company}}",
"title": "{{1.title}}",
"custom_fields": { "source": "typeform" }
}
}A mapped value that contains a double quote breaks the JSON; wrap such fields in Make's replace() function, or build the body with a JSON, Create JSON module, which escapes for you.
The Idempotency-Key makes a re-run safe: the same person and campaign always produce the same key, and the API returns the first response again instead of enrolling twice. See Idempotency.
What the request returns
With Parse response on, the status and body are mappable in later modules, so a Router after the HTTP module can treat each outcome differently:
| Response | Meaning |
|---|---|
201 | A new lead was created and enrolled; lead_id is in the body. |
200 | The person was already a lead in your organization and was enrolled. lead_created is false. |
409 LEAD_ALREADY_IN_CAMPAIGN | Already there. Nothing to do. |
409 LEAD_SUPPRESSED | The person is on your Do Not Contact list. Nothing was created. |
400 VALIDATION_ERROR | A required field is missing or malformed; details.errors lists each. A lead needs first_name, last_name, and email or linkedin_url. |
403 TRIAL_LEAD_CAP_REACHED | The organization is on a free trial and has used its lead quota. |
429 RATE_LIMITED | More than 100 requests a minute to the endpoint. Add a Sleep module in a loop, or use Make's error handler with a Break directive to retry later. See Rate limits. |
By default the HTTP module treats a 4xx as an error and stops the run; turn on Evaluate all states as errors: No in its advanced settings if you'd rather route on the status code.
Next steps
- Receive replies once and fan them out, the same hub as a small server, for teams that would rather run code than a scenario.
- Route prospect replies and add leads with n8n and with Zapier, the same two automations elsewhere.
- Receiving webhooks for delivery, retries and the one-webhook rule.