# Callbacks


The gateway calls your callback URL to tell you about a deposit. Each forwarded deposit produces its own confirmed callback, and, when the order was created with `pending=1`, its own pending callback. Custom query parameters of your callback URL (such as `order` and `nonce`) are sent back unchanged in every callback.

## Events

| Event | `pending` | When |
| --- | --- | --- |
| Pending (detection) | `"1"` | A deposit of at least the minimum was first seen by the indexer. Only with `pending=1` at create. Do not fulfill an order on this event. |
| Confirmed | `"0"` | The deposit was forwarded to your payout address and the forwarding transaction is confirmed. |

Pending and confirmed callbacks have different `uuid` values. Detection uses indexed chain data rather than a separate mempool subscription. Deposits below the minimum, and transfers of other tokens, produce no callback.

## Transports

| Create parameter | Method | Content |
| --- | --- | --- |
| none (default) | `GET` | Fields appended to the query string after your own parameters, sorted by field name. |
| `post=1` | `POST` | `application/x-www-form-urlencoded` body. Your own query parameters stay in the URL. |
| `json=1` | `POST` | `application/json` body with string values. Your own query parameters stay in the URL. `json=1` wins over `post=1`. |

Avoid field names from the table below in your own query parameters. A default `GET` callback looks like this:

```
GET /hook?order=1001&nonce=7f3a9c&address_in=TDepositAddressExample0000000000&address_out=TXyzPayoutAddressExample0000000000000&coin=trc20_usdt&confirmations=21&fee_coin=1.800000&pending=0&price=1.0002&txid_in=9f2c1a0e5b7d4c3a8e6f1b2d3c4a5e6f7a8b9c0d1e2f30415263748596a7b8c9&txid_out=1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f809&uuid=8c4e2a61-5b3d-47f9-a0e2-1d6c9b7f3e45&value_coin=100.000000&value_forwarded_coin=98.200000
x-ca-signature: <base64 RSA-SHA256 signature>
```

## Fields

All values are strings. The examples below are shown as JSON; with `GET` and `post=1` the same fields arrive as query or form parameters.

### Detection (pending) callback

Sent to the order callback URL when a deposit of at least the ticker minimum is first seen and the order was created with pending=1. GET sends the fields in the query string after any custom parameters; post=1 sends a form body; json=1 sends a JSON body. All values are strings. Header x-ca-signature carries the base64 RSA-SHA256 signature. Acknowledge with HTTP 200 and the body `*ok*` within 30 seconds. Do not fulfill an order on a pending callback.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `uuid` | string | yes | Event identifier, stable across retries. Deduplicate on it. Pending and confirmed events have different uuids. |
| `address_in` | string | yes | Deposit address of the order. |
| `address_out` | string | yes | Payout address of the order. |
| `txid_in` | string | yes | Hash of the inbound deposit transaction. |
| `coin` | string | yes | Ticker with the slash replaced by an underscore, for example trc20_usdt. |
| `pending` | string, always `1` | yes | "1" for a detection callback. |
| `price` | string | no | USD price of one whole token, up to 8 decimals. Present only when a fresh price existed at event creation. |

```json
{
  "uuid": "3f1b7c9e-8a52-4d0e-9c41-6b2e5f0a7d13",
  "address_in": "TDepositAddressExample0000000000",
  "address_out": "TXyzPayoutAddressExample0000000000000",
  "txid_in": "9f2c1a0e5b7d4c3a8e6f1b2d3c4a5e6f7a8b9c0d1e2f30415263748596a7b8c9",
  "coin": "trc20_usdt",
  "pending": "1",
  "price": "1.0002"
}
```

### Confirmed callback

Sent after the deposit was forwarded to the payout address and the forwarding transaction is confirmed. Same transports, signature and acknowledgement as the pending callback. Each forwarded deposit produces its own confirmed callback with its own uuid.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `uuid` | string | yes | Event identifier, stable across retries. Deduplicate on it. |
| `address_in` | string | yes | Deposit address of the order. |
| `address_out` | string | yes | Payout address of the order. |
| `txid_in` | string | yes | Hash of the inbound deposit transaction this callback is about. |
| `coin` | string | yes | Ticker with the slash replaced by an underscore, for example trc20_usdt. |
| `pending` | string, always `0` | yes | "0" for a confirmed callback. |
| `price` | string | no | USD price of one whole token, up to 8 decimals. Present only when a fresh price existed at event creation. |
| `txid_out` | string | yes | Hash of the forwarding transaction to the payout address. |
| `confirmations` | string | yes | Confirmations of the inbound deposit at event creation; at least 1. |
| `value_coin` | string | yes | Inbound amount before fees, in whole asset units. |
| `value_forwarded_coin` | string | yes | Amount forwarded to the payout address after fees. |
| `fee_coin` | string | yes | Service and charged network fees, in whole asset units. |
| `value_coin_convert` | string | no | JSON object serialized as a string, keyed by upper-case fiat code with 2-decimal values. Only for orders created with convert=1 and only when a fresh price existed. |
| `value_forwarded_coin_convert` | string | no | Same shape as value_coin_convert, for the forwarded amount. |

```json
{
  "uuid": "8c4e2a61-5b3d-47f9-a0e2-1d6c9b7f3e45",
  "address_in": "TDepositAddressExample0000000000",
  "address_out": "TXyzPayoutAddressExample0000000000000",
  "txid_in": "9f2c1a0e5b7d4c3a8e6f1b2d3c4a5e6f7a8b9c0d1e2f30415263748596a7b8c9",
  "coin": "trc20_usdt",
  "pending": "0",
  "price": "1.0002",
  "txid_out": "1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f809",
  "confirmations": "21",
  "value_coin": "100.000000",
  "value_forwarded_coin": "98.200000",
  "fee_coin": "1.800000",
  "value_coin_convert": "{\"EUR\":\"92.00\",\"USD\":\"100.02\"}",
  "value_forwarded_coin_convert": "{\"EUR\":\"90.34\",\"USD\":\"98.22\"}"
}
```

## Signature

Every callback carries the header `x-ca-signature`. For `GET` it signs the full request URL, for `POST` the exact raw body. Verify it before trusting anything else in the request. See [Verify callback signatures](verify-signature.md).

## Acknowledge

Answer with HTTP status `200` and the body `*ok*` (plain `ok` is accepted too, in any letter case) within 30 seconds. Any other status, body or a timeout counts as a failure and the callback is retried. Redirects are not followed, and destinations resolving to private addresses are refused at send time.

## Retries

There are eight attempts: the initial send, then after 30 seconds, 2 minutes, 10 minutes, 30 minutes, 2 hours, 6 hours and 24 hours, each with up to 10% added jitter. The `uuid` stays the same across retries. Duplicates are possible, so store the `uuid` and ignore events you have processed. A confirmed callback whose retries are exhausted shows as `sent` in [the logs](api-reference.md), which you can poll to reconcile.

## Order

The pending and the confirmed callback of a deposit can arrive in either order after retries. A pending callback never replaces a confirmed one: once you have processed `pending` equal to `"0"` for a deposit, ignore a late `"1"`.

## Key rotation

Callbacks carry no key id and no timestamp, so a rotation is announced rather than negotiated. The operator creates a new key pair, installs the private key on the workers and the public key on the API, and restarts them together; from that moment every callback, including retries of older ones, is signed with the new key. After a failed verification, fetch `GET /pubkey/` once, replace your stored key, and verify again before rejecting the callback. A callback that still fails with the fresh key is not from the gateway.

## Checklist

- Verify `x-ca-signature` against the key from `GET /pubkey/`.
- Compare the nonce and order reference in the query string with your stored order.
- Check `address_in`, `coin` and the amount against the order.
- Deduplicate on `uuid` before doing work.
- Fulfill only on `pending` equal to `"0"`.
- Answer `*ok*`.
