Email webhooks
Send collected email records to your CRM or automation platform in real time.
Email webhooks
Email webhooks send email contact records from Branch to another system when they are created or updated. You can use them to add contacts to a CRM, start an automation, or process contact changes in your own application.
Branch sends an HTTP POST request to your configured endpoint for each email.collected event. The request contains a JSON event envelope and a signature that an application you control can verify.
Current availability
Each organization can have one active webhook. Email collected is currently the only supported webhook event.
Before you begin
You need a public HTTPS endpoint that accepts POST requests. This can be:
- A webhook URL from an automation platform such as Zapier
- An incoming webhook supported by your CRM
- An endpoint in an application managed by your development team
The endpoint must be accessible from the public internet. It should return a successful response within five seconds and perform slower work asynchronously.
Configure a webhook
- Open your organization's settings in the Branch client portal.
- Select Webhooks.
- Enter the public HTTPS endpoint URL.
- Select Email collected.
- Select Set up webhook.
After the webhook is created, Branch displays its signing secret once. Copy the secret and store it securely before closing the dialog.
Do not put the signing secret in the endpoint URL or client-side code. If it may have been exposed, select Rotate signing secret from the configured webhook and update the receiving application with the new value.
After setup, you can send a test event, disable or re-enable delivery, and rotate the signing secret. A disabled webhook can also be permanently deleted.
Test a webhook
Select Test on an active webhook to send a signed email.collected event containing sample contact data. Test payloads use the same headers, signature, timeout, and retry behavior as live events, but include "test": true.
Check the receiving system to confirm that it accepted and processed the sample. Re-enable a disabled webhook before sending another test.
Delete a webhook
Disable the webhook, then select Delete. Deleting a webhook permanently removes its endpoint and signing secret and cannot be undone.
Event payload
Webhook requests use Content-Type: application/json. The payload contains event metadata and the email record under data.object.
{
"id": "b521092f-8ca6-4f0f-b6a8-c7b78a4790dd",
"type": "email.collected",
"createdAt": "2026-07-13T14:32:11.000Z",
"action": "created",
"test": false,
"data": {
"object": {
"id": "6874f3ab991a8688ab9b17d1",
"email": "voter@example.com",
"name": "Taylor Smith",
"firstName": "Taylor",
"lastName": "Smith",
"phone": "+14045550123",
"contactOptIn": true,
"activeState": "GA",
"districts": [
"ga-congressional-district-5",
"ga-state-senate-district-39"
],
"organization": "example-organization",
"ballot": "6874f311991a8688ab9b17ca",
"createdAt": "2026-07-13T14:32:10.000Z",
"updatedAt": "2026-07-13T14:32:10.000Z"
}
}
}Fields that were not collected or do not apply may be omitted. Integrations should ignore fields they do not recognize.
Event fields
| Field | Description |
|---|---|
id | A UUID generated for this webhook event. |
type | The event type. Email webhook events use email.collected. |
createdAt | When Branch generated the event, as an ISO 8601 timestamp. |
action | created when a new email record was stored or updated when an existing record changed. |
test | true for an event sent with the Test button and false for live events. |
data.object | The collected email record. |
Email object fields
| Field | Description |
|---|---|
id | The underlying Branch email record ID. |
email | The collected email address. |
name | The full name, when available. |
firstName | The first name, when available. |
lastName | The last name, when available. |
phone | The phone number, when collected. |
contactOptIn | The contact consent value, when present. Defaults to true. |
activeState | The two-letter active state code associated with the record. |
districts | Human-readable, district IDs associated with the contact. |
organization | The Branch organization key associated with the record. |
ballot | The related ballot record ID, when present. |
createdAt | When the email record was created. |
updatedAt | When the email record was last updated. |
The event id identifies one delivery event, while data.object.id identifies the underlying email record. Use the email record ID when the desired result is one destination contact per Branch email record.
Verify webhook signatures
When using an automation platform, such as Zapier, you do not need to worry about verifying each webhook event. Howevever, when using your own solution, we recommend verifying each request. Branch signs the exact request body with HMAC-SHA256 and sends these headers:
Content-Type: application/json
Branch-Event: email.collected
Branch-Signature: t=1783953131,v1=92af83dbe...The Branch-Signature header contains:
t: the Unix timestamp used when signingv1: the lowercase hexadecimal HMAC-SHA256 digest
To verify a request:
- Extract
tandv1fromBranch-Signature. - Combine the timestamp, a period, and the raw request body:
<timestamp>.<raw-body>. - Compute an HMAC-SHA256 digest using the webhook signing secret.
- Compare that digest to
v1using a timing-safe comparison. - Reject requests with an invalid signature or a timestamp outside your accepted tolerance.
Use the request body exactly as received. Parsing and serializing the JSON before verification can change the bytes and invalidate the signature.
Node.js example
This Express example verifies the signature before parsing the payload:
import crypto from 'node:crypto';
import express from 'express';
const app = express();
const webhookSecret = process.env.BRANCH_WEBHOOK_SECRET;
app.post(
'/webhooks/branch',
express.raw({ type: 'application/json' }),
(req, res) => {
const signatureHeader = req.get('Branch-Signature');
const signatureMatch = /^t=(\d+),v1=([a-f0-9]{64})$/.exec(
signatureHeader ?? ''
);
if (!signatureMatch || !webhookSecret) {
return res.sendStatus(401);
}
const [, timestamp, providedDigest] = signatureMatch;
const ageInSeconds = Math.abs(Date.now() / 1000 - Number(timestamp));
if (!Number.isFinite(ageInSeconds) || ageInSeconds > 300) {
return res.sendStatus(401);
}
const rawBody = req.body.toString('utf8');
const expectedDigest = crypto
.createHmac('sha256', webhookSecret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
const expectedBuffer = Buffer.from(expectedDigest, 'hex');
const providedBuffer = Buffer.from(providedDigest, 'hex');
const isValid =
expectedBuffer.length === providedBuffer.length &&
crypto.timingSafeEqual(expectedBuffer, providedBuffer);
if (!isValid) {
return res.sendStatus(401);
}
const event = JSON.parse(rawBody);
// Queue or process the verified event here.
console.log(event.type, event.data.object.email);
return res.sendStatus(204);
}
);If the application already uses express.json(), register the webhook route with express.raw() before the JSON parser. Signature verification requires the original request body.
Respond to a webhook
Return a 2xx response after accepting an event. Branch does not require a response body.
Branch waits up to five seconds for each request. If the first request times out or fails, Branch tries one more time. It does not currently retain failed deliveries for later replay.
Because an event can be delivered twice, receiving systems should be idempotent. Store the event id, or upsert the destination contact using data.object.id, to avoid duplicate side effects.
Security recommendations
- Accept webhook requests over HTTPS only.
- Keep the signing secret and endpoint URL private.
- Verify signatures for endpoints managed by your development team.
- Use a timestamp tolerance to reduce replay risk.
- Process only the event types your integration expects.
- Validate payload fields before passing them to another service.
- Rotate the signing secret if it may have been exposed.
