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.
Last updated
- Zapier
- Slack
- Google Sheets
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) andleads:write(for the leads Zap). 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.
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 value and read it there.
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
sentimentexactly matchespositive→ 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_keyin 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_actionexactly matchesescalate→ Email by Zapier or Gmail, Send Email with thebriefand themessage.
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
- Turn the Zap on. The trigger's webhook URL is live only while the Zap is on.
- Register it on the campaign with
POST /v1/campaigns/{campaign_id}/webhooks, sending the secret you put in the Code step.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://hooks.zapier.com/hooks/catch/123456/abcdef/\", \"secret\": \"$VICTORIA_WEBHOOK_SECRET\", \"replace\": true}"- Send a signed example from
GET /v1/webhooks/examplesto the Zapier URL, so the trigger has a real sample and the Code step can be tested against it:
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"- 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 |
{
"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.
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 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. |
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 Zap.
- Route prospect replies and add leads with n8n and with Make, the same two automations elsewhere.
- Add leads from Google Sheets to a campaign, a sheet that syncs itself without Zapier.
- Receiving webhooks for delivery, retries and the one-webhook rule.