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

# Webhooks

> Send newly matched jobs to your own tools with signed webhook delivery.

Outbound webhooks are a Pro feature. They send one signed `POST` request for each newly matched job.

Use webhooks when you want matched jobs to flow into tools like Slack, Notion, Airtable, Zapier, Make, or your own workflow.

<Note>
  Email notification settings do not control webhook delivery. Use the webhook **Active** switch to pause or resume webhooks.
</Note>

## What triggers a webhook

JobBeacon sends a `job.matched` webhook when all of these are true:

* You are on Pro.
* You have saved an active webhook endpoint.
* JobBeacon finds a new job after the company's first scan.
* The job matches that company's keyword and location filters.

The first scan seeds existing jobs. Seed jobs do not send webhook delivery.

If a job does not match your filters when JobBeacon first sees it, changing filters later does not send a backfill webhook for that old job.

## Set up a webhook

<Steps>
  <Step title="Open Settings">
    Click your avatar, then click **Settings**.
  </Step>

  <Step title="Find Outbound Webhook">
    In **Outbound Webhook**, enter your **Endpoint URL**.
  </Step>

  <Step title="Choose the payload version">
    Keep the current **Payload version** unless an existing integration needs an older payload shape.
  </Step>

  <Step title="Add authentication if needed">
    If your endpoint requires API key authentication, enter a **Provider auth header** name such as `x-jobbeacon-apikey`.
  </Step>

  <Step title="Save the endpoint">
    Click **Save**. The endpoint must use HTTPS.
  </Step>

  <Step title="Copy the signing secret">
    Copy the **Signing secret** when it appears. JobBeacon only shows the full secret once.
  </Step>

  <Step title="Send a test">
    Click **Send test** to queue a `webhook.test` delivery.
  </Step>
</Steps>

Your account can have one webhook endpoint.

## Endpoint requirements

Your endpoint must:

* Use `https://`
* Be reachable from the public internet
* Return a `2xx` response when delivery succeeds
* Not use credentials in the URL

JobBeacon blocks localhost, private network addresses, and test-only hostnames.

Use the **Provider auth header** field if your endpoint requires an API key in a custom header.

## Request headers

Each delivery includes these headers:

| Header                      | Value                                              |
| --------------------------- | -------------------------------------------------- |
| `Content-Type`              | `application/json`                                 |
| `User-Agent`                | `JobBeacon-Webhooks/1.0`                           |
| `JobBeacon-Event-Id`        | The event ID, such as `evt_...`                    |
| `JobBeacon-Timestamp`       | Unix timestamp in seconds                          |
| `JobBeacon-Signature`       | HMAC signature, such as `v1=...`                   |
| `JobBeacon-Webhook-Version` | The selected payload version, such as `2026-06-15` |
| `Idempotency-Key`           | Stable dedupe key for the event                    |

Use `JobBeacon-Event-Id` or `Idempotency-Key` to ignore duplicate deliveries.

If you set a **Provider auth header**, JobBeacon sends the signing secret as the value of that header.

For example, a provider auth header named `x-jobbeacon-apikey` sends:

```text theme={null}
x-jobbeacon-apikey: whsec_...
```

JobBeacon only sends the provider auth header to the original endpoint origin. If your endpoint redirects to a different origin, the redirected request does not include that header.

## Verify the signature

JobBeacon signs the raw JSON body with your signing secret.

The signed input is:

```text theme={null}
{timestamp}.{rawBody}
```

The signature is an HMAC SHA-256 hex digest with a `v1=` prefix.

Example in Node.js:

```js theme={null}
import crypto from "node:crypto";

export function verifyJobBeaconWebhook({
  rawBody,
  timestamp,
  signature,
  secret,
}) {
  const expected =
    "v1=" +
    crypto
      .createHmac("sha256", secret)
      .update(`${timestamp}.${rawBody}`)
      .digest("hex");

  const signatureBuffer = Buffer.from(signature);
  const expectedBuffer = Buffer.from(expected);

  return (
    signatureBuffer.length === expectedBuffer.length &&
    crypto.timingSafeEqual(signatureBuffer, expectedBuffer)
  );
}
```

Reject requests with an old `JobBeacon-Timestamp`. A 5-minute window is a common choice.

## Payload versions

Your webhook endpoint is pinned to one payload version. New deliveries and test deliveries use that version until you change **Payload version** in Settings.

| Version      | Status  | Notes                                                              |
| ------------ | ------- | ------------------------------------------------------------------ |
| `2026-06-15` | Current | Default for new webhook endpoints.                                 |
| `2026-06-12` | Legacy  | Available for existing integrations that already use this version. |

Both supported versions currently use the same fields. The selected version appears in the `api_version` payload field and the `JobBeacon-Webhook-Version` request header.

## `job.matched` payload

```json theme={null}
{
  "event": "job.matched",
  "event_id": "evt_abc123",
  "api_version": "2026-06-15",
  "created_at": "2026-06-13T09:15:00.000Z",
  "dedupe_key": "job.matched:user_123:job_123",
  "job": {
    "id": "job_123",
    "external_id": "456789",
    "title": "Senior Backend Engineer",
    "location": "Remote",
    "url": "https://company.example/careers/job/456789",
    "first_seen_at": "2026-06-13T09:14:30.000Z",
    "last_seen_at": "2026-06-13T09:14:30.000Z",
    "is_active": true
  },
  "company": {
    "id": "company_123",
    "name": "Acme",
    "slug": "acme"
  },
  "share": {
    "token": "share_token",
    "url": "https://jobbeacon.app/share/share_token",
    "image_url": "https://jobbeacon.app/share/share_token/opengraph-image",
    "twitter_image_url": "https://jobbeacon.app/share/share_token/twitter-image",
    "sharer_name": "alex"
  }
}
```

## `job.matched` fields

| Field         | Type   | Description                                                                              |
| ------------- | ------ | ---------------------------------------------------------------------------------------- |
| `event`       | string | Always `job.matched`.                                                                    |
| `event_id`    | string | Unique ID for this webhook delivery. It also appears in the `JobBeacon-Event-Id` header. |
| `api_version` | string | The selected payload version. It matches the `JobBeacon-Webhook-Version` header.         |
| `created_at`  | string | Time JobBeacon created the payload, in ISO 8601 format.                                  |
| `dedupe_key`  | string | Stable key for this matched job event. It also appears in the `Idempotency-Key` header.  |
| `job`         | object | The matched job.                                                                         |
| `company`     | object | The company that posted the job.                                                         |
| `share`       | object | Public JobBeacon share links and images for the job.                                     |

### `job` object

| Field           | Type           | Description                                                 |
| --------------- | -------------- | ----------------------------------------------------------- |
| `id`            | string         | JobBeacon's ID for the job.                                 |
| `external_id`   | string         | The job ID from the company's careers system.               |
| `title`         | string         | Job title.                                                  |
| `location`      | string or null | Job location, if JobBeacon received one.                    |
| `url`           | string         | Public URL for the job posting.                             |
| `first_seen_at` | string or null | Time JobBeacon first saw the job, in ISO 8601 format.       |
| `last_seen_at`  | string or null | Time JobBeacon last saw the job active, in ISO 8601 format. |
| `is_active`     | boolean        | Whether JobBeacon currently sees the job as active.         |

### `company` object

| Field  | Type   | Description                                       |
| ------ | ------ | ------------------------------------------------- |
| `id`   | string | JobBeacon's ID for the company on your watchlist. |
| `name` | string | Company name.                                     |
| `slug` | string | JobBeacon's company slug.                         |

### `share` object

| Field               | Type   | Description                                     |
| ------------------- | ------ | ----------------------------------------------- |
| `token`             | string | Token for the public JobBeacon share page.      |
| `url`               | string | Public JobBeacon preview page for the job.      |
| `image_url`         | string | Stable Open Graph image URL for the share page. |
| `twitter_image_url` | string | Stable Twitter image URL for the share page.    |
| `sharer_name`       | string | Display name shown on the share page.           |

The image URLs are useful for tools that let you attach or preview images.

## Test payload

Click **Send test** to queue a `webhook.test` event.

The test payload uses the same selected payload version as matched job deliveries.

```json theme={null}
{
  "event": "webhook.test",
  "event_id": "evt_abc123",
  "api_version": "2026-06-15",
  "created_at": "2026-06-13T09:15:00.000Z",
  "dedupe_key": "webhook.test:user_123:delivery_123",
  "test": true,
  "job": {
    "id": "job_test",
    "external_id": "jobbeacon-test",
    "title": "JobBeacon is hiring a Product Engineer",
    "location": "Remote",
    "url": "https://jobbeacon.app/careers/jobbeacon-test",
    "first_seen_at": "2026-06-13T09:15:00.000Z",
    "last_seen_at": "2026-06-13T09:15:00.000Z",
    "is_active": true
  },
  "company": {
    "id": "company_test",
    "name": "JobBeacon",
    "slug": "jobbeacon"
  },
  "share": {
    "token": "jobbeacon-test",
    "url": "https://jobbeacon.app/share/jobbeacon-test",
    "image_url": "https://jobbeacon.app/share/jobbeacon-test/opengraph-image",
    "twitter_image_url": "https://jobbeacon.app/share/jobbeacon-test/twitter-image",
    "sharer_name": "JobBeacon"
  }
}
```

## `webhook.test` fields

The `webhook.test` payload includes the same fields as `job.matched`, plus:

| Field   | Type    | Description                        |
| ------- | ------- | ---------------------------------- |
| `event` | string  | Always `webhook.test`.             |
| `test`  | boolean | Always `true` for test deliveries. |

The `job`, `company`, and `share` objects contain test data. Use them to verify your endpoint, signature check, and field mapping before relying on live matched job deliveries.

You can send one test per minute, up to 25 tests in 24 hours.

## Delivery behavior

JobBeacon waits up to 10 seconds for your endpoint.

A delivery is marked **delivered** when your endpoint returns any `2xx` status.

JobBeacon retries temporary failures, including network errors, `408`, `409`, `425`, `429`, and `5xx` responses.

Retries use backoff and stop after 5 attempts. Other failures are marked **failed**.

JobBeacon follows up to 3 redirects, as long as the final URL still passes endpoint validation.

## Delivery history

The **Recent deliveries** table in Settings shows the latest webhook deliveries.

Delivery history is kept for 30 days.

Common statuses:

| Status      | Meaning                                                               |
| ----------- | --------------------------------------------------------------------- |
| `queued`    | Waiting to be sent                                                    |
| `sending`   | Delivery is in progress                                               |
| `delivered` | Your endpoint returned `2xx`                                          |
| `failed`    | Delivery will not be retried                                          |
| `skipped`   | Delivery was skipped, usually because Pro or the webhook was disabled |

## Rotate or pause webhooks

Click **Rotate secret** if your signing secret may have been exposed.

After rotating, update your endpoint to use the new secret.

Use the **Active** switch to pause delivery without deleting your endpoint.

If your account moves from Pro to Free, webhook delivery pauses. Your endpoint and signing secret stay saved in case you reactivate Pro later.

## Troubleshooting

If a webhook does not arrive:

1. Check that your account is on Pro.
2. Check that the webhook is **Active**.
3. Check that the endpoint uses HTTPS and is publicly reachable.
4. Click **Send test**.
5. Review **Recent deliveries** for response status and error details.
6. Confirm the job was new and matched your filters when JobBeacon first saw it.
