Send PitchWhip events
to your own systems.
PitchWhip posts a small JSON message to your URL the moment something happens. A lead revealed, a call logged, a lead added to a list, a company on your website, a form sent. Every message is signed, so you can check it came from us.
Set up an endpoint
- In the dashboard, open Settings → Connections → Webhooks. Only your team’s admin sees it.
- Paste your https URL and tick the events you want.
- Copy the signing secret (it starts
whsec_). It is shown once. If you lose it, rotate it and use the new one. - Choose Send test to post a sample event and see your server’s answer in the delivery log.
The URL is stored encrypted, because it can contain a secret of its own. After saving, Connections shows only its host and the last four characters.
Events
Every message has the same envelope: id (unique per event), type, created_at (ISO 8601, UTC) and data, whose fields depend on the type. Fields are only ever added, never renamed or removed; ignore any you don’t know.
| Event | Sent when |
|---|---|
lead.revealed | A phone number or email address was revealed, one at a time or in bulk. A number on your team's do-not-call list is left out. |
call.logged | A call was logged with an outcome, or its outcome changed, from the extension or the dashboard. |
list.lead_added | A lead was added to one of your lists (not a do-not-contact list). One event a lead, up to 100 a single add. |
visitor.company | A company was identified visiting your website. Once a company a site a day. |
visitor.person | A named person was identified visiting your website (US visitors only, where it is switched on). |
form.submitted | Someone sent your PitchWhip lead form or request-a-call form on your website. |
Example: lead.revealed
{
"id": "evt_3kq9Zt0aB1",
"type": "lead.revealed",
"created_at": "2026-11-11T10:30:01.000Z",
"data": {
"reveal_id": "rv_8f2c",
"source": "single",
"person": {
"name": "Sam Sample",
"title": "Head of Operations",
"company": "Example Logistics Ltd",
"linkedin_url": "https://www.linkedin.com/in/sam-sample"
},
"phone": "+447700900123",
"email": "sam@example-logistics.example",
"phones": [
{
"number": "+447700900123",
"region": "GB"
}
],
"emails": [
{
"email": "sam@example-logistics.example",
"status": "DELIVERABLE"
}
]
}
}Example: call.logged
{
"id": "evt_3kq9Zt0aB1",
"type": "call.logged",
"created_at": "2026-11-11T10:30:01.000Z",
"data": {
"call_id": "c_1731321000",
"called_at": "2026-11-11T10:30:00.000Z",
"outcome": "booked",
"duration_sec": 312,
"person": {
"name": "Sam Sample",
"title": "Head of Operations",
"company": "Example Logistics Ltd",
"linkedin_url": "https://www.linkedin.com/in/sam-sample"
},
"caller_name": "Alex Rep",
"callback": {
"date": "2026-11-18",
"time": "14:00"
},
"meeting": {
"date": "2026-11-20",
"time": "10:00"
},
"country": "United Kingdom"
}
}Example: list.lead_added
{
"id": "evt_3kq9Zt0aB1",
"type": "list.lead_added",
"created_at": "2026-11-11T10:30:01.000Z",
"data": {
"list": {
"id": 42,
"name": "Manchester logistics"
},
"source": "upload",
"lead": {
"name": "Sam Sample",
"title": "Head of Operations",
"company": "Example Logistics Ltd",
"email": "sam@example-logistics.example",
"linkedin_url": "https://www.linkedin.com/in/sam-sample",
"phone": "+447700900123",
"segment": "Operations",
"country": "GB"
}
}
}Example: visitor.company
{
"id": "evt_3kq9Zt0aB1",
"type": "visitor.company",
"created_at": "2026-11-11T10:30:01.000Z",
"data": {
"site": {
"domain": "example.com"
},
"company": {
"name": "Example Logistics Ltd",
"domain": ""
},
"match": "network",
"page": "/pricing",
"referrer": "www.google.com",
"country": "GB",
"visited_at": "2026-11-11T10:30:00.000Z"
}
}Example: visitor.person
{
"id": "evt_3kq9Zt0aB1",
"type": "visitor.person",
"created_at": "2026-11-11T10:30:01.000Z",
"data": {
"site": {
"domain": "example.com"
},
"source": "pixel",
"person": {
"name": "Sam Sample",
"title": "Head of Operations",
"company": "Example Logistics Ltd",
"company_domain": "example-logistics.example",
"linkedin_url": "https://www.linkedin.com/in/sam-sample"
},
"visited_at": "2026-11-11T10:30:00.000Z"
}
}Example: form.submitted
{
"id": "evt_3kq9Zt0aB1",
"type": "form.submitted",
"created_at": "2026-11-11T10:30:01.000Z",
"data": {
"site": {
"domain": "example.com"
},
"submission_id": "123",
"form": "call",
"name": "Sam Sample",
"email": "sam@example-logistics.example",
"company": "Example Logistics Ltd",
"phone": "+44 7700 900123",
"message": "",
"call_windows": [
"Tuesday morning"
],
"page": "/contact",
"country": "GB",
"submitted_at": "2026-11-11T10:30:00.000Z"
}
}Each event carries only what your team can already see in PitchWhip. People who asked PitchWhip to remove their details are never sent.
Headers
| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | PitchWhip-Webhooks/1 |
X-PitchWhip-Event | The event type, e.g. call.logged |
X-PitchWhip-Delivery | A unique id for this delivery (whd_…). The same on every retry: use it to ignore repeats. |
X-PitchWhip-Timestamp | Unix seconds when this attempt was sent |
X-PitchWhip-Signature | sha256= followed by the hex HMAC-SHA256 of the raw request body, keyed with your signing secret |
Check the signature
X-PitchWhip-Signature is sha256= and the hex HMAC-SHA256 of the raw request body, keyed with your endpoint’s signing secret. Compute it over the bytes you received, before parsing the JSON, and compare in constant time. The timestamp is not part of the signed text. For replay protection, refuse a message whose X-PitchWhip-Timestamp is more than a few minutes old. Ignore an X-PitchWhip-Delivery you have already handled.
Node.js
import crypto from "node:crypto";
import express from "express";
const app = express();
// Keep the raw bytes: the signature is over the body exactly as sent.
app.post("/pitchwhip", express.raw({ type: "application/json" }), (req, res) => {
const expected = "sha256=" + crypto
.createHmac("sha256", process.env.PITCHWHIP_WEBHOOK_SECRET)
.update(req.body)
.digest("hex");
const given = req.get("X-PitchWhip-Signature") || "";
const ok = given.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected));
if (!ok) return res.status(401).end();
// Optional: refuse anything older than 5 minutes.
const age = Date.now() / 1000 - Number(req.get("X-PitchWhip-Timestamp"));
if (!(age < 300)) return res.status(400).end();
const event = JSON.parse(req.body.toString("utf8"));
console.log(event.type, event.data);
res.status(200).end();
});Python
import hashlib, hmac, os, time
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["PITCHWHIP_WEBHOOK_SECRET"].encode()
@app.post("/pitchwhip")
def pitchwhip():
body = request.get_data() # the raw bytes, before any parsing
expected = "sha256=" + hmac.new(SECRET, body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, request.headers.get("X-PitchWhip-Signature", "")):
abort(401)
# Optional: refuse anything older than 5 minutes.
if time.time() - int(request.headers.get("X-PitchWhip-Timestamp", "0")) > 300:
abort(400)
event = request.get_json()
print(event["type"], event["data"])
return "", 200Retries
- Answer with any 2xx status within 5 seconds. The body you send back is ignored.
- A timeout, a network error, 408, 429 or a 5xx is tried again: up to 5 tries in all, about 5 minutes, 30 minutes, 3 hours and 20 hours apart (roughly a day).
- Any other 4xx, or a redirect, is not tried again. Redirects are never followed.
- After 50 failed tries in a row the endpoint is paused and Connections says so. Turn it back on when it is fixed.
- Deliveries can arrive more than once and out of order. Use X-PitchWhip-Delivery or the event id to ignore repeats.
Send a test
Send test posts one sample event of the type you choose, signed with your secret, straight away. Its body has "test": true and sample data. It is tried once, and the result appears at the top of the delivery log.
Limits
- Up to 5 endpoints a team, each with its own events and signing secret. Zapier connections are counted separately.
- https only, on the standard port, to a public host name. Private networks and IP addresses are refused.
- Each delivery is a JSON body of at most 60 KB.
- Connections shows the last 50 deliveries for each endpoint: time, event, status, try, response time and the reason for a failure. Bodies are not kept once a delivery is finished.
- Events are sent on paid plans. A trial can set up endpoints and send tests.
- Only your team's admin can add, change or delete endpoints, or see the delivery log.
Zapier
The PitchWhip Zapier app uses the same events as triggers, plus actions to find or reveal a contact and add a lead to a list. Connect it with an API key from Settings → Connections → API keys. A Zap’s subscription stops when its key is revoked.
Ready for your next conversation?
Free to install in desktop Chrome. Free scripts with a work email.