# Failure Handling

This article covers **async and cross-cutting failures** — the ones that can occur **even when the partner's request was correct and accepted with `2xx`**. Sync request errors are documented per endpoint in the [API Reference](../../../api.html) under the `ClientError` schema. Webhook delivery semantics live in [Events and Webhooks](../concepts/events-and-webhooks.md); idempotency-conflict responses and the partner retry pattern live in [Idempotency](../concepts/idempotency.md).

## 1. Trade execution

The exchange was created and accepted with `200`, but the market order could not fill — insufficient liquidity, slippage rejection, FOK / submission timeout, or a post-acceptance execution failure from the provider. All four sub-cases surface asynchronously via webhook (not as the `POST /v1/exchanges` response) and collapse into a single partner-visible signal.

| Failure | Signal | Recommended action |
|---|---|---|
| Trade execution failed | `exchange.failed`; `onramp.failed` / `offramp.failed` with `failureContext = { "failureReason": "EXCHANGE_ORDER_CANCELLED", "failedPhase": "EXCHANGE" }`; `status: FAILED` | Terminal. On-ramp: reverse partner-side fiat to user; a new business order requires a fresh `externalId`. Off-ramp: refund auto-triggers (`refund.required`). |

> In dev and staging, every sub-case of trade execution failure can be reproduced deterministically against your own pipeline. See [Section 4: Section 1 reproduction recipes](#section-1-trade-execution-recipes).

## 2. Deposit (off-ramp specific)

The deposit address received funds but the deposit cannot be applied. These are operational realities of working with public blockchains; they do not surface as HTTP errors.

> On-ramp has no parallel category in this article. The partner-asserted `depositReference` is treated as authoritative — Spike does not cross-check it against the partner's fiat ledger, and `WAITING_DEPOSIT` does not currently expire. Any failure during on-ramp funding only surfaces later, via the trade leg in Section 1. Partners must time out abandoned `WAITING_DEPOSIT` exchanges in their own bookkeeping and call `POST /v1/exchanges/cancel` to release the reservation.

| Failure | Signal | Recommended action |
|---|---|---|
| Wrong network | `deposit.cancelled` with reason; possibly no event if the asset is not recognised | Recovery may require Spike support. Often unrecoverable depending on the network. UI must make asset/network unambiguous. |
| Wrong asset | `deposit.cancelled`; possibly no event | Same as wrong network. |
| Below configured minimum | `deposit.cancelled` with reason | If refundable, refund auto-triggers. If below dust threshold, refund may not be issued because the network fee would exceed the deposit value. Suppress from the customer-facing activity feed when truly negligible. |
| Reorg invalidating a deposit | Exchange status updates accordingly; deposit identifier is reset | Treat the deposit as having not happened. |
| Address disabled before confirmation | `deposit.cancelled` | Recovery depends on operational policy. Do not rely on disabled-address deposits being processed. |

> In dev and staging, deposit detection, deposit cancellation, and stuck-pending behavior can be reproduced against a real deposit address by simulating an inbound deposit with a chosen status. See [Section 4: Section 2 reproduction recipes](#section-2-deposit-recipes).

## 3. Withdrawal (on-ramp specific)

The trade succeeded but the outbound crypto withdrawal did not. Async failure after `exchange.completed`.

| Failure | Signal | Recommended action |
|---|---|---|
| Withdrawal rejected at submission | `withdraw.failed`; `status: FAILED` | Operational exception. Do not mark order delivered. Contact Spike support; reconcile partner-side fiat with user. |
| Withdrawal stuck in network broadcast | No `withdraw.completed`, no `withdraw.failed` after extended time | Poll `GET /v1/exchanges/by-id`. If the exchange remains in `PROCESSING` past expected duration, contact Spike support. |

> In dev and staging, withdrawal rejection at submission can be reproduced deterministically against your own pipeline. See [Section 4: Section 3 reproduction recipes](#section-3-withdrawal-recipes).

## 4. Reproducing failures in dev and staging (Bitfinex mock)

> Dev and staging environments only. These admin endpoints do not exist in production.

The Spike dev and staging environments are wired to a Bitfinex mock that exposes admin endpoints for deterministically reproducing every async failure in Sections 1–3. Use these to validate partner-side handling of `exchange.failed`, `withdraw.failed`, `deposit.cancelled`, refund / reverse flows, and reconciliation without waiting for real liquidity, network, or address conditions to occur.

**Bitfinex mock admin base URL:**

| Environment | URL |
|---|---|
| Dev | `https://admin-one.spikeapp.cool/mock/bitfinex` |
| Staging | `https://admin-two.spikeapp.cool/mock/bitfinex` |

The examples below use `<adminBaseUrl>` as a placeholder — substitute the URL for the environment you are testing against. The mock behaves identically in both environments; only the URL changes.

### Mock admin endpoints

Two endpoints. The first configures the mock's behavior for orders and withdrawals; the second pushes a simulated inbound deposit at a deposit address.

#### `POST <adminBaseUrl>/configure` — configure mock behavior

Sets per-`apiKey` toggles that affect every subsequent order or withdrawal routed through that key. All fields are optional; omitted fields keep their current value.

| Field | Type | Effect |
|---|---|---|
| `apiKey` | string (required) | The partner's linked Bitfinex `apiKey` for this environment. The toggles below only affect operations routed through this key. See [Choosing the right `apiKey`](#choosing-the-right-apikey). |
| `autoExecuteOrders` | boolean | When `false`, submitted orders are accepted but never fill — they sit until the trade leg gives up and the exchange enters `FAILED`. Reproduces FOK-not-executing / liquidity-timeout. Default `true`. |
| `simulateSlippage` | boolean | When `true`, FOK orders are rejected with a slippage-style failure on submission. Default `false`. |
| `failNextOrder` | enum or `null` | One-shot. The next order is rejected with the chosen mode; the toggle auto-clears after firing. Modes: `INSUFFICIENT_FUNDS`, `BAD_SYMBOL`, `INVALID_ORDER`. |
| `failAllOrders` | enum or `null` | Persistent. Every subsequent order is rejected with the chosen mode until set back to `null`. Same modes as `failNextOrder`. |
| `failNextWithdrawal` | enum or `null` | One-shot. The next outbound crypto withdrawal is rejected. Modes: `INSUFFICIENT_FUNDS`, `INVALID_ADDRESS`, `ID_ZERO`. |
| `failAllWithdrawals` | enum or `null` | Persistent. Every subsequent withdrawal is rejected with the chosen mode until set back to `null`. Same modes as `failNextWithdrawal`. |

Use `failNext*` for normal negative-scenario tests — it auto-resets after the next operation, so concurrent tests on the same `apiKey` are not affected. Use `failAll*` only for soak-style tests or when explicitly modelling a sustained outage; remember to restore.

#### `POST <adminBaseUrl>/deposits/simulate` — simulate inbound deposit

Pushes a simulated blockchain deposit at a real deposit address. The partner-visible behavior depends on the `status` field.

| Field | Type | Effect |
|---|---|---|
| `depositAddressId` | string (required) | The Spike deposit address to simulate against. Must belong to the partner. |
| `amount` | decimal (required) | Deposit amount in the deposit currency. |
| `status` | enum | Drives the partner-visible signal. Values: `COMPLETED` (default — happy-path confirmed deposit), `PENDING` (deposit detected, never confirms), `CANCELED` (deposit rejected), `ERROR` (provider-side error). |

`status` mapping to partner-visible events:

| `status` | Partner-visible signal | Use to reproduce |
|---|---|---|
| `COMPLETED` (default) | `deposit.initiated` → `deposit.received` → exchange progresses to `PROCESSING` → `exchange.completed` | Happy-path off-ramp deposit |
| `PENDING` | `deposit.initiated` only; the deposit never reaches `deposit.received` | Stuck-in-confirmations / late-confirmation handling |
| `CANCELED` | `deposit.cancelled` with reason | Wrong network / below-minimum / address-disabled paths in Section 2 |
| `ERROR` | `deposit.cancelled` with a provider-error reason | Provider-side deposit-detection error path |

Each call simulates one deposit. There is no persistent state to restore.

### Choosing the right `apiKey`

Every `/configure` call must include `apiKey`. The toggles only affect orders and withdrawals routed through that key, so each partner's negative-scenario testing is naturally isolated:

- Use your partner's **linked Bitfinex `apiKey`** for the environment you are testing. Spike shares this value during dev or staging onboarding. It is the same key the environment's pipeline uses to route your partner's on-ramp orders, so configuration changes deterministically affect your own new orders only.
- **Do not** call `/mock/bitfinex/test-keys`. That endpoint generates an unlinked key that is not wired to any partner's pipeline. Configuring it will have no effect on your on-ramp orders.
- After a negative test, restore happy-path configuration on your linked `apiKey` before running other tests against the same key.

A complete restore-to-happy-path call:

```bash
curl -sS -X POST <adminBaseUrl>/configure \
  -H 'Content-Type: application/json' \
  -d '{
    "apiKey": "<linkedApiKey>",
    "autoExecuteOrders": true,
    "simulateSlippage": false,
    "failNextOrder": null,
    "failAllOrders": null,
    "failNextWithdrawal": null,
    "failAllWithdrawals": null
  }'
```

### Section 1 (Trade execution) recipes

#### FOK not executing — `autoExecuteOrders: false`

Reproduces "Insufficient liquidity" / "Trade timeout" from Section 1.

1. Configure the mock for your linked `apiKey`:

   ```bash
   curl -sS -X POST <adminBaseUrl>/configure \
     -H 'Content-Type: application/json' \
     -d '{"autoExecuteOrders":false,"apiKey":"<linkedApiKey>"}'
   ```

2. Create a fresh exchange via `POST /v1/exchanges`.
3. Poll `GET /v1/exchanges/by-id?exchangeId=<exchangeId>`.
4. Expected: the exchange does not reach `AWAITING_AUTHORIZATION`. It transitions to `FAILED`, `exchange.failed` is delivered, and the partner action follows Section 1 (on-ramp: reverse partner-side fiat; off-ramp: refund auto-triggers per Section 2).
5. Restore happy-path (see [restore call](#choosing-the-right-apikey)).

#### Slippage rejection — `simulateSlippage: true`

Reproduces "Slippage rejection" from Section 1.

1. Configure the mock:

   ```bash
   curl -sS -X POST <adminBaseUrl>/configure \
     -H 'Content-Type: application/json' \
     -d '{"simulateSlippage":true,"apiKey":"<linkedApiKey>"}'
   ```

2. Create a fresh exchange.
3. Poll until terminal.
4. Expected: the FOK order is rejected, the exchange transitions to `FAILED`, and `exchange.failed` is delivered. Same partner action as the previous recipe.
5. Restore happy-path.

#### Specific order rejection — `failNextOrder` / `failAllOrders`

Reproduces order-side rejection paths with a specific `failureReason`. The partner-visible event is `exchange.failed` regardless of mode — what differs is the underlying reason carried in `failureContext`. Use these recipes to verify your error branching does not assume a single reason string.

1. Configure the mock with `failNextOrder` (one-shot — auto-clears after firing). For sustained testing, use `failAllOrders` and remember to restore.

   ```bash
   curl -sS -X POST <adminBaseUrl>/configure \
     -H 'Content-Type: application/json' \
     -d '{"failNextOrder":"INSUFFICIENT_FUNDS","apiKey":"<linkedApiKey>"}'
   ```

2. Create a fresh exchange.
3. Poll until terminal.
4. Expected: `exchange.failed` is delivered, the exchange transitions to `FAILED`, and `failureContext.failureReason` reflects the underlying provider error. Same partner action as the recipes above.
5. Restore happy-path (only needed if you used `failAllOrders`).

Modes:

| Mode | What the mock returns | Reproduces |
|---|---|---|
| `INSUFFICIENT_FUNDS` | Provider rejects with insufficient-funds | Account-side liquidity exhaustion before the order can route |
| `BAD_SYMBOL` | Provider rejects with bad-symbol | Misconfigured / unsupported pair surfacing as a runtime error |
| `INVALID_ORDER` | Provider rejects with invalid-order | Generic provider-side validation failure on order submission |

### Section 2 (Deposit) recipes

#### Simulate inbound deposit — `status` field

Reproduces the off-ramp deposit lifecycle and the cancellation paths from Section 2 by pushing a simulated deposit at one of your deposit addresses.

1. Create a deposit address in the normal way (`POST /v1/deposit-addresses`) and record the `depositAddressId`.
2. Push a simulated deposit:

   ```bash
   curl -sS -X POST <adminBaseUrl>/deposits/simulate \
     -H 'Content-Type: application/json' \
     -d '{
       "depositAddressId": "<depositAddressId>",
       "amount": "0.05",
       "status": "CANCELED"
     }'
   ```

3. Watch for the matching webhook events and poll `GET /v1/exchanges/by-id` to confirm.
4. Expected behavior depends on `status` — see the mapping table in [`POST <adminBaseUrl>/deposits/simulate`](#post-adminbaseurldepositssimulate--simulate-inbound-deposit) above.

Use the four `status` values to walk through the full lifecycle:

- `COMPLETED` — verify the happy path end-to-end without sending real on-chain funds.
- `PENDING` — verify your "stuck in confirmations" UX and reconciliation polling.
- `CANCELED` — verify your `deposit.cancelled` handling and refund-flow integration.
- `ERROR` — verify your handling of provider-side deposit detection failures.

There is no per-call restore — each simulated deposit is a one-shot event.

### Section 3 (Withdrawal) recipes

#### Specific withdrawal rejection — `failNextWithdrawal` / `failAllWithdrawals`

Reproduces "Withdrawal rejected at submission" from Section 3. The partner-visible event is `withdraw.failed` followed by `onramp.failed`; the `failureContext` carries the underlying reason.

1. Configure the mock with `failNextWithdrawal` (one-shot — auto-clears after firing). For sustained testing, use `failAllWithdrawals` and remember to restore.

   ```bash
   curl -sS -X POST <adminBaseUrl>/configure \
     -H 'Content-Type: application/json' \
     -d '{"failNextWithdrawal":"INVALID_ADDRESS","apiKey":"<linkedApiKey>"}'
   ```

2. Run a complete on-ramp through the trade leg and approve at `AWAITING_AUTHORIZATION`. The withdrawal will be submitted to the mock, which rejects it.
3. Poll until terminal.
4. Expected: `withdraw.failed` is delivered, the exchange transitions to `FAILED`, `onramp.failed` carries the reason in `failureContext`, and the partner action follows Section 3 (do not mark order delivered; reconcile partner-side fiat with the user; engage Spike support if needed).
5. Restore happy-path (only needed if you used `failAllWithdrawals`).

Modes:

| Mode | What the mock returns | Reproduces |
|---|---|---|
| `INSUFFICIENT_FUNDS` | Provider rejects with insufficient-funds | Hot-wallet liquidity exhaustion at withdrawal submission |
| `INVALID_ADDRESS` | Provider rejects with invalid-address | Destination accepted by partner-side validation but rejected by the provider (e.g. risk-list or per-asset constraint not visible at validation time) |
| `ID_ZERO` | Provider returns withdrawal ID `0` (malformed response) | Generic provider-side malformed-response path that must surface as `withdraw.failed` rather than getting stuck |

> All three modes collapse to the same partner-visible event. The point of testing each mode is to confirm your branching does not pattern-match a single reason string and that all three reach your `FAILED` reconciliation path.

## Cross-reference summary

| Topic | Where it lives |
|---|---|
| Sync request errors (HTTP 4xx/5xx per endpoint) | [API Reference](../../../api.html) |
| Idempotency-conflict responses, retry pattern | [Idempotency](../concepts/idempotency.md) |
| Webhook delivery semantics, signature, ordering, idempotent handler | [Events and Webhooks](../concepts/events-and-webhooks.md) |
| Statuses, transitions, terminal states | [Exchange Lifecycle](../concepts/exchange-lifecycle.md) |
| Refund triggering and lifecycle | [Refunds](../concepts/refunds.md) |
| Address roles and validation | [Addresses](../concepts/addresses.md) |
| Per-flow happy path | [On-Ramp](../flows/non-fixed-rate-on-ramp.md), [Off-Ramp](../flows/non-fixed-rate-off-ramp.md) |
