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

> ## Agent Instructions
> If you are an AI assistant operating Infinite for a user, read the skill first: https://docs.infinite.dev/agent-skill.md (setup per host, tool reference, rules for moving money, journeys).
> When operating Infinite for a user, use the MCP server. Sign in at https://bot.api.infinite.dev/mcp (production, acts as the user) or send `Authorization: Bearer <key>` to https://bot.api.infinite.dev/mcp/sse. Prefer its tools over raw HTTP; use REST only for steps no tool covers, and check `get_endpoint_schema` before guessing a request shape.
> Net-new Infinite Connect (org_connect) is operator-assisted: do not grant end users shared org_connect Auth Kit membership or the shared platform org API key — those credentials are org-wide on /v1. BYO org keys onboard under that customer's org id. Operator sessions should show org_connect in get_organization_info.
> The key decides the tools. Every organization key (`sk_sandbox_…` / `sk_live_…`) gets docs, onboarding, `get_account_balance`, and request logs for your own organization. BYO organizations add payees an agent will pay with `create_payee` (payable once ACTIVE). Platform Connect sessions instead get bound-customer payment lookups (`get_quote`, `get_transfer`, `list_transfers`, `list_account_balances`), payee drafts (`create_payee_draft` / `get_payee_draft`; screening only, never payable), and transfer drafts that wait for Infinite approval (`create_transfer_draft` / `get_transfer_draft`; never POST /v1/transfers). Where offered, platform `create_transfer` files a real transfer that Infinite holds in review before funding; it cannot be submitted from DRAFT.
> An agent key (`sk_sandbox_agent_…` / `sk_live_agent_…`) moves money within that agent's limits: `create_transfer` → `submit_transfer` → `wait_for_transfer`.
> On sandbox organization-key sessions, `sandbox_approve_customer` (makes the capabilities you pass READY, and related ones may turn READY too; `request_capabilities` is not needed), `sandbox_fund_account`, and `sandbox_complete_transfer` stand in for verification, funding, and settlement, so a whole flow runs from MCP. BYO keys can pass `customerId` on each call without binding; platform sessions must bind a customer first (`create_business_customer` auto-binds, or send `X-Infinite-On-Behalf-Of`).
> When searching documentation without a key, use the docs MCP at https://docs.infinite.dev/mcp.
> Start in sandbox (https://sandbox.api.infinite.dev). Do not use production unless the user explicitly asks.
> Confirm amount, currency, and payee with the user before any payment is submitted. Pass the invoice number as `reference` on `create_transfer`; the summary says what the payee will see.
> REST amounts are integer minor units (`"4000"` = 40.00 USD). MCP payment tools take exact decimal strings (`"40.00"`). Never mix them.
> Agent denials return `403 { denial: { code, message, window, limit, used, remaining, attemptedAmount, currency }, hint }` with decimal amounts; per-transfer denials omit `used` and `remaining`. Report them to the user; do not work around them.

# Webhooks

> Learn how to receive real-time notifications about events in your Infinite account

Infinite delivers webhooks as signed HTTPS POST requests with automatic retries. Webhooks allow you to receive real-time notifications about events that occur in your account, such as when a customer is created, an invoice is paid, or a transfer is completed.

## Overview

Webhooks are HTTP POST requests sent to your server when specific events occur. Each webhook event includes:

* **Event ID**: Unique identifier for idempotency
* **Event Type**: The type of event (e.g., `customer.created`, `invoice.paid`)
* **Timestamp**: When the event occurred
* **Organization ID**: The organization that triggered the event
* **Data**: The event payload with relevant information

## Event Catalog

Browse every available event type — with descriptions and sample payloads — in the **Event Catalog** in your console under **Developer → Webhooks**. Events are grouped by domain:

* **Customer Events**: `customer.created`, `customer.updated`, `customer.verified`, `customer.status_changed`, `customer.deleted`, and more
* **Transfer Events**: `transfer.created`, `transfer.processing`, `transfer.updated`, `transfer.completed`, `transfer.failed`, `transfer.cancelled`, `transfer.refund_initiated`, `transfer.refund_completed`, `transfer.reversed`
* **Invoice Events**: `invoice.created`, `invoice.sent`, `invoice.paid`, `invoice.cancelled`, `invoice.overdue`
* **Verification Events**: `verification.session.created`, `verification.session.submitted`, `verification.session.completed`, `verification.session.failed`, `verification.link.sent`, `verification.session.expired`
* **FX Events**: `fx_trade.created`, `fx_trade.completed`, `fx_quote.expired`, and more
* **Agent Events**: `agent.created`, `agent.updated`, `agent.limit_reached`, `agent.revoked` — `transfer.*` payloads also carry `agentId` when an agent key made the transfer

For transfers where funds have already been received, operational issues are sent as `transfer.updated` with `data.status` set to `IN_REVIEW` while Infinite reviews or resolves the transfer. `transfer.failed` is only sent when the transfer fails before funds are received.

## Setting Up Webhooks

### 1. Create a Webhook Endpoint

Use the [Create Webhook Endpoint](/api-reference/webhooks/create-a-new-webhook-endpoint) API to register your webhook URL:

<CodeGroup>
  ```python Python theme={null}
  import requests

  response = requests.post(
      'https://api.infinite.dev/v1/webhooks/endpoints',
      headers={
          'Authorization': 'Bearer YOUR_API_KEY',
          'Content-Type': 'application/json',
      },
      json={
          'url': 'https://your-server.com/webhooks',
          'description': 'Production webhook endpoint',
          'filterTypes': ['customer.created', 'invoice.paid'],
      },
  )
  ```

  ```typescript TypeScript theme={null}
  const response = await fetch('https://api.infinite.dev/v1/webhooks/endpoints', {
    method: 'POST',
    headers: {
      Authorization: 'Bearer YOUR_API_KEY',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      url: 'https://your-server.com/webhooks',
      description: 'Production webhook endpoint',
      filterTypes: ['customer.created', 'invoice.paid'],
    }),
  });
  ```

  ```bash CLI theme={null}
  export INFINITE_API_KEY=YOUR_API_KEY
  infinite api POST /v1/webhooks/endpoints --body '{
    "url": "https://your-server.com/webhooks",
    "description": "Production webhook endpoint",
    "filterTypes": ["customer.created", "invoice.paid"]
  }'
  ```

  ```text MCP theme={null}
  Create a webhook endpoint at https://your-server.com/webhooks for customer.created and invoice.paid events, described as "Production webhook endpoint".

  # Infinite MCP is read-only today — run the CLI tab to execute.
  ```

  ```bash cURL theme={null}
  curl -X POST https://api.infinite.dev/v1/webhooks/endpoints \
    -H "Authorization: Bearer YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "url": "https://your-server.com/webhooks",
      "description": "Production webhook endpoint",
      "filterTypes": ["customer.created", "invoice.paid"]
    }'
  ```
</CodeGroup>

### 2. Configure Event Filtering

You can specify which event types to receive using the `filterTypes` array. Omit it (or pass an empty array) to receive all event types, including new ones as they launch. To receive only specific events:

```json theme={null}
{
  "url": "https://your-server.com/webhooks",
  "description": "Customer events only",
  "filterTypes": ["customer.created", "customer.updated", "customer.verified"]
}
```

### 3. Manage Endpoints

* **List endpoints**: `GET /v1/webhooks/endpoints`
* **Update endpoint**: `PUT /v1/webhooks/endpoints/{endpointId}` (supports enabling/disabling with `disabled`)
* **Delete endpoint**: `DELETE /v1/webhooks/endpoints/{endpointId}`
* **Get signing secret**: `GET /v1/webhooks/endpoints/{endpointId}/secret`
* **Rotate signing secret**: `POST /v1/webhooks/endpoints/{endpointId}/secret/rotate` — the previous secret stays valid for 24 hours so you can roll over without dropping deliveries

You can also manage endpoints in the console under **Developer → Webhooks**.

## Webhook Payload Structure

All webhook events follow this structure:

```json theme={null}
{
  "id": "evt_1234567890abcdef",
  "timestamp": "2026-09-15T10:30:00Z",
  "apiVersion": "1.0",
  "organizationId": "org_abc123",
  "type": "customer.created",
  "data": {
    // Event-specific data
  }
}
```

### Example: Customer Created Event

```json theme={null}
{
  "id": "evt_1234567890abcdef",
  "timestamp": "2026-09-15T10:30:00Z",
  "apiVersion": "1.0",
  "organizationId": "org_abc123",
  "type": "customer.created",
  "data": {
    "id": "cus_1234567890",
    "customerName": "Acme Corporation",
    "email": "contact@acme.com",
    "type": "BUSINESS",
    "status": "PENDING",
    "countryCode": "US",
    "createdAt": "2026-09-15T10:30:00Z",
    "updatedAt": "2026-09-15T10:30:00Z"
  }
}
```

### Example: Invoice Paid Event

```json theme={null}
{
  "id": "evt_9876543210fedcba",
  "timestamp": "2026-09-15T11:45:00Z",
  "apiVersion": "1.0",
  "organizationId": "org_abc123",
  "type": "invoice.paid",
  "data": {
    "id": "inv_1234567890",
    "customerId": "cus_1234567890",
    "amount": 100000,
    "currency": "USD",
    "status": "PAID",
    "paidAt": "2026-09-15T11:45:00Z",
    "createdAt": "2026-09-01T10:00:00Z"
  }
}
```

## Verifying Webhooks

Every delivery is signed following the [Standard Webhooks](https://www.standardwebhooks.com/) specification and includes `webhook-id`, `webhook-timestamp`, and `webhook-signature` headers. Verify these signatures to ensure the webhook is legitimate.

Retrieve your endpoint's signing secret (`whsec_...`) with the [Get Signing Secret](/api-reference/webhooks/get-webhook-endpoint-signing-secret) API or from the console under **Developer → Webhooks → Signing secret**.

### Using a Verification Library

The easiest way to verify webhooks is with a Standard Webhooks library:

**Node.js:**

```javascript theme={null}
import { Webhook } from 'standardwebhooks';

const webhook = new Webhook(process.env.WEBHOOK_SIGNING_SECRET);

// Throws if the signature is invalid
const payload = webhook.verify(req.body, {
  'webhook-id': req.headers['webhook-id'],
  'webhook-timestamp': req.headers['webhook-timestamp'],
  'webhook-signature': req.headers['webhook-signature'],
});
```

**Python:**

```python theme={null}
from standardwebhooks import Webhook

webhook = Webhook(webhook_secret)

# Raises if the signature is invalid
payload = webhook.verify(
    data=request.body,
    headers={
        'webhook-id': request.headers['webhook-id'],
        'webhook-timestamp': request.headers['webhook-timestamp'],
        'webhook-signature': request.headers['webhook-signature'],
    }
)
```

### Manual Verification

To verify manually, compute the signed content as `{webhook-id}.{webhook-timestamp}.{raw request body}`, HMAC-SHA256 it with the base64-decoded portion of your signing secret (the part after `whsec_`), and base64-encode the result. Compare it against the `v1,` entries in the `webhook-signature` header using a constant-time comparison, and reject deliveries whose timestamp is more than a few minutes old to prevent replay attacks.

## Delivery History & Replay

Inspect and replay deliveries programmatically:

* **List messages**: `GET /v1/webhooks/messages` — newest first, with cursor pagination. Filter by `endpointId` (adds per-endpoint delivery `status`), `eventTypes`, `tag` (resource IDs such as a transfer or customer ID), and time range.
* **Get a message**: `GET /v1/webhooks/messages/{messageId}` — includes the payload and every delivery attempt with HTTP status and response body. The message ID matches the `webhook-id` header on the delivery.
* **Resend a message**: `POST /v1/webhooks/messages/{messageId}/resend` — redelivers the original payload to every enabled endpoint subscribed to the event type, or a single endpoint via `endpointId`.
* **Replay failed deliveries**: `POST /v1/webhooks/endpoints/{endpointId}/recover` — redelivers everything that failed since a timestamp. Useful after an outage on your receiver.

<CodeGroup>
  ```python Python theme={null}
  import requests

  # Replay all failed deliveries from the last 24 hours
  response = requests.post(
      'https://api.infinite.dev/v1/webhooks/endpoints/ep_123/recover',
      headers={
          'Authorization': 'Bearer YOUR_API_KEY',
          'Content-Type': 'application/json',
      },
      json={'since': '2026-08-16T14:00:00Z'},
  )
  ```

  ```typescript TypeScript theme={null}
  // Replay all failed deliveries from the last 24 hours
  const response = await fetch(
    'https://api.infinite.dev/v1/webhooks/endpoints/ep_123/recover',
    {
      method: 'POST',
      headers: {
        Authorization: 'Bearer YOUR_API_KEY',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ since: '2026-08-16T14:00:00Z' }),
    },
  );
  ```

  ```bash CLI theme={null}
  export INFINITE_API_KEY=YOUR_API_KEY
  # Replay all failed deliveries from the last 24 hours
  infinite api POST /v1/webhooks/endpoints/ep_123/recover --body '{
    "since": "2026-08-16T14:00:00Z"
  }'
  ```

  ```text MCP theme={null}
  Replay failed webhook deliveries for endpoint ep_123 since 2026-08-16T14:00:00Z.

  # Infinite MCP is read-only today — run the CLI tab to execute.
  ```

  ```bash cURL theme={null}
  # Replay all failed deliveries from the last 24 hours
  curl -X POST https://api.infinite.dev/v1/webhooks/endpoints/ep_123/recover \
    -H "Authorization: Bearer YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{ "since": "2026-08-16T14:00:00Z" }'
  ```
</CodeGroup>

Message history and payloads are retained for 90 days. The same delivery feed — with filters, payload inspection, and one-click resend — is available in the console under **Developer → Webhooks → Deliveries**.

## Best Practices

### Idempotency

All webhook events include an `id` field, and replays redeliver the original event unchanged. Use the ID to implement idempotency in your webhook handler:

```javascript theme={null}
async function handleWebhook(event) {
  // Check if we've already processed this event
  const existing = await db.findEventById(event.id);
  if (existing) {
    return; // Already processed
  }

  // Process the event
  await processEvent(event);

  // Store the event ID
  await db.saveEventId(event.id);
}
```

### Retry Logic

Infinite automatically retries failed webhook deliveries with exponential backoff. Your endpoint should:

* Return `200 OK` immediately after receiving the webhook
* Process the event asynchronously
* Handle errors gracefully

### Security

* Always verify webhook signatures
* Use HTTPS for your webhook endpoints
* Store signing secrets securely (environment variables, secrets manager)
* Monitor your delivery history for suspicious activity

### Error Handling

If your endpoint returns a non-2xx status code, the delivery is retried. If you want to acknowledge receipt but process later:

```javascript theme={null}
app.post('/webhooks', async (req, res) => {
  // Return 200 immediately
  res.status(200).send({ received: true });

  // Process asynchronously
  await processWebhookAsync(req.body);
});
```

## Testing Webhooks

Send an example payload of any event type to an endpoint from the console: **Developer → Webhooks → Endpoints → Send test event**. The delivery shows up in the Deliveries feed like any other event, including the response your server returned.

## Need Help?

* Browse the **Event Catalog** in your console for all available events and sample payloads
* Review the [API Reference](/api-reference/webhooks) for endpoint details


## Related topics

- [Delete a webhook endpoint](/api-reference/webhooks/delete-a-webhook-endpoint.md)
- [Get a webhook message](/api-reference/webhooks/get-a-webhook-message.md)
- [Update a webhook endpoint](/api-reference/webhooks/update-a-webhook-endpoint.md)
- [Resend a webhook message](/api-reference/webhooks/resend-a-webhook-message.md)
- [List webhook messages](/api-reference/webhooks/list-webhook-messages.md)
