Zapier webhooks — Stepora Docs
Send WhatsApp events from Stepora to Zapier (or any webhook receiver) the moment they happen — no paid plan needed. Zapier's free tier includes Webhooks by Zapier (Catch Hook). Any generic receiver works too: the payload is plain JSON over HTTPS, signed with HMAC-SHA256.
Connect in 5 minutes
- In Zapier, create a Zap with a Catch Hook trigger: Zapier → Create Zap → Trigger: Webhooks by Zapier → Catch Hook → copy the webhook URL (looks like
https://hooks.zapier.com/hooks/catch/1234567/abcdefg). - In Stepora, open Connectors → Zapier Webhooks (admins pick the business first; a business client sees only their own).
- Paste the Catch Hook URL, tick the events you want, click Add subscription.
- Copy the signing secret shown once after adding — paste it into a safe place (a Zapier Code step or your receiver's config). It is never shown again; if you lose it, use Rotate secret.
- Click "Send test event" on the subscription. Zapier shows the test payload in "Test trigger" — map its fields to the next step (Google Sheets, Slack, HubSpot, whatever the Zap does). Then turn the Zap ON.
No Zapier account? Any receiver works: paste any https:// URL (a Google Apps Script web app, Pabbly, Make, n8n, your own server). For local testing only, http://127.0.0.1 / localhost URLs are accepted.
Events
| Event | When it fires | |---|---| | new_lead | A chat visitor was scored as a lead for the first time, or JUST became high-intent. Not on every scored message. | | new_message | Any inbound WhatsApp message arrived (before the bot reply). | | new_order | A customer confirmed an order in chat. Store-sync imports do not fire this. | | booking_created | A booking request was recorded from chat. | | handover_requested | A customer asked for a human; the bot paused for that chat. |
Every delivery carries these headers:
Content-Type: application/jsonX-Stepora-Event: the event name, e.g.new_leadX-Stepora-Signature:sha256=<HMAC-SHA256 of the raw request body, keyed with the subscription secret>
Every payload has the same envelope: event, timestamp (ISO 8601), business_id (your Stepora business id), source: "stepora", and a data object. Test events add "test": true at the top level and use clearly-labelled sample data.
Payloads
new_lead
{
"event": "new_lead",
"timestamp": "2026-11-07T12:00:00.000Z",
"business_id": 1,
"source": "stepora",
"data": {
"contact": { "phone": "923001234567", "name": "Ali", "email": "ali@example.com" },
"lead_score": 85,
"intent": "high",
"bant": {
"budget": { "score": 25 },
"authority": { "score": 25 },
"need": { "score": 25 },
"timeline": { "score": 10 }
},
"need": "I want to order the red kurta in medium size",
"source": "agent"
}
}
intent is one of high, medium, low, unqualified. bant carries the per-criterion scores (each 0–25); the full BANT detail (weights, signals) lives in the panel, not in the payload.
new_message
{
"event": "new_message",
"timestamp": "2026-11-07T12:00:00.000Z",
"business_id": 1,
"source": "stepora",
"data": {
"contact": { "phone": "923001234567", "name": "Ali" },
"message": { "text": "Hi, do you have this in blue?", "type": "text", "direction": "in" },
"conversation_id": 12,
"received_at": "2026-11-07T12:00:00.000Z"
}
}
message.type is text, audio (voice note — text is the transcript), or image (text is the OCR text, if any). conversation_id is the Team Inbox conversation id, or null when unknown. contact.name is empty when the number is not saved as a contact yet.
new_order
{
"event": "new_order",
"timestamp": "2026-11-07T12:00:00.000Z",
"business_id": 1,
"source": "stepora",
"data": {
"order": {
"id": 42,
"customer_phone": "923001234567",
"customer_name": "Ali",
"customer_address": "House 12, Street 4, Lahore",
"items": [{ "name": "Red Kurta", "qty": 1, "price": "$25" }],
"total": 25,
"total_text": "$25",
"payment_method": "COD",
"status": "pending"
}
}
}
booking_created
{
"event": "booking_created",
"timestamp": "2026-11-07T12:00:00.000Z",
"business_id": 1,
"source": "stepora",
"data": {
"booking": {
"id": 7,
"customer_phone": "923001234567",
"customer_name": "Ali",
"service": "Haircut",
"slot_date": "2026-11-08",
"slot_time": "10:00",
"status": "pending"
}
}
}
handover_requested
{
"event": "handover_requested",
"timestamp": "2026-11-07T12:00:00.000Z",
"business_id": 1,
"source": "stepora",
"data": {
"contact": { "phone": "923001234567" },
"reason": "customer asked for a human",
"message_text": "I want to talk to a human please"
}
}
Verify the signature
Never trust a webhook body on the URL alone — anyone who guesses the URL can POST to it. Recompute the HMAC over the raw request body (exact bytes, before JSON parsing) with your subscription secret and compare with X-Stepora-Signature using a timing-safe compare.
Node.js (Zapier Code step, or your server):
const crypto = require('crypto');
const expected = 'sha256=' + crypto
.createHmac('sha256', process.env.STEPORA_WEBHOOK_SECRET)
.update(rawBody) // Buffer or string of the exact request bytes
.digest('hex');
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(req.headers['x-stepora-signature'] || ''));
if (!ok) return res.status(401).send('bad signature');
Python:
import hmac, hashlib
expected = 'sha256=' + hmac.new(
SECRET.encode(), raw_body, hashlib.sha256
).hexdigest()
ok = hmac.compare_digest(expected, request.headers.get('X-Stepora-Signature', ''))
In Zapier itself, add a Filter or Code by Zapier step right after the Catch Hook: if the signature does not match, stop the Zap. (Zapier's free tier includes one Code step per Zap.)
Delivery, retries, and the log
- Delivery is async and never blocks the bot reply — a slow receiver cannot delay a customer.
- Each event is tried up to 3 times (first try + 2 retries, ~0.6s then ~1.8s later) with the same payload. A 2xx response counts as delivered.
- After the 3rd failure the delivery stays failed in the log — never silently dropped. Open Zapier Webhooks → Delivery log to see every attempt (event, receiver, HTTP status, attempts, time, error).
- Pause a subscription with Disable (no deliveries while disabled); Rotate secret when a secret leaks (the old secret stops working immediately — update the receiver too); Delete removes the subscription (past delivery rows stay for audit).
Honest limits
- A real Zapier Zap end-to-end needs your own Zapier account — Stepora can only prove delivery to the Catch Hook URL (the "Send test event" button does exactly that).
- Zapier's free tier limits how many tasks a Zap can run per month; a busy inbox can burn through that. Tick only the events the Zap needs.
new_orderfires only for orders confirmed in chat. Orders pulled in by a Shopify/WooCommerce sync are bulk backfill and do not fire it.new_leadfires on first capture or when a lead just becomes high-intent — not on every scored message.- Secrets are stored server-side only and shown once. They are not encrypted at rest — the same storage model as every other secret in Stepora (Meta tokens, API keys). Never paste a secret into a chat, a sheet, or a Zap step that logs it.