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

# Webhooks

> Receive signed event notifications — event types, HMAC signature verification, delivery retries, auto-disable, and secret rotation

Webhooks push events from Reinx to your own infrastructure as signed HTTPS POSTs — transactions completing, approvals resolving, agents changing state. Use them to keep your systems in sync without polling.

Endpoints are configured per organization in **Settings → Webhooks** in the dashboard, or via the API with a session token (Owner or Admin role required).

## Creating an endpoint

```text theme={null}
POST /v1/webhooks
```

| Field         | Type      | Notes                                                                                                              |
| ------------- | --------- | ------------------------------------------------------------------------------------------------------------------ |
| `name`        | string    | Label shown in the dashboard, 1–100 chars                                                                          |
| `url`         | string    | Must be `https://` and publicly resolvable — private/internal addresses are rejected                               |
| `eventTypes`  | string\[] | One or more event types from the table below, or `["*"]` for all (the wildcard can't be mixed with specific types) |
| `scopeType`   | string    | Optional: `organization` (default), `project`, `team`, or `agent`                                                  |
| `scopeIds`    | string\[] | Required for non-organization scopes — the project/team/agent IDs to receive events for                            |
| `description` | string    | Optional notes, up to 500 chars                                                                                    |

The response includes the endpoint plus a **signing secret** (`whsec_…`). It is shown **once** — store it immediately; it can't be retrieved later, only rotated.

## Event types

| Event                    | Fires when                                   |
| ------------------------ | -------------------------------------------- |
| `transaction.completed`  | An agent payment settles successfully        |
| `transaction.denied`     | A payment is denied by policy or a human     |
| `approval.requested`     | A payment needs human approval               |
| `approval.completed`     | A human approves or denies a pending request |
| `agent.created`          | A new agent is created                       |
| `agent.paused`           | An agent is paused                           |
| `agent.resumed`          | A paused agent is resumed                    |
| `agent.archived`         | An agent is archived (terminal)              |
| `funding.deposited`      | A deposit settles into the org treasury      |
| `billing.payment_failed` | A subscription payment fails                 |

### Scope filtering

An `organization`-scoped endpoint receives every matching event in the org. `project`, `team`, and `agent` scopes deliver only events whose project/team/agent matches one of the endpoint's `scopeIds`. Multiple endpoints can coexist — every endpoint whose scope and event types match receives its own delivery.

## The delivery request

Each delivery is an HTTPS `POST` with these headers:

| Header              | Contents                                                             |
| ------------------- | -------------------------------------------------------------------- |
| `Reinx-Signature`   | `sha256=<hex HMAC>` — see verification below                         |
| `Reinx-Timestamp`   | Unix seconds when the delivery was signed                            |
| `Reinx-Event-Type`  | The event type, e.g. `approval.completed`                            |
| `Reinx-Delivery-Id` | Stable delivery ID — identical across retries; use it to deduplicate |

And a JSON body:

```json theme={null}
{
  "id": "same value as Reinx-Delivery-Id",
  "type": "transaction.completed",
  "created_at": "2026-07-15T18:30:00.000Z",
  "org_id": "org_...",
  "project_id": "...",
  "team_id": "...",
  "agent_id": "...",
  "agent_name": "Reddit Agents",
  "data": { "...": "event-specific fields" }
}
```

`project_id`, `team_id`, `agent_id`, and `agent_name` are present on agent-related events; org-level events (billing, funding) omit them.

## Verifying signatures

Every delivery is signed with your endpoint's secret: `HMAC-SHA256(secret, "<timestamp>.<raw body>")`, hex-encoded, prefixed `sha256=`. Always verify against the **raw request bytes**, before any JSON parsing or re-serialization.

During the 24-hour window after a secret rotation, `Reinx-Signature` carries **two** comma-separated signatures (one per secret) — accept the delivery if any of them matches yours.

```ts theme={null}
import { createHmac, timingSafeEqual } from 'node:crypto';

function verifyReinxWebhook(rawBody: string, headers: Record<string, string>, secret: string): boolean {
  const timestamp = headers['reinx-timestamp'];
  // Replay protection: reject deliveries signed more than 5 minutes ago
  if (!timestamp || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;

  const expected = createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex');

  // During secret rotation the header holds two comma-separated signatures
  return (headers['reinx-signature'] ?? '').split(',').some((part) => {
    const candidate = part.trim().replace(/^sha256=/, '');
    return (
      candidate.length === expected.length &&
      timingSafeEqual(Buffer.from(candidate, 'hex'), Buffer.from(expected, 'hex'))
    );
  });
}
```

Respond with any `2xx` status within **15 seconds** to acknowledge. Anything else — including a timeout — counts as a failure and is retried.

## Retries and auto-disable

Delivery is **at-least-once**: a delivery is attempted up to **5 times**, backing off after each failure — 5 minutes, 30 minutes, 2 hours, then 8 hours. Retries reuse the same `Reinx-Delivery-Id`, so deduplicate on it.

After **10 consecutive failed deliveries**, the endpoint is automatically disabled (`isActive: false`) and its queued deliveries are dropped. Check the endpoint's status and delivery log in **Settings → Webhooks**; fix your receiver, then re-enable the endpoint there (or via `PUT /v1/webhooks/:id`), which resets the failure counter.

## Rotating the secret

```text theme={null}
POST /v1/webhooks/:id/rotate-secret
```

Returns the new secret (again shown once) plus `secretPreviousExpiresAt`. For the next **24 hours** deliveries are signed with **both** the new and old secrets, so you can deploy the new secret to your receiver without dropping events. After the window only the new secret signs.

## Testing an endpoint

```text theme={null}
POST /v1/webhooks/:id/test
```

Sends a synthetic signed event (`data: { "test": true, … }`) to your URL and returns the HTTP status and response time. Test sends appear in the delivery log flagged as tests and never count toward the auto-disable failure threshold.

## Delivery log

```text theme={null}
GET /v1/webhooks/:id/deliveries
```

Recent deliveries with status, HTTP response code, response time, and failure reason — also visible per-endpoint in the dashboard.
