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

# Webhooks

> The events Orchestrate sends, how they are delivered, and how to trust them.

Payments do not always finish while your request is open. A customer approves on their
phone, a bank settles minutes later, a refund clears overnight. Webhooks are how Orchestrate tells
you the outcome.

## Two delivery channels

Orchestrate can deliver an event two ways. Both carry the same events and the same
payloads; what differs is scope and whether the delivery is signed.

|            | Project endpoint                                 | Per-resource `webhook_url`                              |
| ---------- | ------------------------------------------------ | ------------------------------------------------------- |
| Configured | In your [dashboard](https://app.orqex.com), once | On the intent, checkout session or payout, per resource |
| Scope      | Every event you subscribe to, across the project | Only that payment or payout                             |
| Signed     | **Yes**, HMAC with the endpoint secret           | **No**                                                  |

Pick a project endpoint when you want one handler for everything, a `webhook_url` when you
want the notification tied to the resource you just created — or both.

<Warning>
  Deliveries to a `webhook_url` are **not signed**. Anyone who learns the URL can post to it.
  Treat the payload as a notification only: re-read the resource from the API before you act
  on it.
</Warning>

For anything that moves value on your side — fulfilling an order, crediting a wallet — use a
project endpoint and verify the signature, or re-fetch the resource. Never trust an unsigned
body.

## Events

Every event below is delivered on both channels. On a project endpoint you subscribe per
event; the `subscribe as` column is the name you pick in your dashboard.

| Event                        | Subscribe as                | Fired when                                      |
| ---------------------------- | --------------------------- | ----------------------------------------------- |
| `payment.initiated`          | `payment_initiated`         | A payment is created.                           |
| `payment.completed`          | `payment_captured`          | The payment succeeded.                          |
| `payment.failed`             | `payment_failed`            | The payment failed for good.                    |
| `payment.expired`            | `payment_expired`           | The payment expired before it completed.        |
| `payment.refunded`           | `payment_refunded`          | A refund brought the payment to fully refunded. |
| `payment.partially_refunded` | `payment_refunded`          | A refund cleared, part of the payment remains.  |
| `payment.attempt.completed`  | `payment_attempt_completed` | One attempt on a payment succeeded.             |
| `payment.attempt.failed`     | `payment_attempt_failed`    | One attempt on a payment failed.                |
| `payout.initiated`           | `payout_initiated`          | A payout is created.                            |
| `payout.completed`           | `payout_completed`          | The payout was delivered.                       |
| `payout.failed`              | `payout_failed`             | The payout failed.                              |

Subscribing to `all` on a project endpoint gives you every event above, including future
ones. Key your handler on the `event` field rather than on a fixed list.

<Note>
  A failed attempt does not mean a failed payment. `payment.attempt.failed` can be followed
  by a successful attempt on the same payment. Act on `payment.*` for the final outcome and
  treat `payment.attempt.*` as detail.

  Both refund events share the `payment_refunded` subscription; read the `event` field to
  tell a full refund from a partial one.
</Note>

## Delivery guarantees

Deliveries are retried up to five times on failure. Delivery is at-least-once and order is
not guaranteed: you can receive the same event twice, or a later event before an earlier
one.

Build your handler accordingly:

* **Be idempotent.** Key on the payment id plus the event name and ignore repeats.
* **Do not rely on order.** Read the current status from the payload or the API rather than
  inferring it from the sequence of events.
* **Respond quickly.** Return `2xx` as soon as you have stored the event; do the work
  afterwards.

## Headers

Project endpoint deliveries:

```
X-Pulse-Id: <endpoint id>
User-Agent: Pulse | Orqex +https://orqex.com
```

Payment `webhook_url` deliveries:

```
X-Payment-Event: payment.completed
X-Payment-Id: pi_...
X-Attempt-Id: att_...      # attempt events only
User-Agent: Orqex +https://orqex.com
```

Payout `webhook_url` deliveries:

```
X-Payout-Event: payout.completed
X-Payout-Id: po_...
User-Agent: Orqex +https://orqex.com
```

## Verifying a project endpoint

Each project endpoint has its own secret, shown when you create it in the dashboard.
Deliveries carry an HMAC signature computed over the raw request body with that secret.

Compare it in constant time, against the **raw** body — not a re-encoded version of the
parsed JSON.

```php theme={null}
$payload   = file_get_contents('php://input');
$expected  = hash_hmac('sha256', $payload, $secret);

if (! hash_equals($expected, $signatureHeader)) {
    http_response_code(400);
    exit;
}

$event = json_decode($payload, true);
```

## Payloads

### Payment events

```json theme={null}
{
  "event": "payment.completed",
  "payment": {
    "id": "pi_...",
    "amount": { "value": 50, "formatted": "$50.00", "short": "$50", "currency": "USD" },
    "status": "completed",
    "description": "Order #1042",
    "return_url": "https://example.com/orders/1042/return",
    "statement_descriptor": null,
    "receipt_email": null,
    "metadata": {},
    "completed_at": "...",
    "failed_at": null,
    "refunded_at": null,
    "partially_refunded_at": null,
    "expires_at": "...",
    "created_at": "..."
  },
  "attempt": {
    "id": "att_...",
    "status": "completed",
    "method_code": "test",
    "failure_code": null,
    "is_failover": false,
    "failover_decision": null
  },
  "customer": { "id": "cus_...", "first_name": "Ada", "last_name": "Lovelace", "email": "ada@example.com" },
  "project": { "id": "prj_...", "name": "Acme" },
  "timestamp": "..."
}
```

### Attempt events

Same shape, with more detail on `attempt` — it adds `amount`, `country`, `next_action`,
`completed_at`, `failed_at` and `created_at` — and a shorter `payment` block that carries
only `created_at` among the timestamps.

### Refund events

```json theme={null}
{
  "event": "payment.refunded",
  "refund": {
    "id": "ref_...",
    "amount": { "value": 50, "formatted": "$50.00", "short": "$50", "currency": "USD" },
    "status": "completed",
    "reason": "requested_by_customer",
    "created_at": "..."
  },
  "payment": {
    "id": "pi_...",
    "amount": { "value": 50, "formatted": "$50.00", "short": "$50", "currency": "USD" },
    "status": "refunded"
  },
  "customer": { "id": "cus_...", "first_name": "Ada", "last_name": "Lovelace", "email": "ada@example.com" },
  "timestamp": "..."
}
```

### Payout events

```json theme={null}
{
  "event": "payout.completed",
  "payout": {
    "id": "po_...",
    "amount": { "value": 50, "formatted": "$50.00", "short": "$50", "currency": "USD" },
    "method": "...",
    "status": "completed",
    "reference": "batch-42-line-7",
    "description": "Supplier settlement",
    "customer": { "id": "cus_...", "first_name": "Ada", "last_name": "Lovelace", "email": "ada@example.com" },
    "instrument": { "id": "pin_...", "type": "bank_account", "...": "..." },
    "gateway": { "transaction": { "id": "...", "reference": "...", "external_id": "..." } },
    "fee_amount": 0,
    "failure": { "code": null, "message": null }
  }
}
```

## When a webhook never arrives

If your endpoint was down or a payment looks stuck, do not poll in a loop. Ask Orchestrate to
re-check the payment with the provider: see [Requery](/payments/requery). For payouts, use
`GET /payouts/{id}/sync` — see [Payouts](/payouts/overview).
