> ## 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.

# Inbound Webhooks

> How payment providers and on-chain indexers push evidence into ReconLayer, and how to submit evidence directly when no webhook exists.

This page covers **inbound** webhooks — evidence pushed *to* ReconLayer by providers and chain indexers. For the opposite direction (ReconLayer notifying your systems of state changes), see [Outbound Webhooks](/integrations/outbound-webhooks).

<Note>
  Provider webhooks are one of the [three ways evidence enters ReconLayer](/integrations/ingestion-flows#the-three-ways-evidence-enters-reconlayer), alongside [file imports](/integrations/file-imports) and integration polling.
</Note>

## Provider webhooks: Bridge

Today, ReconLayer ships a native inbound webhook integration for [Bridge](https://bridge.xyz). Each Bridge integration you configure gets its own dedicated webhook URL:

```
POST /webhooks/bridge/{integrationKey}
```

`{integrationKey}` is the `integrationKey` you chose when creating the [Bridge integration](/integrations/bridge) (`POST /v1/bridge-integrations`), and it's how ReconLayer resolves which organization and which signing key the incoming payload belongs to — there is no `Authorization` or `x-organization-id` header on this route.

<Warning>
  `POST /webhooks/bridge` (without an `{integrationKey}`) is **intentionally rejected** with `400 invalid_request` and the message *"Use /webhooks/bridge/:integrationKey so the webhook can resolve organization context."* Configure Bridge to send webhooks to the per-integration URL only.
</Warning>

### Signature verification

Bridge signs every webhook with the header:

```
X-Webhook-Signature: t=<unix_timestamp_ms>,v0=<base64_rsa_sha256_signature>
```

ReconLayer verifies this signature against the integration's stored `webhookPublicKey`:

1. Parses `t=` (timestamp, milliseconds) and `v0=` (base64-encoded signature) from the header. Missing or malformed headers fail verification immediately.
2. Rejects the webhook if `|now - t|` exceeds a **10-minute (600,000 ms) replay tolerance window**.
3. Verifies an **RSA-SHA256** signature over the string `${timestamp}.${rawBody}` using the integration's public key.

```mermaid theme={null}
sequenceDiagram
    participant Bridge
    participant ReconLayer as POST /webhooks/bridge/:integrationKey
    Bridge->>ReconLayer: payload + X-Webhook-Signature
    ReconLayer->>ReconLayer: resolve integration by integrationKey
    ReconLayer->>ReconLayer: verify RSA-SHA256(timestamp + "." + rawBody)
    alt signature valid
        ReconLayer->>ReconLayer: ingestEvidence (RawRecord + FlowLeg)
        ReconLayer->>ReconLayer: run matching engine
        ReconLayer-->>Bridge: 200 EvidenceAcceptanceResponse
    else signature invalid or expired
        ReconLayer->>ReconLayer: ingestEvidence (RawRecord only, matching skipped)
        ReconLayer-->>Bridge: 400 invalid_webhook_signature
    end
```

<Note>
  Even when the signature is invalid, ReconLayer still persists the payload as a `RawRecord` (with `validationStatus: 'failed'`, `signatureValid: false`) for audit purposes — the matching engine is skipped, but nothing is silently dropped. The `400` response includes `details.rawRecordId` and `details.sourceRef` so you can locate the record.
</Note>

### Responses

<ResponseField name="200 EvidenceAcceptanceResponse" type="object">
  Returned when the signature is valid. Same shape used across all evidence endpoints:

  ```json theme={null}
  {
    "accepted": true,
    "duplicate": false,
    "rawRecordId": "rr_01h...",
    "flowLegIds": ["fl_01h..."],
    "matchedCaseIds": ["case_01h..."],
    "signatureValid": true,
    "source": "webhook",
    "sourceRef": "evt_bridge_01h..."
  }
  ```
</ResponseField>

<ResponseField name="400 invalid_webhook_configuration" type="object">
  The `integrationKey` in the path does not resolve to a known, active Bridge integration.
</ResponseField>

<ResponseField name="400 invalid_webhook_signature" type="object">
  Signature missing, malformed, expired (outside the 10-minute window), or failed RSA-SHA256 verification.

  ```json theme={null}
  {
    "error": "invalid_webhook_signature",
    "message": "Webhook signature verification failed.",
    "details": {
      "rawRecordId": "rr_01h...",
      "sourceRef": "evt_bridge_01h..."
    }
  }
  ```
</ResponseField>

See [Receive a Bridge Webhook](/api-reference/webhooks/receive-a-bridge-webhook) and [Reject Ambiguous Bridge Webhook Entrypoint](/api-reference/webhooks/reject-ambiguous-bridge-webhook-entrypoint). For how the payload is normalized into `FlowLeg`s, see [Bridge Integration](/integrations/bridge#webhook-normalization).

### Listing configured inbound endpoints

`GET /v1/webhook-endpoints` lists the inbound webhook URLs ReconLayer has generated for your active integrations (currently Bridge):

```bash theme={null}
curl https://api.reconlayer.com/v1/webhook-endpoints \
  -H "Authorization: Bearer $RECONLAYER_API_KEY" \
  -H "x-organization-id: $ORG_ID"
```

```json theme={null}
{
  "items": [
    {
      "integrationId": "intg_01h...",
      "integrationKey": "acme_bridge_main",
      "provider": "bridge",
      "webhookUrl": "https://api.reconlayer.com/webhooks/bridge/acme_bridge_main",
      "status": "active"
    }
  ]
}
```

See [List Inbound Integration Webhook Endpoints](/api-reference/integrations/list-inbound-integration-webhook-endpoints).

## No native webhook for your provider? Submit evidence directly

If you're integrating a provider, bank, or system that doesn't have a native ReconLayer webhook adapter, push evidence directly through the canonical evidence endpoints. These are the same entry points used internally by Bridge polling and on-chain ingestion — they go through the identical `ingestEvidence` pipeline and produce the same `EvidenceAcceptanceResponse`.

Both endpoints require `Authorization`, `x-organization-id`, and an `Idempotency-Key` header.

### POST /v1/evidence/provider

Use this for any provider/PSP-side settlement, payout, or transfer event.

```bash theme={null}
curl -X POST https://api.reconlayer.com/v1/evidence/provider \
  -H "Authorization: Bearer $RECONLAYER_API_KEY" \
  -H "x-organization-id: $ORG_ID" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "acme-psp",
    "sourceRef": "acme-psp:txn_8123",
    "providerTransferId": "txn_8123",
    "externalReference": "INV-2026-0099",
    "status": "confirmed",
    "amount": { "amount": "1000.00", "currency": "USD" },
    "occurredAt": "2026-06-14T09:30:00Z",
    "payload": { "raw": "original provider payload, stored as-is" }
  }'
```

<ParamField body="provider" type="string" required>Provider identifier, e.g. `"acme-psp"`.</ParamField>
<ParamField body="sourceRef" type="string" required>A stable identifier unique per `(organizationId, source)` — used for duplicate detection.</ParamField>
<ParamField body="paymentIntentId" type="string">Link directly to a known `PaymentIntent` if you already know it.</ParamField>
<ParamField body="providerTransferId" type="string">The provider's own transfer/transaction ID.</ParamField>
<ParamField body="externalReference" type="string">Your reference, used for matching against `PaymentIntent.externalReference`.</ParamField>
<ParamField body="phase" type="string">e.g. `intermediary_in`, `transfer`, `intermediary_out`, `destination` (defaults to `intermediary_in` for provider evidence).</ParamField>
<ParamField body="status" type="string" default="confirmed">A `FlowLeg` status, e.g. `pending`, `confirmed`, `failed`, `reversed`.</ParamField>
<ParamField body="amount" type="object" required>`{ "amount": string, "currency": string }`.</ParamField>
<ParamField body="occurredAt" type="string">ISO 8601 timestamp of when the event occurred.</ParamField>
<ParamField body="references" type="array">Additional `FlowLegReference` key/value entries.</ParamField>
<ParamField body="payload" type="object" required>The raw payload as received from the provider — stored verbatim on the `RawRecord`.</ParamField>

See [Submit Provider Evidence](/api-reference/reconciliation-cases/submit-provider-evidence).

### POST /v1/evidence/onchain

Use this for an on-chain transfer observed by your own indexer or RPC node, for chains/providers not covered by a [native on-chain integration](/integrations/onchain).

```bash theme={null}
curl -X POST https://api.reconlayer.com/v1/evidence/onchain \
  -H "Authorization: Bearer $RECONLAYER_API_KEY" \
  -H "x-organization-id: $ORG_ID" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "chainFamily": "evm",
    "txHash": "0xabc123...",
    "chainId": 137,
    "fromAddress": "0x1111...",
    "toAddress": "0x2222...",
    "tokenAddress": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359",
    "externalReference": "INV-2026-0099",
    "status": "confirmed",
    "amount": { "amount": "1000.00", "currency": "USDC" },
    "occurredAt": "2026-06-14T09:30:00Z",
    "payload": { "raw": "original indexer payload" }
  }'
```

<ParamField body="chainFamily" type="string" default="evm">Currently only `evm` is supported — other values raise an error during normalization.</ParamField>
<ParamField body="txHash" type="string" required>The on-chain transaction hash. Lowercased during normalization.</ParamField>
<ParamField body="chainId" type="number" required>EVM chain ID (e.g. `1` for Ethereum, `137` for Polygon).</ParamField>
<ParamField body="fromAddress" type="string">Sender address.</ParamField>
<ParamField body="toAddress" type="string">Recipient address.</ParamField>
<ParamField body="tokenAddress" type="string">ERC-20 token contract address, if applicable.</ParamField>
<ParamField body="externalReference" type="string">Your reference, used for matching against `PaymentIntent.externalReference`.</ParamField>
<ParamField body="status" type="string" default="confirmed">A `FlowLeg` status.</ParamField>
<ParamField body="amount" type="object" required>`{ "amount": string, "currency": string }`.</ParamField>
<ParamField body="sourceRef" type="string">Optional override for the dedup key; if omitted, derived from `txHash`.</ParamField>
<ParamField body="payload" type="object" required>The raw payload — stored verbatim on the `RawRecord`.</ParamField>

See [Submit On-chain Evidence](/api-reference/reconciliation-cases/submit-on-chain-evidence) and [On-chain Integrations](/integrations/onchain) for the EVM normalization details (hex address lowercasing, `tx_hash`/`external_reference` references, `chainFamily`/`networkId` metadata).

### Idempotency on evidence endpoints

Both endpoints require `Idempotency-Key`. ReconLayer hashes the request body and stores it against the key:

* Same key + same body → replays the original response with `Idempotency-Replayed: true`.
* Same key + different body → `409 idempotency_key_conflict`.
* Same key while the original request is still processing → `409 idempotency_request_in_progress`.

Both endpoints return `201` for newly accepted evidence and `200` if `ingestEvidence` determined the record was a `(organizationId, source, sourceRef)` duplicate (`duplicate: true` in the response body) — this is independent of, and in addition to, the `Idempotency-Key` mechanism.

## Next steps

* [Ingestion Flows](/integrations/ingestion-flows) — see how webhook-delivered evidence flows into the matching engine.
* [Bridge Integration](/integrations/bridge) — full lifecycle for connecting, enabling, and polling Bridge.
* [On-chain Integrations](/integrations/onchain) — chain-indexer-based polling and push ingestion.
* [Outbound Webhooks](/integrations/outbound-webhooks) — get notified by ReconLayer when expectations and cases change.
