# Route prospect replies and add leads with Zapier

Two Zaps for Victoria AI, a reply hub that verifies the webhook signature in a Code step and routes replies to Slack, Sheets or email, and one that adds leads.

Works with: Zapier, 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

Zapier has no Victoria AI app, and doesn't need one: the REST API and the `prospect_response` webhook are enough. This recipe is two Zaps. The **reply hub** receives every reply to a campaign, checks the signature, and sends each one where it belongs. The **leads** Zap adds a person to a campaign whenever something happens in another app, a new row in a sheet, a form submission, a tagged contact.

## Before you start

- A Zapier plan that includes **Webhooks by Zapier** and **Code by Zapier**. Webhooks by Zapier isn't available on the Free plan.
- An API key with `campaigns:write` (to register the webhook) and `leads:write` (for the leads Zap). 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`.

## Zap 1: the reply hub

A campaign has one webhook, so this Zap is the only receiver for the campaign's replies. Branch inside it rather than registering a second Zap.

### Step 1: Webhooks by Zapier, Catch Raw Hook

Choose **Webhooks by Zapier** as the trigger and the event **Catch Raw Hook**, not Catch Hook. Catch Raw Hook keeps the request body exactly as it arrived, as one string, and includes the request headers; that's what the signature was computed over. Copy the webhook URL Zapier shows.

Leave the trigger's test for later: the sample has to come from a signed request (step 3 of "Register and test" below).

### Step 2: Code by Zapier, verify and parse

Add **Code by Zapier** with **Run Javascript**. Under **Input Data**, add two items:

| Name | Value |
| - | - |
| `body` | the trigger's raw body field |
| `signature` | the trigger's `X-Signature-256` header field (under Headers) |

Then the code. Put your secret in the `SECRET` line, or keep it in a [Storage by Zapier](https://zapier.com/apps/storage/integrations) value and read it there.

```javascript
const crypto = require("crypto");
const SECRET = "replace-with-your-signing-secret";

const expected = "sha256=" + crypto.createHmac("sha256", SECRET).update(inputData.body, "utf8").digest("hex");
const received = inputData.signature || "";
if (expected.length !== received.length || !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received))) {
  throw new Error("Invalid webhook signature; this request didn't come from Victoria AI");
}

const event = JSON.parse(inputData.body);
const lead = event.lead || {};
const ai = event.ai_response || {};
output = {
  idempotency_key: event.idempotency_key,
  campaign: event.campaign,
  channel: event.channel,
  sentiment: ai.sentiment || "",
  out_of_office: ai.out_of_office ? "yes" : "no",
  agent_action: ai.agent_action || "",
  goal_status: ai.goal_status || "",
  brief: ai.sdr_brief || "",
  message: event.prospect_message || "",
  first_name: lead.first_name || "",
  last_name: lead.last_name || "",
  company: lead.company || "",
  title: lead.title || "",
  email: lead.email || "",
  linkedin: lead.linkedin_profile || "",
};
```

A bad signature throws, which stops the Zap run and shows it as errored in the Zap history. Everything after this step works with flat fields such as `sentiment` and `message`, so later steps never parse JSON.

### Step 3: branch and deliver

Add **Paths by Zapier**, with one path per destination, each with a condition on the Code step's fields:

- **Positive replies to Slack**: condition `sentiment` exactly matches `positive` → **Slack, Send Channel Message**, with text such as `*{{first_name}} {{last_name}}* ({{company}}) replied {{channel}} to *{{campaign}}*: {{message}}`.
- **Every reply to a sheet**: no condition → **Google Sheets, Create Spreadsheet Row**, mapping each column to a 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**: condition `agent_action` exactly matches `escalate` → **Email by Zapier** or **Gmail, Send Email** with the `brief` and the `message`.

A Zap without Paths does the same with a **Filter by Zapier** step before a single action; a reply that doesn't pass the filter simply ends the run.

Zapier's own retries are rare, but Victoria AI retries a delivery when the Zap doesn't answer `2xx` within 75 seconds, so a destination that fails can arrive twice. The `idempotency_key` column is the record.

### Register and test

1. Turn the Zap on. The trigger's webhook URL is live only while the Zap is on.
2. Register it on the campaign with [`POST /v1/campaigns/{campaign_id}/webhooks`](https://docs.versionseven.ai/api-reference/campaigns/create-webhook), sending the secret you put in the Code step. `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://hooks.zapier.com/hooks/catch/123456/abcdef/\", \"secret\": \"$VICTORIA_WEBHOOK_SECRET\", \"replace\": true}"
```

3. Send a signed example from [`GET /v1/webhooks/examples`](https://docs.versionseven.ai/api-reference/reference/list-webhook-examples) to the Zapier URL, so the trigger has a real sample and the Code step can be tested against it:

```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://hooks.zapier.com/hooks/catch/123456/abcdef/" \
  -H "Content-Type: application/json" \
  -H "X-Signature-256: sha256=$SIGNATURE" \
  --data "$BODY"
```

4. In the Zap editor, load the sample in the trigger, test the Code step (it outputs the flat fields), and test each path. Then change one character of `BODY`, send again without re-signing, and the Code step errors: that's the check working.

## Zap 2: add leads to a campaign

Any trigger works: **Google Sheets, New Spreadsheet Row**, **Typeform, New Entry**, **Airtable, New Record in View**. The action is **Webhooks by Zapier, Custom Request**:

| Field | Value |
| - | - |
| Method | `POST` |
| URL | `https://api.versionseven.ai/v1/leads` |
| Data Pass-Through? | No |
| Data | the JSON below, with trigger fields mapped in |
| Unflatten | Yes |
| Headers | `Authorization` → `Bearer vk_…`; `Content-Type` → `application/json`; `Idempotency-Key` → a value unique to the person and campaign |

```json
{
  "campaign_id": "550e8400-e29b-41d4-a716-446655440000",
  "lead": {
    "first_name": "{{First Name}}",
    "last_name": "{{Last Name}}",
    "email": "{{Email}}",
    "company": "{{Company}}",
    "title": "{{Title}}",
    "custom_fields": { "source": "typeform" }
  }
}
```

Zapier sends the **Data** field exactly as typed, so a value with a quote in it breaks the JSON; wrap mapped fields that may contain one in a **Formatter by Zapier, Text, Replace** step first, or keep the mapped fields to names, emails and titles.

For the `Idempotency-Key`, build a value from the campaign id and the person's email in a **Formatter by Zapier** step (for example a lowercase join of the two). The same person and campaign then always make the same key, and a Zap run that Zapier replays can't enrol the lead twice. See [Idempotency](https://docs.versionseven.ai/guides/idempotency).

### What the action returns

Zapier shows the API's response on the step. The outcomes to expect:

| 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. Zapier marks the step errored; add a **Filter** before the action, or accept the error as "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. Zapier's auto-replay retries errored runs; see [Rate limits](https://docs.versionseven.ai/guides/rate-limits). |

## 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 Zap.
- [Route prospect replies and add leads with n8n](https://docs.versionseven.ai/cookbook/n8n-reply-hub-and-leads) and [with Make](https://docs.versionseven.ai/cookbook/make-reply-hub-and-leads), the same two automations elsewhere.
- [Add leads from Google Sheets to a campaign](https://docs.versionseven.ai/cookbook/add-leads-from-google-sheets), a sheet that syncs itself without Zapier.
- [Receiving webhooks](https://docs.versionseven.ai/guides/webhooks) for delivery, retries and the one-webhook rule.
