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

# Outbound Webhooks

> Register endpoints, queue events, and track delivery attempts for ReconLayer's outbound webhook system.

Outbound webhooks let ReconLayer push event notifications to **your** systems, instead of you polling the API. This is the opposite direction of [Inbound Webhooks](/integrations/webhooks), which covers providers pushing evidence *to* ReconLayer.

The model has three parts:

* **`OutboundWebhookEndpoint`** — a URL you register, with a list of `subscribedEvents` and a signing secret.
* **`OutboundWebhookEvent`** — a queued event with an `eventType` and `payload`, fanned out to every matching endpoint as one or more deliveries.
* **`OutboundWebhookDelivery`** — one delivery attempt record per `(event, endpoint)` pair, tracked through `pending` → `delivered` / `failed` / `retry_scheduled`.

## Registering an endpoint

```bash theme={null}
curl -X POST https://api.reconlayer.com/v1/outbound-webhook-endpoints \
  -H "Authorization: Bearer $RECONLAYER_API_KEY" \
  -H "x-organization-id: $ORG_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Internal ledger sync",
    "url": "https://api.yourcompany.com/hooks/reconlayer",
    "subscribedEvents": ["payment_intent.created"],
    "status": "active"
  }'
```

<ParamField body="name" type="string" required>A human-readable label shown in the dashboard.</ParamField>
<ParamField body="url" type="string" required>Must be a valid URL. ReconLayer will POST event payloads here.</ParamField>

<ParamField body="subscribedEvents" type="string[]" default="[&#x22;payment_intent.created&#x22;]">
  At least one entry. Use `"*"` to subscribe to **all** event types — an endpoint matches an event if `subscribedEvents` contains the event's `eventType` *or* contains `"*"`.
</ParamField>

<ParamField body="status" type="string" default="active">`active` or `disabled`. (`deleted` is a terminal status set by `DELETE`, not settable on create.)</ParamField>

<Warning>
  The response to `POST /v1/outbound-webhook-endpoints` is the **only time** `signingSecret` is returned, in the format `rlwhsec_<32-hex-chars>`. Store it immediately — subsequent reads (`GET`, `PATCH`) only return `hasSigningSecret: true`.
</Warning>

```json theme={null}
{
  "id": "owe_01h...",
  "organizationId": "org_01h...",
  "name": "Internal ledger sync",
  "url": "https://api.yourcompany.com/hooks/reconlayer",
  "subscribedEvents": ["payment_intent.created"],
  "status": "active",
  "hasSigningSecret": true,
  "signingSecret": "rlwhsec_4f1b9c3a7e8d4f0aa2c5e6b7d8f9a0b1",
  "createdAt": "2026-06-14T09:00:00.000Z",
  "updatedAt": "2026-06-14T09:00:00.000Z"
}
```

See [Create an Outbound Webhook Endpoint](/api-reference/outbound-webhooks/create-an-outbound-webhook-endpoint).

## Managing endpoints

```bash theme={null}
# List endpoints (excludes soft-deleted)
curl https://api.reconlayer.com/v1/outbound-webhook-endpoints \
  -H "Authorization: Bearer $RECONLAYER_API_KEY" \
  -H "x-organization-id: $ORG_ID"

# Update subscribed events or status
curl -X PATCH https://api.reconlayer.com/v1/outbound-webhook-endpoints/owe_01h... \
  -H "Authorization: Bearer $RECONLAYER_API_KEY" \
  -H "x-organization-id: $ORG_ID" \
  -H "Content-Type: application/json" \
  -d '{ "subscribedEvents": ["payment_intent.created", "*"], "status": "active" }'

# Soft-delete an endpoint
curl -X DELETE https://api.reconlayer.com/v1/outbound-webhook-endpoints/owe_01h... \
  -H "Authorization: Bearer $RECONLAYER_API_KEY" \
  -H "x-organization-id: $ORG_ID"
```

`PATCH` requires at least one field. `DELETE` performs a **soft delete** — the endpoint's `status` is set to `deleted`, it stops appearing in `GET /v1/outbound-webhook-endpoints`, and it stops matching new events, but its historical events/deliveries remain queryable. Both `PATCH` and `DELETE` return `404 not_found` for an unknown or already-deleted `outboundWebhookEndpointId`.

See [List Outbound Webhook Endpoints](/api-reference/outbound-webhooks/list-outbound-webhook-endpoints), [Update an Outbound Webhook Endpoint](/api-reference/outbound-webhooks/update-an-outbound-webhook-endpoint), and [Delete an Outbound Webhook Endpoint](/api-reference/outbound-webhooks/delete-an-outbound-webhook-endpoint).

## Events ReconLayer emits

<Note>
  `subscribedEvents` accepts **any non-empty string**, so you can use this system for your own custom event types via `POST /v1/outbound-webhook-events` (see below). As of this writing, the platform automatically emits one event type as part of its core flow:
</Note>

| Event type               | Emitted when                                                                                                                        | Payload                                                                                                                |
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `payment_intent.created` | A `PaymentIntent` is created (directly via the API or via a `client_internal_ledger` [file import](/integrations/file-imports) row) | `{ paymentIntentId, caseId, externalReference, sourceAmount, sourceCurrency, destinationAmount, destinationCurrency }` |

This is also the default value of `subscribedEvents` when creating a new endpoint, and the `sourceKey` used for deduplication follows the pattern `payment_intent.created:{paymentIntentId}` (see [Deduplication](#deduplication-with-sourcekey) below).

Subscribe to `"*"` if you want to receive every event type ReconLayer emits now and in the future without updating your endpoint configuration each time new event types are added.

## Queueing your own events

`POST /v1/outbound-webhook-events` lets you (or an internal service) queue an arbitrary event for fan-out to matching endpoints — useful for custom integrations or for replaying a notification.

```bash theme={null}
curl -X POST https://api.reconlayer.com/v1/outbound-webhook-events \
  -H "Authorization: Bearer $RECONLAYER_API_KEY" \
  -H "x-organization-id: $ORG_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "eventType": "payment_intent.created",
    "sourceKey": "payment_intent.created:pi_01h...",
    "payload": { "paymentIntentId": "pi_01h...", "externalReference": "INV-2026-0099" }
  }'
```

<ParamField body="eventType" type="string" required>Any non-empty string. Matched against endpoints' `subscribedEvents`.</ParamField>
<ParamField body="sourceKey" type="string">Optional dedup key, unique per organization. See below.</ParamField>
<ParamField body="payload" type="object" default="{}">Arbitrary JSON delivered to subscribed endpoints.</ParamField>

Response:

```json theme={null}
{
  "outcome": "queued",
  "event": {
    "id": "owev_01h...",
    "organizationId": "org_01h...",
    "eventType": "payment_intent.created",
    "sourceKey": "payment_intent.created:pi_01h...",
    "status": "pending",
    "endpointCount": 1,
    "payload": { "paymentIntentId": "pi_01h...", "externalReference": "INV-2026-0099" },
    "deliveries": [
      {
        "id": "owd_01h...",
        "organizationId": "org_01h...",
        "endpointId": "owe_01h...",
        "endpointName": "Internal ledger sync",
        "endpointUrl": "https://api.yourcompany.com/hooks/reconlayer",
        "attemptCount": 0,
        "status": "pending",
        "responseStatusCode": null,
        "lastError": null,
        "nextRetryAt": null,
        "attemptedAt": null,
        "deliveredAt": null,
        "createdAt": "2026-06-14T09:00:01.000Z",
        "updatedAt": "2026-06-14T09:00:01.000Z"
      }
    ],
    "createdAt": "2026-06-14T09:00:01.000Z",
    "updatedAt": "2026-06-14T09:00:01.000Z"
  }
}
```

### Deduplication with `sourceKey`

If `sourceKey` is provided and an `OutboundWebhookEvent` with the same `(organizationId, sourceKey)` already exists, no new event or deliveries are created — the existing event is returned with `"outcome": "duplicate"`. This makes it safe to call repeatedly from a retried operation.

### Fan-out and status when nothing is subscribed

When `createOutboundWebhookEvent` runs, ReconLayer finds every **active** endpoint whose `subscribedEvents` contains the event's `eventType` *or* contains `"*"`, and creates one `OutboundWebhookDelivery` per matching endpoint, each starting in `pending`. If **no** endpoint matches, the event is created with `status: "skipped"` and zero deliveries — it is recorded for audit purposes but nothing is queued for delivery.

See [Queue an Outbound Webhook Event](/api-reference/outbound-webhooks/queue-an-outbound-webhook-event) and [List Outbound Webhook Events](/api-reference/outbound-webhooks/list-outbound-webhook-events).

## Listing events

```bash theme={null}
curl "https://api.reconlayer.com/v1/outbound-webhook-events?status=pending&limit=25" \
  -H "Authorization: Bearer $RECONLAYER_API_KEY" \
  -H "x-organization-id: $ORG_ID"
```

<ParamField query="limit" type="number" default="25">1-100.</ParamField>

<ParamField query="offset" type="number" default="0" />

<ParamField query="eventType" type="string">Filter by exact `eventType`.</ParamField>
<ParamField query="status" type="string">One of `pending`, `delivered`, `failed`, `skipped`.</ParamField>

Each event embeds its `deliveries[]`, so you can see per-endpoint outcomes without a separate call.

## Recording delivery attempts

`POST /v1/outbound-webhook-deliveries/{outboundWebhookDeliveryId}/attempts` records the outcome of an attempt to deliver an event to an endpoint — for example, from a delivery worker that performs the actual HTTP `POST` (signing the payload with the endpoint's `signingSecret`) and reports back the result.

```bash theme={null}
curl -X POST https://api.reconlayer.com/v1/outbound-webhook-deliveries/owd_01h.../attempts \
  -H "Authorization: Bearer $RECONLAYER_API_KEY" \
  -H "x-organization-id: $ORG_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "delivered",
    "responseStatusCode": 200,
    "responseBody": { "ok": true }
  }'
```

<ParamField body="status" type="string" required>One of `pending`, `retry_scheduled`, `delivered`, `failed`.</ParamField>
<ParamField body="responseStatusCode" type="number">100-599, or `null`.</ParamField>
<ParamField body="responseBody" type="object">Arbitrary JSON, or `null`.</ParamField>
<ParamField body="lastError" type="string">Error description if the attempt failed, or `null`.</ParamField>
<ParamField body="nextRetryAt" type="string">ISO 8601 timestamp for the next retry, if `status: "retry_scheduled"`.</ParamField>

Each call to this endpoint:

* Increments the delivery's `attemptCount`, sets `attemptedAt` to now.
* Sets `deliveredAt` to now if `status: "delivered"`, otherwise clears it.
* If `status` is `"delivered"` and `lastError` was not supplied, clears `lastError`. If `status` is `"retry_scheduled"` and `nextRetryAt` was not supplied, leaves the existing `nextRetryAt` unchanged; for any other status, `nextRetryAt` defaults to `null` unless explicitly provided.

Returns `404 not_found` if the delivery doesn't exist for the organization.

### How the parent event's status is derived

After recording an attempt, ReconLayer recomputes the parent `OutboundWebhookEvent.status` from all of its deliveries:

| Condition                                                                       | Resulting event status |
| ------------------------------------------------------------------------------- | ---------------------- |
| No deliveries at all                                                            | `skipped`              |
| Every delivery is `delivered`                                                   | `delivered`            |
| At least one delivery is `pending` or `retry_scheduled` (and not all delivered) | `pending`              |
| Otherwise (all deliveries are terminal and at least one `failed`)               | `failed`               |

See [Record an Outbound Webhook Delivery Attempt](/api-reference/outbound-webhooks/record-an-outbound-webhook-delivery-attempt).

## Security

Each endpoint has a unique `signingSecret` (`rlwhsec_<32-hex>`), generated once at creation and never re-displayed. Use this secret to verify that a delivery genuinely originated from your registered endpoint configuration and has not been tampered with — store it securely alongside your endpoint's URL.

## Next steps

* [Ingestion Flows](/integrations/ingestion-flows) — `payment_intent.created` events are queued at the same point a `PaymentIntent`/`ReconciliationCase` is created.
* [File Imports](/integrations/file-imports) — `client_internal_ledger` rows that create `PaymentIntent`s also trigger `payment_intent.created` outbound events.
