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

# API keys and webhooks

> Create API keys in FoxReach, call the REST API with the X-API-Key header, add a webhook, verify its HMAC signature, and read the delivery log when it fails.

FoxReach has two doors for other software. API keys let your code, your CLI, or an automation platform pull data out and push changes in. Webhooks go the other way: FoxReach calls a URL you own the moment a reply lands, a campaign starts, or a lead changes.

By the end of this guide you will have created a scoped API key, made a request to the REST API, added a webhook that listens for replies, sent a test event to it, verified the signature on your side, and read the delivery log to see what FoxReach sent and what your endpoint answered.

<Frame caption="API Keys and Webhooks in FoxReach (1 min)">
  <iframe width="100%" style={{ aspectRatio: "16 / 9" }} src="https://www.youtube-nocookie.com/embed/rpmxdyYhzs8?rel=0" title="API Keys and Webhooks in FoxReach (1 min)" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowFullScreen referrerPolicy="strict-origin-when-cross-origin" />
</Frame>

## Before you start

Everything here lives under Settings > Integrations, and two rules apply throughout. API keys are workspace-scoped: a key belongs to the workspace you create it in, and every request made with it acts inside that workspace only, so two client workspaces need two keys (the [workspaces guide](/docs/product/workspaces) explains the model). And creating or revoking keys needs the owner or admin role.

The screenshots come from a demo workspace called FoxReach Academy Demo. If you are brand new, the [getting started guide](/docs/get-started/overview) gets a first campaign running; come back here when you want to connect FoxReach to something else.

## Step 1: Open Settings > Integrations > API Keys

Click **Settings** in the sidebar, then the **Integrations** tab. It is a stack of collapsible sections: API Keys, MCP Integration, Tracking Domain, Webhooks, and Event Log. Expand **API Keys**.

A banner titled API Key Authentication states the two facts that matter: keys authenticate requests to `/api/v1/` endpoints, and you send the key in the `X-API-Key` header. Below it is the list of keys in this workspace. Each card shows the key's name, its prefix followed by three dots, a badge per scope, the creation date, and when it was last used.

<Frame caption="Settings > Integrations in FoxReach with the API Keys section expanded, showing the HubSpot sync key with its prefix, scopes, and created date">
  <img src="https://mintcdn.com/buildberg/ad05tqpp_PsY593Y/images/guides/api-keys-and-webhooks/api-keys.webp?fit=max&auto=format&n=ad05tqpp_PsY593Y&q=85&s=f04f709dd9cbda5637d4cde07ca876a4" alt="Settings > Integrations in FoxReach with the API Keys section expanded, showing the HubSpot sync key with its prefix, scopes, and created date" data-og-width="2000" width="2000" data-og-height="1125" height="1125" data-path="images/guides/api-keys-and-webhooks/api-keys.webp" data-optimize="true" data-opv="3" srcset="https://mintcdn.com/buildberg/ad05tqpp_PsY593Y/images/guides/api-keys-and-webhooks/api-keys.webp?w=280&fit=max&auto=format&n=ad05tqpp_PsY593Y&q=85&s=3267969e85c79616d84e65426af30f5d 280w, https://mintcdn.com/buildberg/ad05tqpp_PsY593Y/images/guides/api-keys-and-webhooks/api-keys.webp?w=560&fit=max&auto=format&n=ad05tqpp_PsY593Y&q=85&s=a655e36c63bb83985778c32b983de384 560w, https://mintcdn.com/buildberg/ad05tqpp_PsY593Y/images/guides/api-keys-and-webhooks/api-keys.webp?w=840&fit=max&auto=format&n=ad05tqpp_PsY593Y&q=85&s=d195e44721511258e0d29a8d83796b41 840w, https://mintcdn.com/buildberg/ad05tqpp_PsY593Y/images/guides/api-keys-and-webhooks/api-keys.webp?w=1100&fit=max&auto=format&n=ad05tqpp_PsY593Y&q=85&s=61276c4ce5775a2a3349b89f2b70c76f 1100w, https://mintcdn.com/buildberg/ad05tqpp_PsY593Y/images/guides/api-keys-and-webhooks/api-keys.webp?w=1650&fit=max&auto=format&n=ad05tqpp_PsY593Y&q=85&s=c6ac8995cee679c4fef13e7aa1c22748 1650w, https://mintcdn.com/buildberg/ad05tqpp_PsY593Y/images/guides/api-keys-and-webhooks/api-keys.webp?w=2500&fit=max&auto=format&n=ad05tqpp_PsY593Y&q=85&s=a8384be62850f1606a679cbc52ba1357 2500w" />
</Frame>

The demo workspace already has one key, named HubSpot sync. That is the convention worth copying: one key per integration, named after the tool that holds it, so you can revoke one without breaking the others.

## Step 2: Create a key and copy it once

Click **Create Key**. The Create API Key dialog has two fields.

**Key Name.** A label so you can tell keys apart later. The placeholder suggests My Integration; type something specific, like "n8n production" or "Claude Code on my laptop".

**Scopes.** Two toggles, read and write, both selected by default. The dialog's own definition is the one to remember: Read lets the key list and view resources. Write lets it create, update, and delete them. A reporting dashboard only needs read; an n8n workflow that creates leads and starts campaigns needs write too. At least one is required.

Click **Create Key**. The dialog switches to API Key Created with a warning: Save your API key now, because this is the only time the full key will be shown. The key starts with `otr_`. Use the copy button, paste the key into your secret store or the credential field of the tool that will use it, and only then click **Done**.

<Frame caption="The API Key Created dialog in FoxReach showing the full HubSpot sync key once, with the copy button and the warning that it will not be shown again">
  <img src="https://mintcdn.com/buildberg/ad05tqpp_PsY593Y/images/guides/api-keys-and-webhooks/create-api-key.webp?fit=max&auto=format&n=ad05tqpp_PsY593Y&q=85&s=8e137e2d51ab66d174ea06fe79b512e4" alt="The API Key Created dialog in FoxReach showing the full HubSpot sync key once, with the copy button and the warning that it will not be shown again" width="2000" height="1125" data-path="images/guides/api-keys-and-webhooks/create-api-key.webp" />
</Frame>

Back in the list, the new key shows only its prefix. If you lose the full key, there is no way to reveal it again; revoke it and create a new one.

<Tip>
  Keys never expire on their own. Treat them like passwords: server-side only, one per tool, revoked the day the tool is retired.
</Tip>

## Step 3: Make your first request

The base URL is `https://api.foxreach.io/api/v1`, and every request needs the `X-API-Key` header. This call lists the active campaigns in the key's workspace, five per page:

```bash theme={null}
curl "https://api.foxreach.io/api/v1/campaigns?status=active&pageSize=5" \
  -H "X-API-Key: otr_your_key_here"
```

`GET /leads` works the same way and accepts `page`, `pageSize`, `search`, `status`, and a comma-separated `tags` filter:

```bash theme={null}
curl "https://api.foxreach.io/api/v1/leads?tags=demo&pageSize=50" \
  -H "X-API-Key: otr_your_key_here"
```

**The response envelope.** Every successful response wraps its result in a `data` field. List endpoints add a `meta` object with the page, page size, total count, and total pages:

```json theme={null}
{
  "data": [],
  "meta": {
    "page": 1,
    "pageSize": 5,
    "total": 12,
    "totalPages": 3
  }
}
```

Errors use a different envelope: an `error` message, a `code`, and an optional `details` object. A missing, invalid, revoked, or expired key returns 401. A key without the scope an endpoint needs (a read-only key calling `POST /leads`, say) returns 403.

**Rate limits.** Each key can make 100 requests per minute. Every response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset` headers, and going over returns 429 with a `Retry-After` header telling you how many seconds to wait. The official SDKs and the CLI read that header and retry for you.

**The full reference.** The public OpenAPI document for the v1 surface is served at `https://api.foxreach.io/openapi-public.json`, and the rendered reference is at [foxreach.io/docs](/docs/get-started/overview). The same key works unchanged in the [Python SDK](https://www.foxreach.io/blog/python-sdk-guide), the [TypeScript SDK](https://www.foxreach.io/blog/typescript-sdk-guide), the [CLI](https://www.foxreach.io/blog/cli-guide), and the [Claude Code plugin](/docs/integrations/claude-code-plugin).

**Revoking a key.** Each card in the API Keys list has a trash icon titled Revoke key. The confirmation says exactly what happens: any integration still sending this key stops working immediately, and the key cannot be restored. Create the replacement first, update the caller, then revoke the old one.

## Step 4: Add a webhook and pick its events

Still on the Integrations tab, expand **Webhooks** and click **Add Webhook**. A sheet titled Add Webhook slides in with two parts.

**Endpoint URL.** The HTTPS address FoxReach should POST to. In the video it is `https://hooks.brightlane.co/foxreach`; the demo workspace also has an older webhook at `https://automation.buildberg.io/hooks/foxreach`. It can be an [n8n Webhook node](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.webhook/), a [Zapier catch hook](https://zapier.com/apps/webhook/integrations), a Make custom webhook, or a route in your own service.

**Events to subscribe.** The event types are grouped as Email, Reply, Campaign, and Lead. Click a group's checkbox to take all of its events, or click individual pills. Most teams start with `reply.received`, which fires when a lead writes back, and `reply.categorized`, which fires once FoxReach has classified that reply. The webhook in the screenshot also listens for `campaign.started`.

<Frame caption="The Add Webhook sheet in FoxReach with the endpoint URL filled in and the reply.received, reply.categorized, and campaign.started events selected">
  <img src="https://mintcdn.com/buildberg/ad05tqpp_PsY593Y/images/guides/api-keys-and-webhooks/webhook-form.webp?fit=max&auto=format&n=ad05tqpp_PsY593Y&q=85&s=3d794b3fc42eb3864cf513dccb98c89a" alt="The Add Webhook sheet in FoxReach with the endpoint URL filled in and the reply.received, reply.categorized, and campaign.started events selected" width="2000" height="1125" data-path="images/guides/api-keys-and-webhooks/webhook-form.webp" />
</Frame>

Click **Create**. The button stays disabled until the URL is filled in and at least one event is selected.

<Tip>
  Subscribe narrowly. A webhook on every Email event in a busy workspace receives a call per send. Start with the Reply group and add more when you have a consumer for them.
</Tip>

## Step 5: Save the signing secret and send a test

A Webhook Created dialog appears at once: Save your webhook secret. This is the only time the full secret will be shown. It is a 64-character hex string. Copy it into the same secret store as your API key and click **Done**.

The secret is what makes the webhook trustworthy. Anyone who discovers your URL can POST JSON to it, but only FoxReach can produce a valid signature. After this dialog the card shows the secret masked down to its last characters, enough to confirm which one you hold.

The new webhook sits at the top of the list as a card with an Active badge, its URL, the number of events, the masked secret, and icon buttons titled Test, Disable, Edit, Delete, and Deliveries.

Click **Test**. FoxReach immediately sends a signed event of type `webhook.test` with the payload `{"message": "This is a test webhook delivery."}` and waits up to ten seconds. If your endpoint answers with any 2xx status, a green line reads Test successful followed by the status code. Anything else shows Test failed with the status or the connection error.

<Tip>
  The test is synchronous and skips the delivery queue, so it never appears under Deliveries. Use it to check reachability and your signature code, then watch Deliveries for real events.
</Tip>

## Step 6: Read the Deliveries log

Click **Deliveries** on a webhook card to expand its Recent Deliveries panel. Each row is one event FoxReach tried to deliver: the delivery status, the event type, the HTTP status your endpoint returned, the attempt number, and the timestamp. Ten rows show at a time, with Previous and Next when there are more.

<Frame caption="The Recent Deliveries panel for a webhook in FoxReach listing reply.received deliveries with their HTTP status codes and attempt counts">
  <img src="https://mintcdn.com/buildberg/ad05tqpp_PsY593Y/images/guides/api-keys-and-webhooks/webhook-deliveries.webp?fit=max&auto=format&n=ad05tqpp_PsY593Y&q=85&s=29fe592f5c34eb906c656b971d9ab00b" alt="The Recent Deliveries panel for a webhook in FoxReach listing reply.received deliveries with their HTTP status codes and attempt counts" width="2000" height="1125" data-path="images/guides/api-keys-and-webhooks/webhook-deliveries.webp" />
</Frame>

This panel is the first place to look when an automation stops firing. On the sending side, FoxReach behaves like this:

* **Timeout.** Each attempt waits ten seconds for a response.
* **Retries.** A non-2xx response or a timeout is retried after 60 seconds, then after 120 seconds: three attempts in total. Pending retries show as pending; after the third failure the row shows as failed.
* **Consecutive failures.** Every failed delivery adds one to the webhook's consecutive failure count, shown as a warning badge on the card. A successful delivery resets it to zero.
* **Auto-disable.** After ten consecutive failed deliveries the webhook is switched to Inactive and stops receiving events. Fix the endpoint, then click **Enable** on the card; re-enabling resets the count.

The Event Log section below Webhooks lists every event the workspace produced, subscribed or not, so you can tell "the event never happened" apart from "the delivery failed".

## Manage webhooks through the API

Everything above is available on the v1 API with the same key, which is how the n8n trigger node and the Zapier and Make apps register themselves when you activate a workflow:

* `GET /webhooks/events` returns the valid event types
* `GET /webhooks` lists the workspace's webhooks
* `POST /webhooks` creates one and returns the plaintext secret in that response only
* `PATCH /webhooks/{id}` updates the URL, events, or active flag
* `DELETE /webhooks/{id}` removes it

A create call needs a key with the write scope:

```bash theme={null}
curl -X POST "https://api.foxreach.io/api/v1/webhooks" \
  -H "X-API-Key: otr_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://automation.buildberg.io/hooks/foxreach",
    "events": ["reply.received", "campaign.started"]
  }'
```

Creating the same URL with the same event set twice returns the existing active webhook instead of a duplicate, so integrations that re-register on every activation do not multiply. The [n8n integration guide](/docs/integrations/n8n) shows the trigger node doing exactly this.

## Verify a webhook signature

Every delivery, including the test, is a POST with `Content-Type: application/json` and three custom headers:

| Header                | Value                                                                 |
| --------------------- | --------------------------------------------------------------------- |
| `X-Webhook-Signature` | HMAC-SHA256 hex digest of the raw body, keyed with the webhook secret |
| `X-Event-Type`        | The event type, for example `reply.received`                          |
| `X-Event-Id`          | The event's unique id                                                 |

The body is a JSON object with `id`, `type`, `payload`, and `timestamp`. The signature is computed over the exact bytes of that body, so read the raw request body before any JSON parsing and compare digests with a constant-time function. HMAC is specified in [RFC 2104](https://www.rfc-editor.org/rfc/rfc2104); in Node the [crypto module](https://nodejs.org/api/crypto.html) has everything you need:

```js theme={null}
import { createHmac, timingSafeEqual } from "node:crypto"

export function verifySignature(rawBody, header, secret) {
  const expected = createHmac("sha256", secret)
    .update(rawBody, "utf8")
    .digest("hex")
  const a = Buffer.from(expected)
  const b = Buffer.from(header || "")
  return a.length === b.length && timingSafeEqual(a, b)
}
```

Call it with the raw body, the value of `X-Webhook-Signature`, and the secret from Step 5. Return 401 if it fails; if it passes, respond 2xx quickly and do the real work afterwards so slow processing does not turn into retries. A `reply.received` payload carries the reply id, campaign id, lead id, sender email, subject, and a body preview, enough to fetch the thread through the API or route the lead in your CRM.

## Events reference

| Event                | Fires when                                                                                                          |
| -------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `email.sent`         | A campaign email was sent from a connected account                                                                  |
| `email.failed`       | A send attempt failed                                                                                               |
| `email.bounced`      | A bounce was detected for a sent email                                                                              |
| `email.opened`       | A tracked open was recorded; needs open tracking on for the campaign, and opens are only tracked on follow-up steps |
| `email.clicked`      | A tracked link was clicked; needs click tracking on. Available through the API only, not in the Add Webhook sheet   |
| `reply.received`     | A lead replied to a campaign email                                                                                  |
| `reply.categorized`  | FoxReach classified a reply; the payload includes the category                                                      |
| `campaign.started`   | A campaign was started                                                                                              |
| `campaign.paused`    | A campaign was paused                                                                                               |
| `campaign.completed` | A campaign finished sending                                                                                         |
| `lead.created`       | A lead was created                                                                                                  |
| `lead.updated`       | A lead's fields were changed                                                                                        |

Events are delivered from a queue that runs every ten seconds, so expect a short delay between the action and the POST.

## Troubleshooting

**401 from the API.** The header is missing, misspelled, or the key is wrong. The header is literally `X-API-Key`, not Authorization, and the value starts with `otr_` with nothing around it. A revoked key also returns 401, so if a key stopped working overnight, check whether it is still listed under API Keys.

**403 from the API.** The key is valid but lacks the scope the endpoint needs. Read-only keys cannot create, update, or delete anything, and scopes cannot be changed on an existing key, so create a new one with write.

**The API returns data, but not the data you expect.** The key belongs to a different workspace than the one open in your browser. Switch to the workspace you meant and create the key there. One key, one workspace.

**Webhook shows Inactive with a failures badge.** The endpoint failed ten deliveries in a row and was disabled automatically. Expand Deliveries to see the HTTP codes it returned, fix the cause, and click Enable.

**Signature mismatch.** Almost always the body was parsed and re-serialized before hashing. Compute the HMAC over the raw bytes exactly as received, hex-encode it, and compare to the header. Also confirm you are using this webhook's secret: every webhook has its own, and deleting and recreating one issues a new secret.

**Test event never arrives.** The test waits ten seconds, so a cold-starting function can time out; run it twice. If the test succeeds but no real events appear under Deliveries, open the Event Log. If the event is not there, the action did not happen in this workspace (replies only arrive for campaigns sending from an email account connected here). If it is there, confirm the webhook is subscribed to that event type and is Active.

## Next step

With a key and a webhook in place, the rest is picking a client. The [n8n integration guide](/docs/integrations/n8n) wires the trigger node to these webhook events and the action node to the same key. For code, the [Python SDK](https://www.foxreach.io/blog/python-sdk-guide) and [TypeScript SDK](https://www.foxreach.io/blog/typescript-sdk-guide) wrap every endpoint above, webhooks included. If you want an AI assistant working in your workspace, the [MCP integration guide](/docs/integrations/mcp-clients) uses Google sign-in rather than an API key, while the [Claude Code plugin guide](/docs/integrations/claude-code-plugin) takes the key-based route. For CRM sync patterns, see [connecting GoHighLevel to FoxReach](/docs/integrations/gohighlevel). Every client and marketplace app is listed on the [integrations page](https://www.foxreach.io/integrations).

## Frequently asked questions

<AccordionGroup>
  <Accordion title="Where do I create an API key in FoxReach?">
    Open Settings, click the Integrations tab, and expand API Keys. Click Create Key, give the key a name, choose the read and write scopes it needs, and click Create Key again. The full key is shown once in the API Key Created dialog. Copy it there, because afterwards the list only shows the first few characters.
  </Accordion>

  <Accordion title="How do I authenticate a request to the FoxReach API?">
    Send the key in the X-API-Key header on every request to [https://api.foxreach.io/api/v1](https://api.foxreach.io/api/v1). There is no Bearer token. A missing, revoked, or expired key returns 401, and a key that lacks the scope an endpoint needs returns 403. Each key belongs to one workspace, so every request acts inside that workspace only.
  </Accordion>

  <Accordion title="How does FoxReach sign webhook deliveries?">
    Each webhook has its own signing secret, shown once when you create it. Every delivery carries an X-Webhook-Signature header containing the HMAC-SHA256 hex digest of the raw request body, computed with that secret. Recompute the digest on your side from the exact bytes you received and compare it to the header before trusting the payload.
  </Accordion>

  <Accordion title="What happens when my webhook endpoint is down?">
    FoxReach waits up to ten seconds for a 2xx response. If it does not get one, it retries after 60 seconds and again after 120 seconds, three attempts in total, then marks the delivery failed. After ten consecutive failed deliveries the webhook is disabled automatically. Fix the endpoint, then click Enable on the webhook to reactivate it.
  </Accordion>

  <Accordion title="Can I create webhooks from code instead of the dashboard?">
    Yes. The v1 API exposes GET, POST, PATCH, and DELETE on /webhooks, plus GET /webhooks/events for the list of event types. This is what the n8n trigger node and the Zapier and Make apps use to subscribe and unsubscribe. Creating the same URL and event set twice returns the existing webhook instead of a duplicate.
  </Accordion>
</AccordionGroup>
