VoterGuideOS
Concepts

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

  1. Open your organization's settings in the Branch client portal.
  2. Select Webhooks.
  3. Enter the public HTTPS endpoint URL.
  4. Select Email collected.
  5. 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

FieldDescription
idA UUID generated for this webhook event.
typeThe event type. Email webhook events use email.collected.
createdAtWhen Branch generated the event, as an ISO 8601 timestamp.
actioncreated when a new email record was stored or updated when an existing record changed.
testtrue for an event sent with the Test button and false for live events.
data.objectThe collected email record.

Email object fields

FieldDescription
idThe underlying Branch email record ID.
emailThe collected email address.
nameThe full name, when available.
firstNameThe first name, when available.
lastNameThe last name, when available.
phoneThe phone number, when collected.
contactOptInThe contact consent value, when present. Defaults to true.
activeStateThe two-letter active state code associated with the record.
districtsHuman-readable, district IDs associated with the contact.
organizationThe Branch organization key associated with the record.
ballotThe related ballot record ID, when present.
createdAtWhen the email record was created.
updatedAtWhen 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 signing
  • v1: the lowercase hexadecimal HMAC-SHA256 digest

To verify a request:

  1. Extract t and v1 from Branch-Signature.
  2. Combine the timestamp, a period, and the raw request body: <timestamp>.<raw-body>.
  3. Compute an HMAC-SHA256 digest using the webhook signing secret.
  4. Compare that digest to v1 using a timing-safe comparison.
  5. 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.

On this page