> ## Documentation Index
> Fetch the complete documentation index at: https://heybtw.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom Webhook

> Push event lead data into heyBTW from any source using a signed HTTP webhook.

Not every event runs on Luma, Splash, Zuddl, or Meetup. Conference badge scans, a Typeform, a Google Form, or registration you built yourself all produce the same thing: a list of people who showed interest in an event. The Custom Webhook takes that data directly.

Find it under **Integrations** in the left sidebar, then **Custom Webhook**.

## Setup

### 1. Copy your ingest URL

Each workspace has its own endpoint:

```
https://heybtw.com/api/webhooks/ingest/{workspace_id}
```

The full URL, with your workspace ID already filled in, is on the Custom Webhook page. Use the copy button rather than retyping it.

### 2. Generate a webhook secret

Click **Generate secret**. Until a secret exists, the endpoint does not accept leads.

The secret signs every request. Store it wherever you keep credentials for the system that will be sending, and treat it like a password.

## Signing a request

Every request carries two headers:

| Header               | Value                                   |
| -------------------- | --------------------------------------- |
| `X-HeyBTW-Timestamp` | Unix timestamp in seconds, at send time |
| `X-HeyBTW-Signature` | `sha256=` followed by the hex HMAC      |

The signature is an HMAC-SHA256 over the timestamp and the raw request body, joined by a period, keyed with your webhook secret:

```
signature_base = "{timestamp}.{raw_json_body}"
signature      = HMAC_SHA256(secret, signature_base)
header         = "sha256=" + hex(signature)
```

Sign the exact bytes you send. Re-serializing the JSON after signing changes the body and the signature will not match.

## Sending leads

`POST` to your ingest URL with `Content-Type: application/json`.

```json theme={null}
{
  "event_name": "Test Event",
  "source": "manual",
  "leads": [
    { "email": "test@example.com" }
  ],
  "dry_run": true
}
```

| Field        | Required | Notes                                                       |
| ------------ | -------- | ----------------------------------------------------------- |
| `event_name` | Yes      | The event these leads belong to                             |
| `source`     | Yes      | Where the data came from, for example `manual`              |
| `leads`      | Yes      | Array of lead objects. `email` is the matching key          |
| `dry_run`    | No       | When `true`, validates the request without writing anything |

Send `dry_run: true` first. It exercises the signature, the payload shape, and the endpoint without creating records, which makes it the right thing to wire into a staging environment or a health check.

## Example

```bash theme={null}
TIMESTAMP=$(date +%s)
PAYLOAD='{"event_name":"Test Event","source":"manual","leads":[{"email":"test@example.com"}],"dry_run":true}'
SECRET="<your webhook secret>"
SIGNATURE=$(echo -n "${TIMESTAMP}.${PAYLOAD}" | openssl dgst -sha256 -hmac "${SECRET}" | sed 's/^.* //')

curl -X POST "https://heybtw.com/api/webhooks/ingest/{workspace_id}" \
  -H "Content-Type: application/json" \
  -H "X-HeyBTW-Signature: sha256=${SIGNATURE}" \
  -H "X-HeyBTW-Timestamp: ${TIMESTAMP}" \
  -d "${PAYLOAD}"
```

The Custom Webhook page shows this same snippet with your workspace ID already substituted.

## What happens next

Leads arriving through the webhook behave like attendees from any other source. They can be enriched, added to [Lists](/docs/lists/overview), matched to your CRM, and counted in [Event Attribution](/docs/features/event-attribution-funnel). Email is the primary key for all of it, so a lead with no email produces a record but no attribution.

## Security notes

* **Rotate the secret** if it is ever exposed. Generating a new secret invalidates the old one, so update your sender first.
* **Verify over TLS only.** The endpoint is HTTPS and requests over plain HTTP are rejected.
* **Never put the secret in client-side code.** Send from a server, a form backend, or an automation platform, not from a browser.

## Related

* [Event Platforms](/docs/integrations/event-platforms): native integrations for Luma, Meetup Pro, Splash, and Zuddl
* [Enrichment](/docs/integrations/lead-enrichment): resolve incoming leads to company, title, and seniority
* [CRM](/docs/integrations/crm): connect HubSpot, Salesforce, or Attio
