> For the complete documentation index, see [llms.txt](https://developers.unigox.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://developers.unigox.com/api-reference/webhooks.md).

# Webhooks

Real-time notifications for order and KYC status changes. The system sends a signed HTTP POST to your configured webhook URL on every relevant event.

**Event model**

Two event types are delivered:

| Event type             | Fired when                                        |
| ---------------------- | ------------------------------------------------- |
| `order.status.changed` | An order transitions to a new status              |
| `user.kyc.updated`     | A user's KYC verification is approved or declined |

All events share the same envelope (`event_id`, `event_type`, `created_at`, `data`) and the same signature scheme. The structure of the `data` object differs per event type.

**Payload shape — `order.status.changed`**

```json
{
  "event_id": "evt_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "event_type": "order.status.changed",
  "created_at": "2026-02-01T12:00:05Z",
  "data": {
    "order_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
    "status": "crypto_received",
    "user_id": "550e8400-e29b-41d4-a716-446655440000",
    "crypto_amount": "101.5",
    "crypto_currency": "USDT",
    "partner_fee": "1",
    "partner_fee_pct": 1,
    "fiat_amount": "152880.00",
    "fiat_currency": "NGN",
    "provider": "p2p",
    "payment_details_id": "987"
  }
}
```

**Payload shape — `user.kyc.updated`**

Fired when a user's KYC status changes — to `verified`, `rejected`, or to a manual compliance review hold (`UNDER_REVIEW`). Use this event to unlock order creation for the user on your side (only on `VERIFIED`; `UNDER_REVIEW` is not cleared).

```json
{
  "event_id": "evt_2d8418dd-e3a8-4463-a11e-6b54f354ca90",
  "event_type": "user.kyc.updated",
  "created_at": "2026-03-10T11:37:42Z",
  "data": {
    "user_uuid": "550e8400-e29b-41d4-a716-446655440000",
    "kyc_status": "VERIFIED"
  }
}
```

When KYC is declined, `rejection_reasons` is included:

```json
{
  "event_id": "evt_fc75484d-b374-4d05-b54e-820f3dd80e6d",
  "event_type": "user.kyc.updated",
  "created_at": "2026-03-10T12:00:00Z",
  "data": {
    "user_uuid": "550e8400-e29b-41d4-a716-446655440000",
    "kyc_status": "REJECTED",
    "rejection_reasons": ["Document expired", "Photo quality low"]
  }
}
```

When a customer is placed on a manual compliance review hold, `kyc_status` is `UNDER_REVIEW`:

```json
{
  "event_id": "evt_9a1c7f20-4d3b-4e88-9f2a-1b6d0c5e7a3f",
  "event_type": "user.kyc.updated",
  "created_at": "2026-03-10T12:05:00Z",
  "data": {
    "user_uuid": "550e8400-e29b-41d4-a716-446655440000",
    "kyc_status": "UNDER_REVIEW"
  }
}
```

`kyc_status` values: `VERIFIED` | `REJECTED` | `UNDER_REVIEW`

`UNDER_REVIEW` is a manual compliance review: the customer is NOT cleared and can take up to 24 hours (unlike an automated check of a couple of minutes). Do not unlock order creation until `VERIFIED`.

**Signature verification**

Every delivery includes two headers for HMAC-SHA256 verification:

* `X-Unigox-Signature: sha256=<hex>` — HMAC of `"<timestamp>.<raw_body>"`
* `X-Unigox-Timestamp: <unix_seconds>`

Verification pseudocode:

```
expected = HMAC-SHA256(webhook_secret, "<timestamp>.<raw_body>")
valid    = constant_time_compare(expected, signature_from_header)
```

**Delivery guarantees**

* At-least-once delivery. Your endpoint may receive the same event more than once.
* De-duplicate by `event_id`.
* Respond with any 2xx status within 10 seconds to acknowledge receipt.

**Retry policy**

Failed deliveries (non-2xx or timeout) are retried with exponential backoff: 1 min → 5 min → 15 min → 1 h → 6 h → 12 h → 24 h (max 10 attempts over \~3.5 days).

**Partner-facing statuses** (`order.status.changed`)

| Status                                   | Description                                                      |
| ---------------------------------------- | ---------------------------------------------------------------- |
| `created`                                | Order created                                                    |
| `awaiting_liquidity_provider`            | Waiting for vendor match                                         |
| `awaiting_crypto_transfer_authorization` | Partner must authorize crypto transfer                           |
| `crypto_received`                        | Crypto received in escrow                                        |
| `fiat_payment_started`                   | Buyer submitted fiat payment proof                               |
| `fiat_payment_review_started`            | Payment proof under admin review                                 |
| `awaiting_fiat_received_confirmation`    | Proof approved, awaiting seller confirmation                     |
| `completed`                              | Order completed, crypto released                                 |
| `cancelled`                              | Order cancelled or payment declined                              |
| `failed`                                 | Escrow error, or fiat-anchored safety bound exceeded (see below) |
| `dispute_started`                        | Dispute opened                                                   |

**Fiat-anchored safety bound failure**

For fiat-anchored off-ramp orders (`anchor_type: "fiat"`), if the crypto cost we quote exceeds 1.20× the expected amount at the time of settlement, the order moves to `failed` status asynchronously (not during `/initiate`). The deposit is automatically refunded from escrow back to the partner wallet. Create a new quote to retry the trade flow.

**Settlement notifications (recipient received the fiat)**

Some liquidity providers automatically confirm when the fiat payout reaches the recipient's bank account or mobile wallet. When the provider matched to an order supports this, the order moves to `completed` on its own and you receive an `order.status.changed` webhook with `status=completed` — that webhook is your signal that the recipient has actually received the money.

Whether the matched provider sends this is reported per order by the `has_fiat_settlement_notification` field on the **quote** and **order** responses:

* `true` — the matched provider sends an automatic settlement confirmation. You will receive the `completed` webhook once fiat is delivered; no manual action is needed.
* `false` — no automatic confirmation for this order. Confirm delivery yourself by calling `confirm-fiat-received` (or rely on your own banking/payment systems).

To match **only** providers that send settlement notifications, pass `has_fiat_settlement_notification: true` in the quote request — providers that can't offer it are then excluded from matching.

Availability is provider- and corridor-dependent: not every currency has a provider that sends settlement notifications, so always read the flag on each quote rather than assuming it. Manual `confirm-fiat-received` and the automatic confirmation are mutually idempotent — whichever happens first wins and the other is a safe no-op.

## Get webhook config

> Returns the current webhook configuration for your partner account,\
> including the URL and whether the webhook is enabled.<br>

````json
{"openapi":"3.0.3","info":{"title":"Unigox API Gateway","version":"1.0.0"},"tags":[{"name":"Webhooks","description":"Real-time notifications for order and KYC status changes.\nThe system sends a signed HTTP POST to your configured webhook URL on every relevant event.\n\n**Event model**\n\nTwo event types are delivered:\n\n| Event type | Fired when |\n|---|---|\n| `order.status.changed` | An order transitions to a new status |\n| `user.kyc.updated` | A user's KYC verification is approved or declined |\n\nAll events share the same envelope (`event_id`, `event_type`, `created_at`, `data`)\nand the same signature scheme. The structure of the `data` object differs per event type.\n\n**Payload shape — `order.status.changed`**\n\n```json\n{\n  \"event_id\": \"evt_a1b2c3d4-e5f6-7890-abcd-ef1234567890\",\n  \"event_type\": \"order.status.changed\",\n  \"created_at\": \"2026-02-01T12:00:05Z\",\n  \"data\": {\n    \"order_id\": \"b2c3d4e5-f6a7-8901-bcde-f12345678901\",\n    \"status\": \"crypto_received\",\n    \"user_id\": \"550e8400-e29b-41d4-a716-446655440000\",\n    \"crypto_amount\": \"101.5\",\n    \"crypto_currency\": \"USDT\",\n    \"partner_fee\": \"1\",\n    \"partner_fee_pct\": 1,\n    \"fiat_amount\": \"152880.00\",\n    \"fiat_currency\": \"NGN\",\n    \"provider\": \"p2p\",\n    \"payment_details_id\": \"987\"\n  }\n}\n```\n\n**Payload shape — `user.kyc.updated`**\n\nFired when a user's KYC status changes — to `verified`, `rejected`, or to a manual compliance\nreview hold (`UNDER_REVIEW`). Use this event to unlock order creation for the user on your side\n(only on `VERIFIED`; `UNDER_REVIEW` is not cleared).\n\n```json\n{\n  \"event_id\": \"evt_2d8418dd-e3a8-4463-a11e-6b54f354ca90\",\n  \"event_type\": \"user.kyc.updated\",\n  \"created_at\": \"2026-03-10T11:37:42Z\",\n  \"data\": {\n    \"user_uuid\": \"550e8400-e29b-41d4-a716-446655440000\",\n    \"kyc_status\": \"VERIFIED\"\n  }\n}\n```\n\nWhen KYC is declined, `rejection_reasons` is included:\n\n```json\n{\n  \"event_id\": \"evt_fc75484d-b374-4d05-b54e-820f3dd80e6d\",\n  \"event_type\": \"user.kyc.updated\",\n  \"created_at\": \"2026-03-10T12:00:00Z\",\n  \"data\": {\n    \"user_uuid\": \"550e8400-e29b-41d4-a716-446655440000\",\n    \"kyc_status\": \"REJECTED\",\n    \"rejection_reasons\": [\"Document expired\", \"Photo quality low\"]\n  }\n}\n```\n\nWhen a customer is placed on a manual compliance review hold, `kyc_status` is `UNDER_REVIEW`:\n\n```json\n{\n  \"event_id\": \"evt_9a1c7f20-4d3b-4e88-9f2a-1b6d0c5e7a3f\",\n  \"event_type\": \"user.kyc.updated\",\n  \"created_at\": \"2026-03-10T12:05:00Z\",\n  \"data\": {\n    \"user_uuid\": \"550e8400-e29b-41d4-a716-446655440000\",\n    \"kyc_status\": \"UNDER_REVIEW\"\n  }\n}\n```\n\n`kyc_status` values: `VERIFIED` | `REJECTED` | `UNDER_REVIEW`\n\n`UNDER_REVIEW` is a manual compliance review: the customer is NOT cleared and can take up to\n24 hours (unlike an automated check of a couple of minutes). Do not unlock order creation until\n`VERIFIED`.\n\n**Signature verification**\n\nEvery delivery includes two headers for HMAC-SHA256 verification:\n- `X-Unigox-Signature: sha256=<hex>` — HMAC of `\"<timestamp>.<raw_body>\"`\n- `X-Unigox-Timestamp: <unix_seconds>`\n\nVerification pseudocode:\n```\nexpected = HMAC-SHA256(webhook_secret, \"<timestamp>.<raw_body>\")\nvalid    = constant_time_compare(expected, signature_from_header)\n```\n\n**Delivery guarantees**\n\n- At-least-once delivery. Your endpoint may receive the same event more than once.\n- De-duplicate by `event_id`.\n- Respond with any 2xx status within 10 seconds to acknowledge receipt.\n\n**Retry policy**\n\nFailed deliveries (non-2xx or timeout) are retried with exponential backoff:\n1 min → 5 min → 15 min → 1 h → 6 h → 12 h → 24 h (max 10 attempts over ~3.5 days).\n\n**Partner-facing statuses** (`order.status.changed`)\n\n| Status | Description |\n|---|---|\n| `created` | Order created |\n| `awaiting_liquidity_provider` | Waiting for vendor match |\n| `awaiting_crypto_transfer_authorization` | Partner must authorize crypto transfer |\n| `crypto_received` | Crypto received in escrow |\n| `fiat_payment_started` | Buyer submitted fiat payment proof |\n| `fiat_payment_review_started` | Payment proof under admin review |\n| `awaiting_fiat_received_confirmation` | Proof approved, awaiting seller confirmation |\n| `completed` | Order completed, crypto released |\n| `cancelled` | Order cancelled or payment declined |\n| `failed` | Escrow error, or fiat-anchored safety bound exceeded (see below) |\n| `dispute_started` | Dispute opened |\n\n**Fiat-anchored safety bound failure**\n\nFor fiat-anchored off-ramp orders (`anchor_type: \"fiat\"`), if the crypto cost we quote exceeds\n1.20× the expected amount at the time of settlement, the order moves to `failed` status asynchronously\n(not during `/initiate`). The deposit is automatically refunded from escrow back to the partner wallet.\nCreate a new quote to retry the trade flow.\n\n**Settlement notifications (recipient received the fiat)**\n\nSome liquidity providers automatically confirm when the fiat payout reaches the\nrecipient's bank account or mobile wallet. When the provider matched to an order\nsupports this, the order moves to `completed` on its own and you receive an\n`order.status.changed` webhook with `status=completed` — that webhook is your signal\nthat the recipient has actually received the money.\n\nWhether the matched provider sends this is reported per order by the\n`has_fiat_settlement_notification` field on the **quote** and **order** responses:\n\n- `true` — the matched provider sends an automatic settlement confirmation. You will\n  receive the `completed` webhook once fiat is delivered; no manual action is needed.\n- `false` — no automatic confirmation for this order. Confirm delivery yourself by\n  calling `confirm-fiat-received` (or rely on your own banking/payment systems).\n\nTo match **only** providers that send settlement notifications, pass\n`has_fiat_settlement_notification: true` in the quote request — providers that can't\noffer it are then excluded from matching.\n\nAvailability is provider- and corridor-dependent: not every currency has a provider\nthat sends settlement notifications, so always read the flag on each quote rather than\nassuming it. Manual `confirm-fiat-received` and the automatic confirmation are mutually\nidempotent — whichever happens first wins and the other is a safe no-op.\n"}],"servers":[{"url":"https://api-staging.unigox.com","description":"Sandbox server"},{"url":"https://api.unigox.com","description":"Production server"}],"security":[{"ApiKeyAuth":[]}],"components":{"securitySchemes":{"ApiKeyAuth":{"type":"apiKey","in":"header","name":"X-API-Key","description":"Partner API key for authentication. Required for all partner account endpoints."}},"schemas":{"APIResponse":{"type":"object","required":["success"],"properties":{"success":{"type":"boolean","description":"Indicates if the request was successful"},"data":{"description":"Response data (structure varies by endpoint) or error information if success is false"}}},"WebhookConfigResponse":{"type":"object","properties":{"url":{"type":"string","nullable":true,"description":"Currently configured webhook URL (null if not set)"},"enabled":{"type":"boolean","description":"Whether webhook delivery is active"}}}}},"paths":{"/api/v1/partner/webhooks":{"get":{"tags":["Webhooks"],"summary":"Get webhook config","description":"Returns the current webhook configuration for your partner account,\nincluding the URL and whether the webhook is enabled.\n","operationId":"getWebhook","responses":{"200":{"description":"Webhook configuration retrieved","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/APIResponse"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/WebhookConfigResponse"}}}]}}}},"401":{"description":"Unauthorized — invalid or missing API key"}}}}}}
````

## Register webhook URL

> Register or update the webhook URL for your partner account. The URL must use HTTPS.\
> Once registered, the webhook is automatically enabled and will start receiving order status events.<br>

````json
{"openapi":"3.0.3","info":{"title":"Unigox API Gateway","version":"1.0.0"},"tags":[{"name":"Webhooks","description":"Real-time notifications for order and KYC status changes.\nThe system sends a signed HTTP POST to your configured webhook URL on every relevant event.\n\n**Event model**\n\nTwo event types are delivered:\n\n| Event type | Fired when |\n|---|---|\n| `order.status.changed` | An order transitions to a new status |\n| `user.kyc.updated` | A user's KYC verification is approved or declined |\n\nAll events share the same envelope (`event_id`, `event_type`, `created_at`, `data`)\nand the same signature scheme. The structure of the `data` object differs per event type.\n\n**Payload shape — `order.status.changed`**\n\n```json\n{\n  \"event_id\": \"evt_a1b2c3d4-e5f6-7890-abcd-ef1234567890\",\n  \"event_type\": \"order.status.changed\",\n  \"created_at\": \"2026-02-01T12:00:05Z\",\n  \"data\": {\n    \"order_id\": \"b2c3d4e5-f6a7-8901-bcde-f12345678901\",\n    \"status\": \"crypto_received\",\n    \"user_id\": \"550e8400-e29b-41d4-a716-446655440000\",\n    \"crypto_amount\": \"101.5\",\n    \"crypto_currency\": \"USDT\",\n    \"partner_fee\": \"1\",\n    \"partner_fee_pct\": 1,\n    \"fiat_amount\": \"152880.00\",\n    \"fiat_currency\": \"NGN\",\n    \"provider\": \"p2p\",\n    \"payment_details_id\": \"987\"\n  }\n}\n```\n\n**Payload shape — `user.kyc.updated`**\n\nFired when a user's KYC status changes — to `verified`, `rejected`, or to a manual compliance\nreview hold (`UNDER_REVIEW`). Use this event to unlock order creation for the user on your side\n(only on `VERIFIED`; `UNDER_REVIEW` is not cleared).\n\n```json\n{\n  \"event_id\": \"evt_2d8418dd-e3a8-4463-a11e-6b54f354ca90\",\n  \"event_type\": \"user.kyc.updated\",\n  \"created_at\": \"2026-03-10T11:37:42Z\",\n  \"data\": {\n    \"user_uuid\": \"550e8400-e29b-41d4-a716-446655440000\",\n    \"kyc_status\": \"VERIFIED\"\n  }\n}\n```\n\nWhen KYC is declined, `rejection_reasons` is included:\n\n```json\n{\n  \"event_id\": \"evt_fc75484d-b374-4d05-b54e-820f3dd80e6d\",\n  \"event_type\": \"user.kyc.updated\",\n  \"created_at\": \"2026-03-10T12:00:00Z\",\n  \"data\": {\n    \"user_uuid\": \"550e8400-e29b-41d4-a716-446655440000\",\n    \"kyc_status\": \"REJECTED\",\n    \"rejection_reasons\": [\"Document expired\", \"Photo quality low\"]\n  }\n}\n```\n\nWhen a customer is placed on a manual compliance review hold, `kyc_status` is `UNDER_REVIEW`:\n\n```json\n{\n  \"event_id\": \"evt_9a1c7f20-4d3b-4e88-9f2a-1b6d0c5e7a3f\",\n  \"event_type\": \"user.kyc.updated\",\n  \"created_at\": \"2026-03-10T12:05:00Z\",\n  \"data\": {\n    \"user_uuid\": \"550e8400-e29b-41d4-a716-446655440000\",\n    \"kyc_status\": \"UNDER_REVIEW\"\n  }\n}\n```\n\n`kyc_status` values: `VERIFIED` | `REJECTED` | `UNDER_REVIEW`\n\n`UNDER_REVIEW` is a manual compliance review: the customer is NOT cleared and can take up to\n24 hours (unlike an automated check of a couple of minutes). Do not unlock order creation until\n`VERIFIED`.\n\n**Signature verification**\n\nEvery delivery includes two headers for HMAC-SHA256 verification:\n- `X-Unigox-Signature: sha256=<hex>` — HMAC of `\"<timestamp>.<raw_body>\"`\n- `X-Unigox-Timestamp: <unix_seconds>`\n\nVerification pseudocode:\n```\nexpected = HMAC-SHA256(webhook_secret, \"<timestamp>.<raw_body>\")\nvalid    = constant_time_compare(expected, signature_from_header)\n```\n\n**Delivery guarantees**\n\n- At-least-once delivery. Your endpoint may receive the same event more than once.\n- De-duplicate by `event_id`.\n- Respond with any 2xx status within 10 seconds to acknowledge receipt.\n\n**Retry policy**\n\nFailed deliveries (non-2xx or timeout) are retried with exponential backoff:\n1 min → 5 min → 15 min → 1 h → 6 h → 12 h → 24 h (max 10 attempts over ~3.5 days).\n\n**Partner-facing statuses** (`order.status.changed`)\n\n| Status | Description |\n|---|---|\n| `created` | Order created |\n| `awaiting_liquidity_provider` | Waiting for vendor match |\n| `awaiting_crypto_transfer_authorization` | Partner must authorize crypto transfer |\n| `crypto_received` | Crypto received in escrow |\n| `fiat_payment_started` | Buyer submitted fiat payment proof |\n| `fiat_payment_review_started` | Payment proof under admin review |\n| `awaiting_fiat_received_confirmation` | Proof approved, awaiting seller confirmation |\n| `completed` | Order completed, crypto released |\n| `cancelled` | Order cancelled or payment declined |\n| `failed` | Escrow error, or fiat-anchored safety bound exceeded (see below) |\n| `dispute_started` | Dispute opened |\n\n**Fiat-anchored safety bound failure**\n\nFor fiat-anchored off-ramp orders (`anchor_type: \"fiat\"`), if the crypto cost we quote exceeds\n1.20× the expected amount at the time of settlement, the order moves to `failed` status asynchronously\n(not during `/initiate`). The deposit is automatically refunded from escrow back to the partner wallet.\nCreate a new quote to retry the trade flow.\n\n**Settlement notifications (recipient received the fiat)**\n\nSome liquidity providers automatically confirm when the fiat payout reaches the\nrecipient's bank account or mobile wallet. When the provider matched to an order\nsupports this, the order moves to `completed` on its own and you receive an\n`order.status.changed` webhook with `status=completed` — that webhook is your signal\nthat the recipient has actually received the money.\n\nWhether the matched provider sends this is reported per order by the\n`has_fiat_settlement_notification` field on the **quote** and **order** responses:\n\n- `true` — the matched provider sends an automatic settlement confirmation. You will\n  receive the `completed` webhook once fiat is delivered; no manual action is needed.\n- `false` — no automatic confirmation for this order. Confirm delivery yourself by\n  calling `confirm-fiat-received` (or rely on your own banking/payment systems).\n\nTo match **only** providers that send settlement notifications, pass\n`has_fiat_settlement_notification: true` in the quote request — providers that can't\noffer it are then excluded from matching.\n\nAvailability is provider- and corridor-dependent: not every currency has a provider\nthat sends settlement notifications, so always read the flag on each quote rather than\nassuming it. Manual `confirm-fiat-received` and the automatic confirmation are mutually\nidempotent — whichever happens first wins and the other is a safe no-op.\n"}],"servers":[{"url":"https://api-staging.unigox.com","description":"Sandbox server"},{"url":"https://api.unigox.com","description":"Production server"}],"security":[{"ApiKeyAuth":[]}],"components":{"securitySchemes":{"ApiKeyAuth":{"type":"apiKey","in":"header","name":"X-API-Key","description":"Partner API key for authentication. Required for all partner account endpoints."}},"schemas":{"RegisterWebhookRequest":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri","description":"HTTPS URL that will receive webhook events"}}},"APIResponse":{"type":"object","required":["success"],"properties":{"success":{"type":"boolean","description":"Indicates if the request was successful"},"data":{"description":"Response data (structure varies by endpoint) or error information if success is false"}}},"WebhookConfigResponse":{"type":"object","properties":{"url":{"type":"string","nullable":true,"description":"Currently configured webhook URL (null if not set)"},"enabled":{"type":"boolean","description":"Whether webhook delivery is active"}}},"ErrorResponse":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean"},"data":{"type":"object","required":["error_key"],"properties":{"error_key":{"type":"string","description":"Standardized error key for client error handling"}}}}}}},"paths":{"/api/v1/partner/webhooks":{"post":{"tags":["Webhooks"],"summary":"Register webhook URL","description":"Register or update the webhook URL for your partner account. The URL must use HTTPS.\nOnce registered, the webhook is automatically enabled and will start receiving order status events.\n","operationId":"registerWebhook","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterWebhookRequest"}}}},"responses":{"200":{"description":"Webhook registered successfully","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/APIResponse"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/WebhookConfigResponse"}}}]}}}},"400":{"description":"Bad request — missing or invalid URL","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Unauthorized — invalid or missing API key"}}}}}}
````

## Delete webhook

> Remove the webhook URL and disable webhook delivery for your partner account.\
> No further events will be sent until a new URL is registered.<br>

````json
{"openapi":"3.0.3","info":{"title":"Unigox API Gateway","version":"1.0.0"},"tags":[{"name":"Webhooks","description":"Real-time notifications for order and KYC status changes.\nThe system sends a signed HTTP POST to your configured webhook URL on every relevant event.\n\n**Event model**\n\nTwo event types are delivered:\n\n| Event type | Fired when |\n|---|---|\n| `order.status.changed` | An order transitions to a new status |\n| `user.kyc.updated` | A user's KYC verification is approved or declined |\n\nAll events share the same envelope (`event_id`, `event_type`, `created_at`, `data`)\nand the same signature scheme. The structure of the `data` object differs per event type.\n\n**Payload shape — `order.status.changed`**\n\n```json\n{\n  \"event_id\": \"evt_a1b2c3d4-e5f6-7890-abcd-ef1234567890\",\n  \"event_type\": \"order.status.changed\",\n  \"created_at\": \"2026-02-01T12:00:05Z\",\n  \"data\": {\n    \"order_id\": \"b2c3d4e5-f6a7-8901-bcde-f12345678901\",\n    \"status\": \"crypto_received\",\n    \"user_id\": \"550e8400-e29b-41d4-a716-446655440000\",\n    \"crypto_amount\": \"101.5\",\n    \"crypto_currency\": \"USDT\",\n    \"partner_fee\": \"1\",\n    \"partner_fee_pct\": 1,\n    \"fiat_amount\": \"152880.00\",\n    \"fiat_currency\": \"NGN\",\n    \"provider\": \"p2p\",\n    \"payment_details_id\": \"987\"\n  }\n}\n```\n\n**Payload shape — `user.kyc.updated`**\n\nFired when a user's KYC status changes — to `verified`, `rejected`, or to a manual compliance\nreview hold (`UNDER_REVIEW`). Use this event to unlock order creation for the user on your side\n(only on `VERIFIED`; `UNDER_REVIEW` is not cleared).\n\n```json\n{\n  \"event_id\": \"evt_2d8418dd-e3a8-4463-a11e-6b54f354ca90\",\n  \"event_type\": \"user.kyc.updated\",\n  \"created_at\": \"2026-03-10T11:37:42Z\",\n  \"data\": {\n    \"user_uuid\": \"550e8400-e29b-41d4-a716-446655440000\",\n    \"kyc_status\": \"VERIFIED\"\n  }\n}\n```\n\nWhen KYC is declined, `rejection_reasons` is included:\n\n```json\n{\n  \"event_id\": \"evt_fc75484d-b374-4d05-b54e-820f3dd80e6d\",\n  \"event_type\": \"user.kyc.updated\",\n  \"created_at\": \"2026-03-10T12:00:00Z\",\n  \"data\": {\n    \"user_uuid\": \"550e8400-e29b-41d4-a716-446655440000\",\n    \"kyc_status\": \"REJECTED\",\n    \"rejection_reasons\": [\"Document expired\", \"Photo quality low\"]\n  }\n}\n```\n\nWhen a customer is placed on a manual compliance review hold, `kyc_status` is `UNDER_REVIEW`:\n\n```json\n{\n  \"event_id\": \"evt_9a1c7f20-4d3b-4e88-9f2a-1b6d0c5e7a3f\",\n  \"event_type\": \"user.kyc.updated\",\n  \"created_at\": \"2026-03-10T12:05:00Z\",\n  \"data\": {\n    \"user_uuid\": \"550e8400-e29b-41d4-a716-446655440000\",\n    \"kyc_status\": \"UNDER_REVIEW\"\n  }\n}\n```\n\n`kyc_status` values: `VERIFIED` | `REJECTED` | `UNDER_REVIEW`\n\n`UNDER_REVIEW` is a manual compliance review: the customer is NOT cleared and can take up to\n24 hours (unlike an automated check of a couple of minutes). Do not unlock order creation until\n`VERIFIED`.\n\n**Signature verification**\n\nEvery delivery includes two headers for HMAC-SHA256 verification:\n- `X-Unigox-Signature: sha256=<hex>` — HMAC of `\"<timestamp>.<raw_body>\"`\n- `X-Unigox-Timestamp: <unix_seconds>`\n\nVerification pseudocode:\n```\nexpected = HMAC-SHA256(webhook_secret, \"<timestamp>.<raw_body>\")\nvalid    = constant_time_compare(expected, signature_from_header)\n```\n\n**Delivery guarantees**\n\n- At-least-once delivery. Your endpoint may receive the same event more than once.\n- De-duplicate by `event_id`.\n- Respond with any 2xx status within 10 seconds to acknowledge receipt.\n\n**Retry policy**\n\nFailed deliveries (non-2xx or timeout) are retried with exponential backoff:\n1 min → 5 min → 15 min → 1 h → 6 h → 12 h → 24 h (max 10 attempts over ~3.5 days).\n\n**Partner-facing statuses** (`order.status.changed`)\n\n| Status | Description |\n|---|---|\n| `created` | Order created |\n| `awaiting_liquidity_provider` | Waiting for vendor match |\n| `awaiting_crypto_transfer_authorization` | Partner must authorize crypto transfer |\n| `crypto_received` | Crypto received in escrow |\n| `fiat_payment_started` | Buyer submitted fiat payment proof |\n| `fiat_payment_review_started` | Payment proof under admin review |\n| `awaiting_fiat_received_confirmation` | Proof approved, awaiting seller confirmation |\n| `completed` | Order completed, crypto released |\n| `cancelled` | Order cancelled or payment declined |\n| `failed` | Escrow error, or fiat-anchored safety bound exceeded (see below) |\n| `dispute_started` | Dispute opened |\n\n**Fiat-anchored safety bound failure**\n\nFor fiat-anchored off-ramp orders (`anchor_type: \"fiat\"`), if the crypto cost we quote exceeds\n1.20× the expected amount at the time of settlement, the order moves to `failed` status asynchronously\n(not during `/initiate`). The deposit is automatically refunded from escrow back to the partner wallet.\nCreate a new quote to retry the trade flow.\n\n**Settlement notifications (recipient received the fiat)**\n\nSome liquidity providers automatically confirm when the fiat payout reaches the\nrecipient's bank account or mobile wallet. When the provider matched to an order\nsupports this, the order moves to `completed` on its own and you receive an\n`order.status.changed` webhook with `status=completed` — that webhook is your signal\nthat the recipient has actually received the money.\n\nWhether the matched provider sends this is reported per order by the\n`has_fiat_settlement_notification` field on the **quote** and **order** responses:\n\n- `true` — the matched provider sends an automatic settlement confirmation. You will\n  receive the `completed` webhook once fiat is delivered; no manual action is needed.\n- `false` — no automatic confirmation for this order. Confirm delivery yourself by\n  calling `confirm-fiat-received` (or rely on your own banking/payment systems).\n\nTo match **only** providers that send settlement notifications, pass\n`has_fiat_settlement_notification: true` in the quote request — providers that can't\noffer it are then excluded from matching.\n\nAvailability is provider- and corridor-dependent: not every currency has a provider\nthat sends settlement notifications, so always read the flag on each quote rather than\nassuming it. Manual `confirm-fiat-received` and the automatic confirmation are mutually\nidempotent — whichever happens first wins and the other is a safe no-op.\n"}],"servers":[{"url":"https://api-staging.unigox.com","description":"Sandbox server"},{"url":"https://api.unigox.com","description":"Production server"}],"security":[{"ApiKeyAuth":[]}],"components":{"securitySchemes":{"ApiKeyAuth":{"type":"apiKey","in":"header","name":"X-API-Key","description":"Partner API key for authentication. Required for all partner account endpoints."}},"schemas":{"APIResponse":{"type":"object","required":["success"],"properties":{"success":{"type":"boolean","description":"Indicates if the request was successful"},"data":{"description":"Response data (structure varies by endpoint) or error information if success is false"}}}}},"paths":{"/api/v1/partner/webhooks/{id}":{"delete":{"tags":["Webhooks"],"summary":"Delete webhook","description":"Remove the webhook URL and disable webhook delivery for your partner account.\nNo further events will be sent until a new URL is registered.\n","operationId":"deleteWebhook","parameters":[{"name":"id","in":"path","required":true,"description":"Webhook ID (any value accepted — the system uses the authenticated partner context)","schema":{"type":"string"}}],"responses":{"200":{"description":"Webhook deleted successfully","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/APIResponse"},{"type":"object","properties":{"data":{"type":"object","properties":{"message":{"type":"string"}}}}}]}}}},"401":{"description":"Unauthorized — invalid or missing API key"}}}}}}
````

## Test webhook

> Send a signed test event to your configured webhook URL. Uses the same HMAC-SHA256\
> signature scheme as production events. Use this to verify your endpoint is reachable\
> and correctly validating signatures.<br>

````json
{"openapi":"3.0.3","info":{"title":"Unigox API Gateway","version":"1.0.0"},"tags":[{"name":"Webhooks","description":"Real-time notifications for order and KYC status changes.\nThe system sends a signed HTTP POST to your configured webhook URL on every relevant event.\n\n**Event model**\n\nTwo event types are delivered:\n\n| Event type | Fired when |\n|---|---|\n| `order.status.changed` | An order transitions to a new status |\n| `user.kyc.updated` | A user's KYC verification is approved or declined |\n\nAll events share the same envelope (`event_id`, `event_type`, `created_at`, `data`)\nand the same signature scheme. The structure of the `data` object differs per event type.\n\n**Payload shape — `order.status.changed`**\n\n```json\n{\n  \"event_id\": \"evt_a1b2c3d4-e5f6-7890-abcd-ef1234567890\",\n  \"event_type\": \"order.status.changed\",\n  \"created_at\": \"2026-02-01T12:00:05Z\",\n  \"data\": {\n    \"order_id\": \"b2c3d4e5-f6a7-8901-bcde-f12345678901\",\n    \"status\": \"crypto_received\",\n    \"user_id\": \"550e8400-e29b-41d4-a716-446655440000\",\n    \"crypto_amount\": \"101.5\",\n    \"crypto_currency\": \"USDT\",\n    \"partner_fee\": \"1\",\n    \"partner_fee_pct\": 1,\n    \"fiat_amount\": \"152880.00\",\n    \"fiat_currency\": \"NGN\",\n    \"provider\": \"p2p\",\n    \"payment_details_id\": \"987\"\n  }\n}\n```\n\n**Payload shape — `user.kyc.updated`**\n\nFired when a user's KYC status changes — to `verified`, `rejected`, or to a manual compliance\nreview hold (`UNDER_REVIEW`). Use this event to unlock order creation for the user on your side\n(only on `VERIFIED`; `UNDER_REVIEW` is not cleared).\n\n```json\n{\n  \"event_id\": \"evt_2d8418dd-e3a8-4463-a11e-6b54f354ca90\",\n  \"event_type\": \"user.kyc.updated\",\n  \"created_at\": \"2026-03-10T11:37:42Z\",\n  \"data\": {\n    \"user_uuid\": \"550e8400-e29b-41d4-a716-446655440000\",\n    \"kyc_status\": \"VERIFIED\"\n  }\n}\n```\n\nWhen KYC is declined, `rejection_reasons` is included:\n\n```json\n{\n  \"event_id\": \"evt_fc75484d-b374-4d05-b54e-820f3dd80e6d\",\n  \"event_type\": \"user.kyc.updated\",\n  \"created_at\": \"2026-03-10T12:00:00Z\",\n  \"data\": {\n    \"user_uuid\": \"550e8400-e29b-41d4-a716-446655440000\",\n    \"kyc_status\": \"REJECTED\",\n    \"rejection_reasons\": [\"Document expired\", \"Photo quality low\"]\n  }\n}\n```\n\nWhen a customer is placed on a manual compliance review hold, `kyc_status` is `UNDER_REVIEW`:\n\n```json\n{\n  \"event_id\": \"evt_9a1c7f20-4d3b-4e88-9f2a-1b6d0c5e7a3f\",\n  \"event_type\": \"user.kyc.updated\",\n  \"created_at\": \"2026-03-10T12:05:00Z\",\n  \"data\": {\n    \"user_uuid\": \"550e8400-e29b-41d4-a716-446655440000\",\n    \"kyc_status\": \"UNDER_REVIEW\"\n  }\n}\n```\n\n`kyc_status` values: `VERIFIED` | `REJECTED` | `UNDER_REVIEW`\n\n`UNDER_REVIEW` is a manual compliance review: the customer is NOT cleared and can take up to\n24 hours (unlike an automated check of a couple of minutes). Do not unlock order creation until\n`VERIFIED`.\n\n**Signature verification**\n\nEvery delivery includes two headers for HMAC-SHA256 verification:\n- `X-Unigox-Signature: sha256=<hex>` — HMAC of `\"<timestamp>.<raw_body>\"`\n- `X-Unigox-Timestamp: <unix_seconds>`\n\nVerification pseudocode:\n```\nexpected = HMAC-SHA256(webhook_secret, \"<timestamp>.<raw_body>\")\nvalid    = constant_time_compare(expected, signature_from_header)\n```\n\n**Delivery guarantees**\n\n- At-least-once delivery. Your endpoint may receive the same event more than once.\n- De-duplicate by `event_id`.\n- Respond with any 2xx status within 10 seconds to acknowledge receipt.\n\n**Retry policy**\n\nFailed deliveries (non-2xx or timeout) are retried with exponential backoff:\n1 min → 5 min → 15 min → 1 h → 6 h → 12 h → 24 h (max 10 attempts over ~3.5 days).\n\n**Partner-facing statuses** (`order.status.changed`)\n\n| Status | Description |\n|---|---|\n| `created` | Order created |\n| `awaiting_liquidity_provider` | Waiting for vendor match |\n| `awaiting_crypto_transfer_authorization` | Partner must authorize crypto transfer |\n| `crypto_received` | Crypto received in escrow |\n| `fiat_payment_started` | Buyer submitted fiat payment proof |\n| `fiat_payment_review_started` | Payment proof under admin review |\n| `awaiting_fiat_received_confirmation` | Proof approved, awaiting seller confirmation |\n| `completed` | Order completed, crypto released |\n| `cancelled` | Order cancelled or payment declined |\n| `failed` | Escrow error, or fiat-anchored safety bound exceeded (see below) |\n| `dispute_started` | Dispute opened |\n\n**Fiat-anchored safety bound failure**\n\nFor fiat-anchored off-ramp orders (`anchor_type: \"fiat\"`), if the crypto cost we quote exceeds\n1.20× the expected amount at the time of settlement, the order moves to `failed` status asynchronously\n(not during `/initiate`). The deposit is automatically refunded from escrow back to the partner wallet.\nCreate a new quote to retry the trade flow.\n\n**Settlement notifications (recipient received the fiat)**\n\nSome liquidity providers automatically confirm when the fiat payout reaches the\nrecipient's bank account or mobile wallet. When the provider matched to an order\nsupports this, the order moves to `completed` on its own and you receive an\n`order.status.changed` webhook with `status=completed` — that webhook is your signal\nthat the recipient has actually received the money.\n\nWhether the matched provider sends this is reported per order by the\n`has_fiat_settlement_notification` field on the **quote** and **order** responses:\n\n- `true` — the matched provider sends an automatic settlement confirmation. You will\n  receive the `completed` webhook once fiat is delivered; no manual action is needed.\n- `false` — no automatic confirmation for this order. Confirm delivery yourself by\n  calling `confirm-fiat-received` (or rely on your own banking/payment systems).\n\nTo match **only** providers that send settlement notifications, pass\n`has_fiat_settlement_notification: true` in the quote request — providers that can't\noffer it are then excluded from matching.\n\nAvailability is provider- and corridor-dependent: not every currency has a provider\nthat sends settlement notifications, so always read the flag on each quote rather than\nassuming it. Manual `confirm-fiat-received` and the automatic confirmation are mutually\nidempotent — whichever happens first wins and the other is a safe no-op.\n"}],"servers":[{"url":"https://api-staging.unigox.com","description":"Sandbox server"},{"url":"https://api.unigox.com","description":"Production server"}],"security":[{"ApiKeyAuth":[]}],"components":{"securitySchemes":{"ApiKeyAuth":{"type":"apiKey","in":"header","name":"X-API-Key","description":"Partner API key for authentication. Required for all partner account endpoints."}},"schemas":{"APIResponse":{"type":"object","required":["success"],"properties":{"success":{"type":"boolean","description":"Indicates if the request was successful"},"data":{"description":"Response data (structure varies by endpoint) or error information if success is false"}}},"WebhookTestPingResponse":{"type":"object","properties":{"delivered":{"type":"boolean","description":"Whether the test event was delivered successfully"},"http_status":{"type":"integer","description":"HTTP status code returned by the webhook endpoint"}}},"ErrorResponse":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean"},"data":{"type":"object","required":["error_key"],"properties":{"error_key":{"type":"string","description":"Standardized error key for client error handling"}}}}}}},"paths":{"/api/v1/partner/webhooks/test":{"post":{"tags":["Webhooks"],"summary":"Test webhook","description":"Send a signed test event to your configured webhook URL. Uses the same HMAC-SHA256\nsignature scheme as production events. Use this to verify your endpoint is reachable\nand correctly validating signatures.\n","operationId":"testPingWebhook","responses":{"200":{"description":"Test event delivered successfully","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/APIResponse"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/WebhookTestPingResponse"}}}]}}}},"400":{"description":"Webhook URL not configured","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Unauthorized — invalid or missing API key"},"502":{"description":"Webhook delivery failed — target endpoint returned non-2xx or timed out","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}}}}
````
