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

Works with: Make, Slack, Google Sheets.

Endpoints used:

- [`POST /v1/campaigns/{campaign_id}/webhooks`](https://docs.versionseven.ai/api-reference/campaigns/create-webhook) Create a webhook
- [`GET /v1/webhooks/examples`](https://docs.versionseven.ai/api-reference/reference/list-webhook-examples) List webhook examples
- [`POST /v1/leads`](https://docs.versionseven.ai/api-reference/leads/create-lead) Create a lead

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) and `leads:write` (for the leads scenario). See [Create an API key](https://docs.versionseven.ai/help/api-keys).
- 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 the `x-signature-256` entry 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.sentiment` equal to `positive` → **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_key` in 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_action` equal to `escalate` → **Email, Send an Email** with `ai_response.sdr_brief` and 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

1. Turn the scenario on, or leave it in the editor with **Run once** waiting on the webhook for the first test.
2. Send a signed example from [`GET /v1/webhooks/examples`](https://docs.versionseven.ai/api-reference/reference/list-webhook-examples) to the webhook's address, so Make learns the output structure and you can see the body and headers fields:

```bash
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"
```

3. Register the address on the campaign with [`POST /v1/campaigns/{campaign_id}/webhooks`](https://docs.versionseven.ai/api-reference/campaigns/create-webhook), sending the same secret. `replace: true` repoints the campaign's one webhook here if something else held it:

```bash
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}"
```

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

```json
{
  "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](https://docs.versionseven.ai/guides/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](https://docs.versionseven.ai/help/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](https://docs.versionseven.ai/guides/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](https://docs.versionseven.ai/cookbook/webhook-receiver), 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](https://docs.versionseven.ai/cookbook/n8n-reply-hub-and-leads) and [with Zapier](https://docs.versionseven.ai/cookbook/zapier-reply-hub-and-leads), the same two automations elsewhere.
- [Receiving webhooks](https://docs.versionseven.ai/guides/webhooks) for delivery, retries and the one-webhook rule.
