# Events and Webhooks

Spike emits webhook events at every meaningful state change in an exchange. Partners use these events as triggers; `GET /v1/exchanges/by-id` is the authoritative source of truth for current state.

This page is the canonical catalog of events Spike emits, the failure-context payload structure, the typical orderings per flow, and the delivery guarantees.

## Event catalog

Field-level payload schemas are defined in the [API Reference](../../../api.html) under `WebhookPayload`. The wire-format event names below match the `eventType` field of the webhook payload exactly.

### Deposit events

| Event | Emitted when | Notes |
|---|---|---|
| `deposit.initiated` | A funding deposit was detected but is not yet confirmed. | On-ramp: fiat funding transition when applicable. Off-ramp: crypto deposit detected on-chain and awaiting confirmations. |
| `deposit.progressed` | Confirmation count advanced toward the required threshold but the deposit is not yet fully confirmed. | Off-ramp only. Carries the current and required confirmation counts. |
| `deposit.received` | The funding deposit was confirmed and is ready to be converted. | On-ramp: the partner-supplied fiat `depositReference` was accepted. Off-ramp: the crypto deposit reached the required confirmation count. |

### Trading-leg events

These reflect the **provider order** lifecycle, not the partner-visible aggregate state. An on-ramp can emit `exchange.completed` (trade filled) while the aggregate is still `PROCESSING`, because the crypto withdrawal is still in flight. Branch on the **aggregate terminal events** below to know whether the exchange is done.

| Event | Emitted when | Notes |
|---|---|---|
| `exchange.authorization_required` | The exchange entered `AWAITING_AUTHORIZATION` and the partner must approve or reject. | On-ramp only. Carries `authorizationContext` for partner verification. |
| `exchange.completed` | The trading leg filled (provider order completed). | On-ramp: trade filled; crypto not yet delivered. Off-ramp: conversion is final but the off-ramp aggregate may still be running. When a partner rejects authorization on an on-ramp, this event can already have fired and is **not** retracted. |
| `exchange.failed` | The trading leg failed (provider order rejected). | NOT a partner-facing terminal — branch on the aggregate `*ramp.failed` event. |
| `exchange.cancelled` | The trading leg was cancelled. | On-ramp cancellation is only supported before the trading exchange is created. |

### Withdrawal events (on-ramp)

| Event | Emitted when | Notes |
|---|---|---|
| `withdraw.initiated` | An outbound crypto withdrawal was submitted to the network. | On-ramp only, after partner approval. |
| `withdraw.completed` | The withdrawal was confirmed on-chain. | Carries the blockchain transaction hash and `netAmount` after network fees. |
| `withdraw.failed` | The withdrawal failed after submission. | The on-ramp aggregate enters `FAILED` and emits `onramp.failed`. |

### Aggregate terminal events

These are the partner's **canonical "is the exchange done" signals.** Each ramp emits exactly one terminal event in its lifetime — either `*.completed` or `*.failed`. Subscribe to these for terminal-state reconciliation.

| Event | Emitted when | Notes |
|---|---|---|
| `onramp.completed` | The on-ramp aggregate entered `COMPLETED`. | Emitted after `withdraw.completed`. Trade filled and crypto delivered. |
| `onramp.failed` | The on-ramp aggregate entered `FAILED`. | Carries `failureContext` (see below). |
| `offramp.completed` | The off-ramp aggregate entered `COMPLETED`. | Emitted after the fiat settlement leg completes. |
| `offramp.failed` | The off-ramp aggregate entered `FAILED`. | Carries `failureContext` (see below). |

### Refund events (off-ramp)

| Event | Emitted when | Notes |
|---|---|---|
| `refund.required` | A failed or cancelled off-ramp received funds that must be returned. | **Signal only.** The partner must call `POST /v1/exchanges/refund` to start the refund. See [Refunds](refunds.md). |
| `refund.initiated` | A refund withdrawal was submitted to the network. | A single exchange can have multiple refunds. |
| `refund.completed` | The refund was confirmed on-chain. | Carries the blockchain transaction hash. |
| `refund.failed` | The refund withdrawal failed. | Resolution typically requires partner contact with Spike support. |

## Failure context

The aggregate failure events `onramp.failed` and `offramp.failed` carry a structured `failureContext` payload so partners can branch deterministically without parsing free text.

| Field | Type | Description |
|---|---|---|
| `failureReason` | enum | Machine-readable reason. One of: `AUTHORIZATION_REJECTED`, `AUTHORIZATION_EXPIRED`, `EXCHANGE_ORDER_CANCELLED`, `EXCHANGE_CREATION_FAILED`, `DEPOSIT_REFUNDED`, `WITHDRAWAL_FAILED`. |
| `failedPhase` | enum | Phase the ramp died in. One of: `DEPOSIT`, `EXCHANGE`, `AUTHORIZATION`, `WITHDRAWAL`. Derived deterministically from `failureReason`. |
| `rejectionReason` | string \| null | Free-text reason supplied by the partner via `POST /authorization/reject`. Populated **only** when `failureReason == AUTHORIZATION_REJECTED`. |

For per-`failureReason` recovery actions, see [Failure handling](../operations/failure-handling.md).

## Event ordering by flow

The diagrams below show the typical event order. Webhook delivery is at-least-once and may reorder, retry, or duplicate events. Always reconcile against `GET /v1/exchanges/by-id` rather than relying on receive order.

### On-ramp — happy path

```mermaid
sequenceDiagram
    participant P as Partner
    participant S as Spike
    P->>S: POST /v1/exchanges
    S-->>P: 200 (status: WAITING_DEPOSIT)
    Note over S: Fiat deposit reference processing
    S-->>P: deposit.received
    Note over S: Trade leg
    S-->>P: exchange.completed
    S-->>P: exchange.authorization_required
    P->>S: POST /authorization/approve
    S-->>P: withdraw.initiated
    Note over S: On-chain confirmation
    S-->>P: withdraw.completed
    S-->>P: onramp.completed
```

### On-ramp — partner rejects authorization

```mermaid
sequenceDiagram
    participant P as Partner
    participant S as Spike
    Note over S: ... up to authorization
    S-->>P: exchange.completed
    S-->>P: exchange.authorization_required
    P->>S: POST /authorization/reject
    S-->>P: onramp.failed (failureReason: AUTHORIZATION_REJECTED)
```

The trading-leg `exchange.completed` already fired and is not retracted. The trade is final on Spike's side; the partner reverses fiat off-platform.

### Off-ramp — happy path

```mermaid
sequenceDiagram
    participant U as End User
    participant P as Partner
    participant S as Spike
    P->>S: POST /v1/deposit-addresses
    S-->>P: 201 (depositAddressId, depositAddress)
    U->>S: Crypto deposit on chain
    S-->>P: deposit.initiated
    Note over S: Confirmations accrue
    S-->>P: deposit.progressed (1..N times)
    S-->>P: deposit.received
    Note over S: Trade leg
    S-->>P: exchange.completed
    S-->>P: offramp.completed
```

### Off-ramp — failure with refund

```mermaid
sequenceDiagram
    participant P as Partner
    participant S as Spike
    Note over S: ... up to deposit.received
    S-->>P: deposit.received
    Note over S: Trade leg or downstream phase fails
    S-->>P: offramp.failed (failureContext)
    S-->>P: refund.required
    P->>S: POST /v1/exchanges/refund
    S-->>P: refund.initiated
    Note over S: On-chain refund confirmation
    S-->>P: refund.completed
```

## Delivery guarantees

- **Signature** — every webhook is signed with RSA-SHA256 using the partner's RSA key pair. Verify the `X-Webhook-Signature` header before trusting the payload. Signature scheme details are in the [API Reference](../../../api.html).
- **Retries** — failed deliveries are retried automatically with exponential backoff. A `2xx` response from the partner endpoint is required to acknowledge delivery; any non-`2xx` triggers a retry.
- **Suspension** — after 5 consecutive delivery failures the webhook is suspended. Failed events remain queryable via `GET /v1/webhooks/{webhookId}/failed-events` and can be re-driven via `POST /v1/webhooks/{webhookId}/events/{eventId}/retry`. Reactivate the webhook via `POST /v1/webhooks/{webhookId}/activate`.
- **Ordering** — not guaranteed. A retry of an earlier event can arrive after a later event. Do not rely on receive order for branching.
- **Duplicates** — partner endpoints may receive the same event more than once. Handlers must be idempotent on the event ID.
- **At-least-once** — events may be missed entirely if the partner endpoint is unreachable for an extended period. Reconcile with `GET /v1/exchanges/by-id` periodically.

## Idempotent handler pattern

Every event payload includes a unique event ID. The recommended pattern is:

1. On receipt, verify the signature and parse the event ID.
2. Look up the event ID in your local processed-event store.
3. If already processed, return `200` and stop.
4. If new, fetch the current state with `GET /v1/exchanges/by-id` (or the relevant resource).
5. Update your local order based on that fetched state — not on the event payload alone.
6. Mark the event ID processed and return `200`.

This pattern is safe against duplicates, reordering, and stale webhook payloads.

## Reading state without webhooks

Webhooks are optional. Any partner system can operate by polling `GET /v1/exchanges/by-id` and `GET /v1/exchanges` (with filters and cursor pagination). Recommended polling cadences are documented in [Reconciliation](../operations/reconciliation.md). Webhooks reduce latency and cost; polling is the safety net.

## Common questions

**Why did I receive `exchange.completed` but the exchange is still `PROCESSING`?**
On-ramp: `exchange.completed` is the trading-leg fill signal. The exchange remains in `PROCESSING` while the crypto withdrawal is in flight. Wait for `onramp.completed` (or `withdraw.completed` followed by `status: COMPLETED`).

**Which event tells me an exchange is definitively done?**
The aggregate terminal events: `onramp.completed` / `onramp.failed` / `offramp.completed` / `offramp.failed`. Each ramp emits exactly one in its lifetime.

**Should I act on the event payload or refetch?**
Refetch from `GET /v1/exchanges/by-id`. The event payload is a trigger; the API is the truth.

**How do I subscribe to events?**
Register a webhook endpoint via `POST /v1/webhooks` with the event types you want delivered. See the [API Reference](../../../api.html).
