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

# Webhooks

> Signed partner callback delivery.

Webhook endpoints are registered through the signed partner API. PayFi can keep multiple endpoints per partner integration, and each endpoint declares which events it wants to receive.

PayFi only sends HTTPS callbacks and blocks localhost, private IP, link-local, metadata, and credential-bearing URLs. Payloads are redacted: raw documents, prompts, completions, secrets, signatures, presigned URLs, and full tax IDs are never sent.

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant Partner as Partner API client
    participant PayFi as PayFi API
    participant Worker as Webhook worker
    participant Endpoint as Partner webhook endpoint

    Partner->>PayFi: POST /v1/webhooks
    PayFi-->>Partner: webhookId + signing secret
    PayFi->>PayFi: Operation or analysis event occurs
    PayFi->>PayFi: Persist redacted delivery payload
    Worker->>Worker: Sign exact raw JSON body
    Worker->>Endpoint: POST signed webhook
    alt 2xx response
        Endpoint-->>Worker: delivered
        Worker->>PayFi: Mark delivery DELIVERED
    else timeout, 408, 429, or 5xx
        Endpoint-->>Worker: transient failure
        Worker->>PayFi: Persist attempt and next retry time
    else terminal 4xx
        Endpoint-->>Worker: contract/auth failure
        Worker->>PayFi: Mark delivery FAILED
    end
```

## Manage endpoints

Use the same PayFi partner authentication headers used by the rest of the API.

* `GET /v1/webhooks`
* `POST /v1/webhooks`
* `GET /v1/webhooks/{webhookId}`
* `PATCH /v1/webhooks/{webhookId}`
* `DELETE /v1/webhooks/{webhookId}`
* `POST /v1/webhooks/{webhookId}/test`

Create request:

```json theme={null}
{
  "name": "Acme endpoint",
  "url": "https://acme.example/payfi/webhooks",
  "eventTypes": [
    "operation.created",
    "operation.requirements.ready",
    "operation.analysis.completed",
    "operation.action_required",
    "operation.manual_review.completed"
  ]
}
```

PayFi generates the webhook signing secret and returns it only in the create response or when `rotateSecret` is `true` on update. Store it immediately.

## Events

Supported events:

* `operation.created`
* `operation.requirements.ready`
* `operation.documents.partial`
* `operation.analysis.completed`
* `operation.action_required`
* `operation.manual_review.completed`
* `operation.callback.failed`

`operation.callback.failed` is emitted to other subscribed endpoints when a delivery fails terminally. PayFi does not recursively emit `operation.callback.failed` for a failed callback-failed delivery.

| Event                               | When PayFi emits it                                                                      | Operational meaning                                                                 |
| ----------------------------------- | ---------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| `operation.created`                 | After `POST /v1/operations` creates the operation.                                       | The partner can show the PayFi operation ID while initial requirements are pending. |
| `operation.requirements.ready`      | After the initial document requirement assessment is ready or auto-approved.             | The partner can review the list or start approved document collection.              |
| `operation.documents.partial`       | After document completeness changes and at least one required document is still missing. | The partner should ask the customer for the missing document types in the payload.  |
| `operation.analysis.completed`      | After analysis finishes and the operation is ready for human review.                     | Compliance evidence is available; the final decision is still manual.               |
| `operation.action_required`         | After analysis or completeness checks find missing evidence or low-confidence data.      | The partner should pause the operation and collect the requested material.          |
| `operation.manual_review.completed` | After an authorized reviewer records approval, rejection, or changes requested.          | This is the final human decision notification for the partner workflow.             |
| `operation.callback.failed`         | After a different delivery reaches terminal failure.                                     | This is an operational alert for webhook processing, not a compliance decision.     |

## Payload

The API Reference includes one schema per event:

* `WebhookOperationCreatedPayload`
* `WebhookDocumentsPartialPayload`
* `WebhookAnalysisCompletedPayload`
* `WebhookActionRequiredPayload`
* `WebhookManualReviewCompletedPayload`
* `WebhookCallbackFailedPayload`

All events share the same top-level shape so partners can route by `event` first and then apply event-specific rules.

Default shape:

```json theme={null}
{
  "event": "operation.analysis.completed",
  "operationId": "op_example",
  "partnerOperationId": "acme-op-123",
  "status": "READY_FOR_MANUAL_REVIEW",
  "score0To100": 82,
  "score1To10": 9,
  "riskLevel": "medium",
  "missingDocumentTypes": [],
  "actionRequired": false,
  "manualApprovalRequired": true,
  "analysisRunId": "job_example",
  "manualReviewDecision": null,
  "processedAt": "2026-05-20T12:00:00.000Z"
}
```

Do not treat webhook delivery as the source of truth for PayFi state. The source of truth remains `GET /v1/operations/{operationId}`.

## End-to-end flow

1. The partner creates one or more webhook endpoints with `POST /v1/webhooks`.
2. PayFi validates the signed partner request, reserves the idempotency key, validates the URL, generates a signing secret, stores the encrypted secret, and returns the plaintext secret once.
3. When an operation changes state, PayFi selects active endpoints subscribed to that event.
4. PayFi builds the allowlisted payload, redacts it, serializes it to raw JSON, hashes the raw body, and persists a `WebhookDelivery` before any HTTP call.
5. The worker loads due deliveries, reloads the endpoint, revalidates that it is active and has a safe URL, decrypts the endpoint secret, verifies the stored payload hash, signs the exact raw JSON body, and sends the callback.
6. PayFi persists every attempt with status, response status, response body hash, request header hash, duration, failure code, and attempted timestamp.
7. A `2xx` response marks the delivery as `DELIVERED`. A retryable failure sets `RETRY_SCHEDULED` and `nextAttemptAt`. A terminal failure marks the delivery as `FAILED`.

## Registration validations

Webhook management endpoints use the same partner request signing model as the rest of the API:

* `X-PayFi-Api-Key`
* `X-PayFi-Nonce`
* `X-PayFi-Signature`
* `Idempotency-Key` on create, update, delete, and test requests

PayFi validates:

* API key is active and scoped to the partner integration.
* Nonce has not been replayed.
* Request timestamp is inside the allowed skew.
* HMAC signature matches the exact raw request body.
* Idempotency key is reused only with the same method, path, and body hash.
* Webhook ID belongs to the signed partner integration.
* URL uses HTTPS and does not include credentials.
* URL host is not localhost, private IP, link-local, or metadata infrastructure.
* `eventTypes` contains only supported event names.
* Signing secrets are generated server-side and returned only on create or explicit rotation.

## Delivery validations

Before each HTTP delivery attempt, PayFi validates the stored delivery again:

* Endpoint still exists, is active, and belongs to the same organization and partner integration.
* Endpoint URL still passes SSRF validation.
* Endpoint signing secret decrypts successfully.
* Re-serialized payload hash matches the persisted `payloadHash`.
* Payload is signed with the exact raw JSON body that will be sent.
* Response bodies are hashed for auditability; raw response bodies are not stored.
* Authorization headers, webhook secrets, signatures, raw documents, prompts, completions, and full tax IDs are never written into delivery logs.

These checks happen on retries too, so disabling an endpoint or rotating a secret affects future attempts without changing already persisted delivery payloads.

## Signature verification

PayFi signs the exact raw JSON body and sends:

```http theme={null}
x-payfi-webhook-id: whd_example
x-payfi-webhook-timestamp: 1779000000000
x-payfi-webhook-signature: t=1779000000000,d=whd_example,h=<sha256_body>,v1=<hmac_sha256>
```

Signature base string:

```text theme={null}
<timestampMs>.<deliveryId>.<sha256(rawJsonBody)>
```

Use the webhook signing secret as the HMAC-SHA256 key. Verify the signature before parsing business fields, using the exact raw body bytes received by your HTTP server.

## Retries

PayFi retries transient partner processing failures:

* network errors
* timeout
* HTTP `408`
* HTTP `429`
* any HTTP `5xx`

Terminal failures:

* any HTTP `2xx`: delivered
* HTTP `4xx` except `408` and `429`: failed

Retry schedule:

| Attempt | Next retry       |
| ------- | ---------------- |
| 1       | 1 minute         |
| 2       | 5 minutes        |
| 3       | 15 minutes       |
| 4       | 1 hour           |
| 5       | 4 hours          |
| 6       | 12 hours         |
| 7       | 24 hours         |
| 8       | terminal failure |

Partners should make webhook handlers idempotent by storing the `x-payfi-webhook-id` header or a tuple of `event` and `operationId`.

## Test delivery

`POST /v1/webhooks/{webhookId}/test` sends one signed sample payload to the endpoint. Test deliveries do not schedule retries, so a failed test response is terminal for that test request.
