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

# Get Started With Webhooks

> Send Jam events to your own HTTPS endpoint, Zapier workflow, or n8n automation when a new Jam is created.

[Jam webhooks](https://jam.dev/s/settings/webhooks) send HTTP push notifications when a Jam is created. Use them to trigger CI builds, forward bug reports to external systems, post to Slack, file tickets automatically, or start any workflow that should react to a new Jam.

Webhooks are scoped to a Jam workspace. Configure them per workspace to match each team's tooling.

Jam supports the following webhook events:

* `jam.created`: fired when a new Jam is captured from any origin, including the Chrome extension, iOS app, dashboard, Fin (Intercom) integration, or Recording Link. Most integrations should subscribe to this event.
* `intercom.recorder.recorded`: fired when a customer submits a screen recording through Jam's Intercom integration. Use this when tuning Fin AI Agent behavior after a recording arrives mid-conversation.
* `intercom.recorder.opted_out`: fired when a customer declines to record through Jam's Intercom integration. Use this when tuning Fin AI Agent behavior after a customer opts out.

If you are not configuring the Fin AI Agent, you only need `jam.created`.

## Getting started

First, build a webhook consumer for Jam's webhook agent to call. You can deploy your own HTTP server, use a function platform like Netlify Functions, Vercel Functions, or Cloudflare Workers, or test with a service like [RequestBin](https://requestbin.com/). You can also use no-code and low-code tools such as [Zapier](https://zapier.com/) and [n8n](https://n8n.io/).

Once your consumer accepts requests, configure the webhook for your workspace.

### A minimal webhook consumer

<Warning>
  Webhook URLs are unauthenticated by default. Anyone who learns yours can POST to it. Verifying the `svix-signature` header confirms the request actually came from Jam. Skip verification only for throwaway test endpoints.
</Warning>

Jam signs webhook deliveries using the [Standard Webhooks](https://www.standardwebhooks.com/) format, powered by [Svix](https://docs.svix.com/receiving/verifying-payloads/why). The easiest way to verify is the official Svix SDK, which handles signature comparison, timestamp tolerance, and key rotation for you.

Deployed on Netlify, a basic consumer looks like this:

```ts theme={"theme":"css-variables"}
import { Webhook } from "svix";

export default async (request) => {
  const payload = await request.text();
  const headers = Object.fromEntries(request.headers);

  let event;
  try {
    const wh = new Webhook(Netlify.env.get("WEBHOOK_SECRET"));
    event = wh.verify(payload, headers); // throws on invalid signature
  } catch {
    return new Response(null, { status: 400 });
  }

  // Do something with the jam, e.g. post to Slack, file a ticket, kick off a CI run.

  return new Response(null, { status: 200 });
};

export const config = {
  path: "/my-jam-webhook",
};
```

If you can't use the Svix SDK, verify manually. The signature is HMAC-SHA256 over `${svix-id}.${svix-timestamp}.${body}`, base64-encoded, and may include multiple space-separated versions during key rotation:

```ts theme={"theme":"css-variables"}
import { createHmac, timingSafeEqual } from "node:crypto";

export default async (request) => {
  const payload = await request.text();
  const svixId = request.headers.get("svix-id");
  const svixTimestamp = request.headers.get("svix-timestamp");
  const svixSignature = request.headers.get("svix-signature");

  if (!svixId || !svixTimestamp || !svixSignature) {
    return new Response(null, { status: 400 });
  }

  const signed = `${svixId}.${svixTimestamp}.${payload}`;
  const secret = Netlify.env.get("WEBHOOK_SECRET").replace(/^whsec_/, "");
  const expected = createHmac("sha256", Buffer.from(secret, "base64"))
    .update(signed)
    .digest();

  const provided = svixSignature
    .split(" ")
    .filter((s) => s.startsWith("v1,"))
    .map((s) => Buffer.from(s.slice(3), "base64"));

  const valid = provided.some(
    (p) => p.length === expected.length && timingSafeEqual(p, expected),
  );
  if (!valid) {
    return new Response(null, { status: 400 });
  }

  // Handle the jam...

  return new Response(null, { status: 200 });
};

export const config = {
  path: "/my-jam-webhook",
};
```

### Creating a webhook

In the Jam dashboard, go to [**Settings → Webhooks**](https://jam.dev/s/settings/webhooks) and click **Manage** to open the Webhook Portal. All endpoint configuration happens in the portal.

**Manual endpoint setup**

1. In the portal, click **Add Endpoint**.
2. Enter your endpoint URL.
3. Select the events to subscribe to.
4. Confirm creation.

**Intercom Fin connector**

Only relevant if you are tuning Jam's Intercom Fin AI Agent setup.

1. Select **Intercom Fin** from the webhook dropdown.
2. Paste the URL provided by Intercom.
3. Subscribe to `intercom.recorder.recorded` and `intercom.recorder.opted_out`.
4. Confirm creation.

After the endpoint is created, copy its **Signing secret** from the portal (it starts with `whsec_`). Store it as `WEBHOOK_SECRET`, or whatever name fits your stack, and use it to verify incoming requests.

The webhook is enabled by default. Capture a Jam to test it.

### Webhook Portal

The Webhook Portal, also under [**Settings → Webhooks**](https://jam.dev/s/settings/webhooks), gives you operational visibility into every endpoint:

* **Delivery logs** for each attempt, with success and failure status.
* **Replay** for failed deliveries so you can re-fire a payload after fixing your consumer.
* **Signing secret** for verifying request authenticity.
* **Success rate** and delivery statistics over time.
* **Full payload inspection**, including headers and body, for any past delivery.

Use the portal to debug consumer issues and to confirm that retries land cleanly once your endpoint is back online.

## Webhook payload

The payload arrives as JSON over HTTPS with these headers:

```http theme={"theme":"css-variables"}
Accept-Charset: utf-8
Content-Type: application/json; charset=utf-8
svix-id: msg_loFOjxBNrRLzqYUf
svix-timestamp: 1715694798
svix-signature: v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=
```

| HTTP header      | Description                                                                                                                                                     |
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `svix-id`        | Unique ID for this delivery. Use it as an idempotency key on your side.                                                                                         |
| `svix-timestamp` | Unix timestamp (seconds) when the delivery was signed.                                                                                                          |
| `svix-signature` | One or more space-separated `v1,<base64-hmac>` signatures of `${svix-id}.${svix-timestamp}.${body}`. Multiple versions are present during signing-key rotation. |

The event name is not sent as a header. Read it from your subscription config or inspect the payload shape directly.

### `jam.created` body

| Field           | Type    | Description                                                                                                                                                                                                                                  |
| --------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `jamId`         | string  | Unique ID of the Jam.                                                                                                                                                                                                                        |
| `jamUrl`        | string  | Shareable URL where the Jam can be viewed.                                                                                                                                                                                                   |
| `teamId`        | string  | ID of the workspace the Jam belongs to.                                                                                                                                                                                                      |
| `type`          | string  | One of `video`, `screenshot`, `sessionReplay`.                                                                                                                                                                                               |
| `createdAt`     | string  | ISO 8601 timestamp when the Jam was created.                                                                                                                                                                                                 |
| `title`         | string  | Jam title. May be absent.                                                                                                                                                                                                                    |
| `description`   | string  | Jam description. May be absent.                                                                                                                                                                                                              |
| `originalUrl`   | string  | Website URL where the Jam was captured. May be absent.                                                                                                                                                                                       |
| `origin`        | string  | Capture source, such as `recording_link`, `extension_chrome`, `mobile_ios`, `dashboard`, or `csup_intercom`. May be absent.                                                                                                                  |
| `isIncognito`   | boolean | Whether the Jam was captured in an incognito or private session. May be absent.                                                                                                                                                              |
| `author`        | object  | The user who captured the Jam. Includes `email` and may include `name`.                                                                                                                                                                      |
| `media`         | object  | URLs for the captured media. Includes `videoUrl`, `screenshotUrl`, and `thumbnailUrl` where applicable.                                                                                                                                      |
| `systemInfo`    | object  | Capture environment. Includes `browser` (`name`, `version`), `os` (`name`, `version`), and `screen` (`width`, `height`) where applicable.                                                                                                    |
| `recordingLink` | object  | Present only when the Jam was captured via a Recording Link. Includes `publicId`, `type` (`one_time` or `reusable`), and may include `recordingUrl`, `description`, and `reference`. Use `reference` to tie the Jam back to your own system. |

Required fields: `jamId`, `jamUrl`, `teamId`, `type`, `createdAt`, `author`, `media`, `systemInfo`.

### Example payload

```json theme={"theme":"css-variables"}
{
  "jamId": "2174add1-f7c8-44e3-bbf3-2d60b5ea8bc9",
  "jamUrl": "https://jam.dev/c/2174add1-f7c8-44e3-bbf3-2d60b5ea8bc9",
  "teamId": "dc844923-f9a4-40a3-825c-dea7747e57d6",
  "type": "video",
  "createdAt": "2026-05-14T12:53:18.084Z",
  "title": "Checkout button does nothing on Safari",
  "description": "Repro: add item to cart, hit checkout. Console shows a TypeError.",
  "originalUrl": "https://example.com/cart",
  "origin": "recording_link",
  "author": {
    "email": "alex@example.com",
    "name": "Alex Smith"
  },
  "media": {
    "videoUrl": "https://media.jam.dev/.../video.mp4",
    "thumbnailUrl": "https://media.jam.dev/.../thumb.jpg"
  },
  "systemInfo": {
    "browser": { "name": "Safari", "version": "17.4" },
    "os": { "name": "macOS", "version": "14.4.1" },
    "screen": { "width": 1512, "height": 982 }
  },
  "recordingLink": {
    "publicId": "aB3kZ4p",
    "type": "reusable",
    "recordingUrl": "https://example.com/cart",
    "description": "Checkout repro request",
    "reference": "ZD-48217"
  }
}
```

## FAQ

<AccordionGroup>
  <Accordion title="How does a Webhook work?">
    A webhook push is an `HTTP POST` request sent to a URL you choose. Jam triggers it automatically when a Jam is created.

    Your webhook consumer is an HTTP endpoint. It must:

    * Be available at a publicly accessible HTTPS, non-localhost URL.
    * Respond to the push with `HTTP 200` ("OK").

    Each message is attempted based on the following schedule, where each period starts after the preceding attempt fails:

    * Immediately
    * 5 seconds
    * 5 minutes
    * 30 minutes
    * 2 hours
    * 5 hours
    * 10 hours
    * 10 hours, in addition to the previous attempt

    If an endpoint is removed or disabled, delivery attempts to the endpoint are disabled as well.

    For example, an attempt that fails three times before succeeding is delivered roughly 35 minutes and 5 seconds after the first attempt.
  </Accordion>

  <Accordion />
</AccordionGroup>
