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

# Webhooks

> How webhook delivery works, which events are sent, and what payloads to expect.

## Overview

Use webhooks to receive real-time notifications from Cora when key events happen.

Current outbound event types:

* `payment.success`
* `payment.failed`
* `payment.plan_created` (unified tokenized plan flows and USAePay multi-installment success only)
* `call.completed`

Webhook URLs are configured in the platform under `Settings > Webhooks`.

## Events

* [`payment.success`](/webhooks-payment-success)
* [`payment.failed`](/webhooks-payment-failed)
* [`payment.plan_created`](/webhooks-payment-plan-created)
* [`call.completed`](/webhooks-call-completed)

## How Delivery Works

1. Create a webhook with a destination `url` and `subscribedEvents`.
   You do this in the platform UI at `Settings > Webhooks`.
2. Cora generates and stores a per-webhook secret.
3. When an event happens, Cora builds a standard webhook envelope and sends an HTTP `POST` to your URL.
4. Cora logs every attempt and retries transient failures.

## Request Format

Every delivery is a `POST` with `Content-Type: application/json`.

Headers:

* `X-Webhook-Signature`: HMAC-SHA256 hex digest of the **full JSON** request body (`JSON.stringify` of the whole envelope) signed with your webhook secret
* `X-Webhook-Event`: Event name (for example `payment.success` or `call.completed`)
* `X-Webhook-Timestamp`: ISO-8601 timestamp from the payload
* `X-Webhook-Delivery-Id`: Internal delivery log id
* `X-Webhook-Idempotency-Key`: Stable idempotency key for the event (for `call.completed`, derived from organization and `callId` when present)
* `User-Agent`: `Cora-Webhooks/1.0`

Body envelope:

```json theme={null}
{
  "event": "payment.success",
  "eventCategory": "payments",
  "idempotencyKey": "payment.success:org_123:2026-02-17T18:05:21.841Z",
  "organization": {
    "id": "org_123",
    "name": "Acme Corp"
  },
  "agent": {
    "id": "agent_123",
    "name": "Collections Agent"
  },
  "timestamp": "2026-02-17T18:05:21.841Z",
  "payload": {}
}
```

`eventCategory` is `payments` for payment events and `calls` for `call.completed`. `agent` is only included for agent-level events where an `agentId` exists. `idempotencyKey` format varies by event type; payment events often include a timestamp component in the fallback form.

## Retries, Timeouts, And Failures

* Delivery timeout: `30s`
* Success: any `2xx` response
* Retryable HTTP status codes: `408`, `429`, `500`, `502`, `503`, `504`
* Network failures are retried
* Defaults per webhook:
  * `maxRetries`: `3` (total attempts: 4 including initial attempt)
  * `retryDelayMs`: `5000`
  * `exponentialBackoff`: `true` (multiplier `x5` per retry)

## Verify Signatures

Cora signs the full webhook envelope with your per-webhook secret using HMAC-SHA256. The value in `X-Webhook-Signature` is the lowercase hex digest of the JSON body Cora sends.

Verify the signature against the exact raw request body bytes/string received by your server, before JSON parsing or re-serializing the body. Signing a parsed object, such as `JSON.stringify(req.body)`, can fail if your framework changes whitespace, key order, or formatting.

Example verifier in Node.js:

```ts theme={null}
import crypto from "crypto";

function isValidSignature(rawBody: string | Buffer, signature: string, secret: string) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");

  const receivedBuffer = Buffer.from(signature.trim(), "hex");
  const expectedBuffer = Buffer.from(expected, "hex");

  if (receivedBuffer.length !== expectedBuffer.length) {
    return false;
  }

  return crypto.timingSafeEqual(receivedBuffer, expectedBuffer);
}
```
