# API reference

> Every merchant endpoint, request field, response and webhook payload, generated from openapi.yaml.

This page is generated from [`openapi.yaml`](https://github.com/loruorg/turmite/blob/main/openapi.yaml) in the repository. The file is an OpenAPI 3.1 document. Load it into your own tools to generate a client or to test requests.

The base URL is your site URL, for example `https://pay.example.com`. Send the project API key in the `x-api-key` header. Payout endpoints also need a JWT from `POST /v1/auth` in the `Authorization: Bearer` header. The [Merchant API guide](/docs/merchant-api/) explains each flow with examples.

## Payments

Create payments and read their status.

### GET /v1/payment

List payments

Auth: `apiKey`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `limit` | query | `integer` | no | Payments per page. Turmite changes a value below 1 to 1 and a value above 500 to 500. |
| `page` | query | `integer` | no | Page number. The first page is 0. |
| `sortBy` | query | `string` | no |  One of: `payment_id`, `payment_status`, `pay_address`, `price_amount`, `price_currency`, `pay_amount`, `actually_paid`, `pay_currency`, `order_id`, `order_description`, `purchase_id`, `outcome_amount`, `outcome_currency`, `created_at`, `updated_at`. |
| `orderBy` | query | `string` | no |  One of: `asc`, `desc`. |
| `dateFrom` | query | `string` | no | Earliest `created_at`. A date or a date-time. |
| `dateTo` | query | `string` | no | Latest `created_at`. A date or a date-time. |
| `invoiceId` | query | `integer (int64)` | no | Only the payments of this invoice. |
| `payment_status` | query | `string` | no | Only the payments with this status. One of: `waiting`, `confirming`, `confirmed`, `sending`, `partially_paid`, `finished`, `failed`, `refunded`, `expired`. |
| `order_id` | query | `string` | no | Only the payments with this `order_id`. |

Response `200`: One page of payments.

```json
{
  "data": [
    {
      "invoice_id": 0,
      "payment_status": "waiting",
      "pay_address": "string",
      "payin_extra_id": "string",
      "price_amount": 0,
      "price_currency": "usd",
      "pay_amount": 0,
      "actually_paid": 0,
      "pay_currency": "btc",
      "order_id": "string",
      "order_description": "string",
      "outcome_amount": 0,
      "outcome_currency": "string",
      "network": "btc",
      "network_precision": 0,
      "smart_contract": "string",
      "payin_hash": "string",
      "payout_hash": "string",
      "payment_url": "string",
      "created_at": "2026-10-23T12:00:00Z",
      "updated_at": "2026-10-23T12:00:00Z",
      "expiration_estimate_date": "2026-10-23T12:00:00Z",
      "parent_payment_id": null,
      "type": "crypto2crypto",
      "time_limit": null,
      "burning_percent": null,
      "payment_extra_ids": null,
      "amount_received": 0,
      "actually_paid_at_fiat": 0,
      "fee": {
        "currency": "btc",
        "depositFee": 0,
        "withdrawalFee": 0,
        "serviceFee": 0
      },
      "payment_id": 0,
      "purchase_id": 6084815412
    }
  ],
  "limit": 0,
  "page": 0,
  "pagesCount": 0,
  "total": 0
}
```

Response `400`: The request is not valid.

```json
{
  "status": false,
  "statusCode": 400,
  "code": "INVALID_REQUEST_PARAMS",
  "message": "`price_amount` must be greater than 0."
}
```

Response `401`: The API key or the bearer token is missing or not valid.

```json
{
  "status": false,
  "statusCode": 401,
  "code": "AUTH_REQUIRED",
  "message": "Invalid API key."
}
```

Response `429`: Too many requests.

```json
{
  "status": false,
  "statusCode": 429,
  "code": "TOO_MANY_REQUESTS",
  "message": "too many requests, try again later"
}
```

Response `500`: The server failed. The message has no details.

```json
{
  "status": false,
  "statusCode": 500,
  "code": "INTERNAL_ERROR",
  "message": "Internal error."
}
```

### POST /v1/payment

Create a payment

Reserves a deposit address and fixes the amount to pay in `pay_currency`. Send the customer to your own
page with `pay_address` and `pay_amount`, or use an invoice for the hosted checkout. A retry with the same
`idempotency_key` returns the first payment. In this response `payment_id` and `purchase_id` are strings.

Auth: `apiKey`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `Idempotency-Key` | header | `string` | no | The same as `idempotency_key` in the body. The body field has priority. |

| Field | Type | Required | Description |
|---|---|---|---|
| `price_amount` | `number \| string` | yes | A positive amount, as a JSON number or a decimal string. |
| `price_currency` | `string` | yes | A fiat code or a ticker. |
| `pay_currency` | `string` | yes | The ticker the customer pays with. |
| `pay_amount` | `number \| string` | no | A fixed amount in `pay_currency`. Without it, Turmite converts `price_amount` at the current rate. |
| `order_id` | `string` | no | Your order reference. Webhooks include it. |
| `order_description` | `string` | no |  |
| `ipn_callback_url` | `string (uri)` | no | Turmite sends payment webhooks to this URL. On mainnet it must be a public `https` URL. |
| `success_url` | `string (uri)` | no | Where the checkout sends the customer after payment. |
| `cancel_url` | `string (uri)` | no |  |
| `purchase_id` | `string` | no | Your id for the purchase. Without it, Turmite makes numeric text. |
| `idempotency_key` | `string` | no | A request with a key that the project used before returns the first payment. This is also true for two requests at the same time. Turmite does not compare the other fields. |

```json
{
  "price_amount": 100,
  "price_currency": "usd",
  "pay_currency": "btc",
  "order_id": "1042",
  "order_description": "Order #1042",
  "ipn_callback_url": "https://shop.example.com/turmite/ipn",
  "idempotency_key": "order-1042"
}
```

Response `201`: The new payment.

```json
{
  "payment_id": "5077125051",
  "invoice_id": null,
  "payment_status": "waiting",
  "pay_address": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
  "payin_extra_id": null,
  "price_amount": 100,
  "price_currency": "usd",
  "pay_amount": 0.00123,
  "actually_paid": 0,
  "pay_currency": "btc",
  "order_id": "1042",
  "order_description": "Order #1042",
  "purchase_id": "6084815412",
  "outcome_amount": null,
  "outcome_currency": "btc",
  "network": "btc",
  "network_precision": 8,
  "smart_contract": null,
  "payin_hash": null,
  "payout_hash": null,
  "payment_url": "https://pay.example.com/pay/Q7mX2pK9vT4nB8cR1sW6yZ3h",
  "created_at": "2026-10-23T12:00:00.000Z",
  "updated_at": "2026-10-23T12:00:00.000Z",
  "expiration_estimate_date": "2026-10-23T12:15:00.000Z",
  "parent_payment_id": null,
  "type": "crypto2crypto",
  "time_limit": null,
  "burning_percent": null,
  "payment_extra_ids": null,
  "amount_received": 0.00123,
  "actually_paid_at_fiat": 0,
  "fee": {
    "currency": "btc",
    "depositFee": 0,
    "withdrawalFee": 0,
    "serviceFee": 0
  }
}
```

Response `400`: The request is not valid.

```json
{
  "status": false,
  "statusCode": 400,
  "code": "INVALID_REQUEST_PARAMS",
  "message": "`price_amount` must be greater than 0."
}
```

Response `401`: The API key or the bearer token is missing or not valid.

```json
{
  "status": false,
  "statusCode": 401,
  "code": "AUTH_REQUIRED",
  "message": "Invalid API key."
}
```

Response `429`: Too many requests.

```json
{
  "status": false,
  "statusCode": 429,
  "code": "TOO_MANY_REQUESTS",
  "message": "too many requests, try again later"
}
```

Response `500`: The server failed. The message has no details.

```json
{
  "status": false,
  "statusCode": 500,
  "code": "INTERNAL_ERROR",
  "message": "Internal error."
}
```

Response `503`: A service that the request needs is not available, for example the exchange rates. Try again later.

```json
{
  "status": false,
  "statusCode": 503,
  "code": "UNAVAILABLE",
  "message": "Exchange rates are not available. Try again in a minute."
}
```

### GET /v1/payment/{payment_id}

Get a payment

Ship the order only when `payment_status` is `finished`. Use webhooks instead of frequent polls.

Auth: `apiKey`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `payment_id` | path | `integer (int64)` | yes |  |

Response `200`: The payment.

```json
{
  "payment_id": 5077125051,
  "invoice_id": 4522625843,
  "payment_status": "finished",
  "pay_address": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
  "payin_extra_id": null,
  "price_amount": 100,
  "price_currency": "usd",
  "pay_amount": 0.00123,
  "actually_paid": 0.00123,
  "pay_currency": "btc",
  "order_id": "1042",
  "order_description": "Order #1042",
  "purchase_id": 6084815412,
  "outcome_amount": 0.00123,
  "outcome_currency": "btc",
  "network": "btc",
  "network_precision": 8,
  "smart_contract": null,
  "payin_hash": "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b",
  "payout_hash": null,
  "payment_url": "https://pay.example.com/pay/Q7mX2pK9vT4nB8cR1sW6yZ3h",
  "created_at": "2026-10-23T12:00:00.000Z",
  "updated_at": "2026-10-23T12:14:03.512Z",
  "expiration_estimate_date": "2026-10-23T12:15:00.000Z",
  "parent_payment_id": null,
  "type": "crypto2crypto",
  "time_limit": null,
  "burning_percent": null,
  "payment_extra_ids": null,
  "amount_received": 0.00123,
  "actually_paid_at_fiat": 100,
  "fee": {
    "currency": "btc",
    "depositFee": 0,
    "withdrawalFee": 0,
    "serviceFee": 0
  }
}
```

Response `401`: The API key or the bearer token is missing or not valid.

```json
{
  "status": false,
  "statusCode": 401,
  "code": "AUTH_REQUIRED",
  "message": "Invalid API key."
}
```

Response `404`: Not found, or it belongs to another project.

```json
{
  "status": false,
  "statusCode": 404,
  "code": "NOT_FOUND",
  "message": "not found"
}
```

Response `429`: Too many requests.

```json
{
  "status": false,
  "statusCode": 429,
  "code": "TOO_MANY_REQUESTS",
  "message": "too many requests, try again later"
}
```

Response `500`: The server failed. The message has no details.

```json
{
  "status": false,
  "statusCode": 500,
  "code": "INTERNAL_ERROR",
  "message": "Internal error."
}
```


## Invoices

Hosted invoices. The customer selects the coin on the invoice page.

### POST /v1/invoice

Create an invoice

Returns `invoice_url`, a hosted checkout page. Send the customer there. The customer selects a coin, and
Turmite creates a payment for the invoice. The payment copies `order_id`, `order_description`,
`ipn_callback_url`, `success_url` and `cancel_url` from the invoice. Webhooks for that payment go to the
invoice's `ipn_callback_url`.

Auth: `apiKey`

| Field | Type | Required | Description |
|---|---|---|---|
| `price_amount` | `number \| string` | yes | A positive amount, as a JSON number or a decimal string. |
| `price_currency` | `string` | yes |  |
| `pay_currency` | `string` | no | Set it to skip the coin selection on the invoice page. |
| `order_id` | `string` | no |  |
| `order_description` | `string` | no |  |
| `ipn_callback_url` | `string (uri)` | no | Turmite sends the webhooks of the invoice's payments to this URL. |
| `success_url` | `string (uri)` | no |  |
| `cancel_url` | `string (uri)` | no |  |

```json
{
  "price_amount": 49.9,
  "price_currency": "eur",
  "order_id": "1042",
  "order_description": "Order #1042",
  "ipn_callback_url": "https://shop.example.com/turmite/ipn",
  "success_url": "https://shop.example.com/checkout/order-received/1042",
  "cancel_url": "https://shop.example.com/checkout"
}
```

Response `201`: The new invoice.

```json
{
  "id": "4522625843",
  "token_id": "k3J9xQ2vT8mZ5wR1nB7cY4pL",
  "order_id": "1042",
  "order_description": "Order #1042",
  "price_amount": "49.9",
  "price_currency": "eur",
  "pay_currency": null,
  "ipn_callback_url": "https://shop.example.com/turmite/ipn",
  "invoice_url": "https://pay.example.com/i/k3J9xQ2vT8mZ5wR1nB7cY4pL",
  "success_url": "https://shop.example.com/checkout/order-received/1042",
  "cancel_url": "https://shop.example.com/checkout",
  "created_at": "2026-10-23T12:00:00.000Z",
  "updated_at": "2026-10-23T12:00:00.000Z",
  "expiration_estimate_date": "2026-10-23T13:00:00.000Z"
}
```

Response `400`: The request is not valid.

```json
{
  "status": false,
  "statusCode": 400,
  "code": "INVALID_REQUEST_PARAMS",
  "message": "`price_amount` must be greater than 0."
}
```

Response `401`: The API key or the bearer token is missing or not valid.

```json
{
  "status": false,
  "statusCode": 401,
  "code": "AUTH_REQUIRED",
  "message": "Invalid API key."
}
```

Response `429`: Too many requests.

```json
{
  "status": false,
  "statusCode": 429,
  "code": "TOO_MANY_REQUESTS",
  "message": "too many requests, try again later"
}
```

Response `500`: The server failed. The message has no details.

```json
{
  "status": false,
  "statusCode": 500,
  "code": "INTERNAL_ERROR",
  "message": "Internal error."
}
```

### POST /v1/invoice-payment

Create a payment for an invoice

Use this to build your own coin selection for an invoice. The payment gets the invoice's price, order and
URLs. If the invoice has an open payment in `pay_currency`, Turmite returns that payment. In this response
`payment_id` and `purchase_id` are strings.

Auth: `apiKey`

| Field | Type | Required | Description |
|---|---|---|---|
| `iid` | `integer (int64) \| string` | yes | The invoice `id`. |
| `pay_currency` | `string` | yes |  |
| `purchase_id` | `string` | no |  |

```json
{
  "iid": "4522625843",
  "pay_currency": "usdttrc20"
}
```

Response `201`: The new payment.

```json
{
  "payment_id": "5077125051",
  "invoice_id": null,
  "payment_status": "waiting",
  "pay_address": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
  "payin_extra_id": null,
  "price_amount": 100,
  "price_currency": "usd",
  "pay_amount": 0.00123,
  "actually_paid": 0,
  "pay_currency": "btc",
  "order_id": "1042",
  "order_description": "Order #1042",
  "purchase_id": "6084815412",
  "outcome_amount": null,
  "outcome_currency": "btc",
  "network": "btc",
  "network_precision": 8,
  "smart_contract": null,
  "payin_hash": null,
  "payout_hash": null,
  "payment_url": "https://pay.example.com/pay/Q7mX2pK9vT4nB8cR1sW6yZ3h",
  "created_at": "2026-10-23T12:00:00.000Z",
  "updated_at": "2026-10-23T12:00:00.000Z",
  "expiration_estimate_date": "2026-10-23T12:15:00.000Z",
  "parent_payment_id": null,
  "type": "crypto2crypto",
  "time_limit": null,
  "burning_percent": null,
  "payment_extra_ids": null,
  "amount_received": 0.00123,
  "actually_paid_at_fiat": 0,
  "fee": {
    "currency": "btc",
    "depositFee": 0,
    "withdrawalFee": 0,
    "serviceFee": 0
  }
}
```

Response `400`: The request is not valid.

```json
{
  "status": false,
  "statusCode": 400,
  "code": "INVALID_REQUEST_PARAMS",
  "message": "`price_amount` must be greater than 0."
}
```

Response `401`: The API key or the bearer token is missing or not valid.

```json
{
  "status": false,
  "statusCode": 401,
  "code": "AUTH_REQUIRED",
  "message": "Invalid API key."
}
```

Response `404`: Not found, or it belongs to another project.

```json
{
  "status": false,
  "statusCode": 404,
  "code": "NOT_FOUND",
  "message": "not found"
}
```

Response `429`: Too many requests.

```json
{
  "status": false,
  "statusCode": 429,
  "code": "TOO_MANY_REQUESTS",
  "message": "too many requests, try again later"
}
```

Response `500`: The server failed. The message has no details.

```json
{
  "status": false,
  "statusCode": 500,
  "code": "INTERNAL_ERROR",
  "message": "Internal error."
}
```

Response `503`: A service that the request needs is not available, for example the exchange rates. Try again later.

```json
{
  "status": false,
  "statusCode": 503,
  "code": "UNAVAILABLE",
  "message": "Exchange rates are not available. Try again in a minute."
}
```


## Payouts

Send funds from the project's hot wallets.

### GET /v1/payout

List payouts

Auth: `apiKey`, `bearerAuth`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `batch_id` | query | `string` | no | Only the payouts of this batch. |
| `status` | query | `string` | no | Only the payouts with this status. An unknown status returns `400`. One of: `pending-approval`, `pending-otp-verification`, `pending`, `signing`, `initiated`, `sent`, `failed`, `rejected`, `cancelled`, `needs-review`. |
| `date_from` | query | `string` | no | Earliest `created_at`. A date or a date-time. |
| `date_to` | query | `string` | no | Latest `created_at`. A date or a date-time. |
| `order` | query | `string` | no |  One of: `asc`, `desc`. |
| `limit` | query | `integer` | no | Payouts per page. Turmite changes a value below 1 to 1 and a value above 500 to 500. |
| `page` | query | `integer` | no | Page number. The first page is 0. |

Response `200`: One page of payouts.

```json
{
  "payouts": [
    {
      "id": "string",
      "batch_withdrawal_id": "string",
      "address": "string",
      "extra_id": "string",
      "currency": "string",
      "network": "string",
      "amount": "250",
      "fee": "string",
      "fee_currency": "string",
      "status": "pending-approval",
      "hash": "string",
      "error": "string",
      "ipn_callback_url": "string",
      "payout_description": "string",
      "unique_external_id": "string",
      "created_at": "2026-10-23T12:00:00Z",
      "requested_at": "2026-10-23T12:00:00Z",
      "updated_at": "2026-10-23T12:00:00Z"
    }
  ],
  "limit": 0,
  "page": 0,
  "pagesCount": 0,
  "total": 0
}
```

Response `400`: The request is not valid.

```json
{
  "status": false,
  "statusCode": 400,
  "code": "INVALID_REQUEST_PARAMS",
  "message": "`price_amount` must be greater than 0."
}
```

Response `401`: The API key or the bearer token is missing or not valid.

```json
{
  "status": false,
  "statusCode": 401,
  "code": "AUTH_REQUIRED",
  "message": "Invalid API key."
}
```

Response `403`: The key or the user cannot do this action.

```json
{
  "status": false,
  "statusCode": 403,
  "code": "FORBIDDEN",
  "message": "Only a project admin can create or verify payouts."
}
```

Response `429`: Too many requests.

```json
{
  "status": false,
  "statusCode": 429,
  "code": "TOO_MANY_REQUESTS",
  "message": "too many requests, try again later"
}
```

Response `500`: The server failed. The message has no details.

```json
{
  "status": false,
  "statusCode": 500,
  "code": "INTERNAL_ERROR",
  "message": "Internal error."
}
```

### POST /v1/payout

Create a payout batch

Creates one payout per withdrawal. The bearer token must belong to a project admin of the API key's
project. Each address must be in the project's address book. New payouts are `pending-otp-verification`
until you verify the batch with `POST /v1/payout/{batch_id}/verify`. A withdrawal with a
`unique_external_id` that already exists returns the existing payout. The same id with other fields
returns `409`. At most 100 withdrawals per batch. Bitcoin and Monero have no payouts.

Auth: `apiKey`, `bearerAuth`

| Field | Type | Required | Description |
|---|---|---|---|
| `withdrawals` | `object[]` | yes |  |
| `withdrawals[].address` | `string` | yes | The destination. It must be in the project's address book. |
| `withdrawals[].currency` | `string` | yes |  |
| `withdrawals[].amount` | `number \| string` | yes | A positive amount, as a JSON number or a decimal string. |
| `withdrawals[].extra_id` | `string` | no | The memo or tag of the destination. |
| `withdrawals[].ipn_callback_url` | `string (uri)` | no | Turmite sends payout webhooks to this URL. Without it, the batch's `ipn_callback_url` is used. |
| `withdrawals[].payout_description` | `string` | no |  |
| `withdrawals[].unique_external_id` | `string` | no | Your id for this payout. A second request with the same id and fields returns the existing payout. The same id with other fields returns `409`. |
| `ipn_callback_url` | `string (uri)` | no | Payout webhooks for each withdrawal without its own `ipn_callback_url`. |

```json
{
  "withdrawals": [
    {
      "address": "TQrZ9wBzhXnHdAdKwRBfdMH2RiTjH6yGtW",
      "currency": "usdttrc20",
      "amount": "250.00",
      "ipn_callback_url": "https://shop.example.com/turmite/payout-ipn",
      "unique_external_id": "refund-1042"
    }
  ]
}
```

Response `201`: The new batch.

```json
{
  "id": "5000000713",
  "withdrawals": [
    {
      "id": "5000248325",
      "batch_withdrawal_id": "5000000713",
      "address": "TQrZ9wBzhXnHdAdKwRBfdMH2RiTjH6yGtW",
      "extra_id": null,
      "currency": "usdttrc20",
      "network": "tron",
      "amount": "250",
      "fee": null,
      "fee_currency": "trx",
      "status": "pending-otp-verification",
      "hash": null,
      "error": null,
      "ipn_callback_url": "https://shop.example.com/turmite/payout-ipn",
      "payout_description": null,
      "unique_external_id": "refund-1042",
      "created_at": "2026-10-23T13:00:00.000Z",
      "requested_at": null,
      "updated_at": "2026-10-23T13:00:00.000Z"
    }
  ]
}
```

Response `400`: The request is not valid.

```json
{
  "status": false,
  "statusCode": 400,
  "code": "INVALID_REQUEST_PARAMS",
  "message": "`price_amount` must be greater than 0."
}
```

Response `401`: The API key or the bearer token is missing or not valid.

```json
{
  "status": false,
  "statusCode": 401,
  "code": "AUTH_REQUIRED",
  "message": "Invalid API key."
}
```

Response `403`: The key or the user cannot do this action.

```json
{
  "status": false,
  "statusCode": 403,
  "code": "FORBIDDEN",
  "message": "Only a project admin can create or verify payouts."
}
```

Response `409`: The request conflicts with an existing object.

```json
{
  "status": false,
  "statusCode": 409,
  "code": "CONFLICT",
  "message": "`unique_external_id` is used with other fields."
}
```

Response `429`: Too many requests.

```json
{
  "status": false,
  "statusCode": 429,
  "code": "TOO_MANY_REQUESTS",
  "message": "too many requests, try again later"
}
```

Response `500`: The server failed. The message has no details.

```json
{
  "status": false,
  "statusCode": 500,
  "code": "INTERNAL_ERROR",
  "message": "Internal error."
}
```

### POST /v1/payout/{batch_id}/verify

Verify a payout batch

Send the current TOTP code of the user who got the bearer token. That user must be a project admin. Each
payout of the batch that waits for verification then goes through the project limits: within them it is
`pending`; above the auto-approve amount or the hourly or daily limit it is `pending-approval` until an
admin approves it in the dashboard. A wrong code returns `400`. After 5 wrong codes the user is locked for
15 minutes and gets `429`. A user without two-factor authentication gets `403`.

Auth: `apiKey`, `bearerAuth`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `batch_id` | path | `string` | yes | The `id` of the payout batch. |

| Field | Type | Required | Description |
|---|---|---|---|
| `verification_code` | `string` | yes | The current TOTP code of the dashboard user. |

```json
{
  "verification_code": "123456"
}
```

Response `200`: The batch after verification.

```json
{
  "id": "string",
  "withdrawals": [
    {
      "id": "string",
      "batch_withdrawal_id": "string",
      "address": "string",
      "extra_id": "string",
      "currency": "string",
      "network": "string",
      "amount": "250",
      "fee": "string",
      "fee_currency": "string",
      "status": "pending-approval",
      "hash": "string",
      "error": "string",
      "ipn_callback_url": "string",
      "payout_description": "string",
      "unique_external_id": "string",
      "created_at": "2026-10-23T12:00:00Z",
      "requested_at": "2026-10-23T12:00:00Z",
      "updated_at": "2026-10-23T12:00:00Z"
    }
  ]
}
```

Response `400`: The request is not valid.

```json
{
  "status": false,
  "statusCode": 400,
  "code": "INVALID_REQUEST_PARAMS",
  "message": "`price_amount` must be greater than 0."
}
```

Response `401`: The API key or the bearer token is missing or not valid.

```json
{
  "status": false,
  "statusCode": 401,
  "code": "AUTH_REQUIRED",
  "message": "Invalid API key."
}
```

Response `403`: The key or the user cannot do this action.

```json
{
  "status": false,
  "statusCode": 403,
  "code": "FORBIDDEN",
  "message": "Only a project admin can create or verify payouts."
}
```

Response `404`: Not found, or it belongs to another project.

```json
{
  "status": false,
  "statusCode": 404,
  "code": "NOT_FOUND",
  "message": "not found"
}
```

Response `429`: Too many requests.

```json
{
  "status": false,
  "statusCode": 429,
  "code": "TOO_MANY_REQUESTS",
  "message": "too many requests, try again later"
}
```

Response `500`: The server failed. The message has no details.

```json
{
  "status": false,
  "statusCode": 500,
  "code": "INTERNAL_ERROR",
  "message": "Internal error."
}
```

### GET /v1/payout/{batch_id}

Get a payout batch

Auth: `apiKey`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `batch_id` | path | `string` | yes | The `id` of the payout batch. |

Response `200`: The batch.

```json
{
  "id": "5000000713",
  "withdrawals": [
    {
      "id": "5000248325",
      "batch_withdrawal_id": "5000000713",
      "address": "TQrZ9wBzhXnHdAdKwRBfdMH2RiTjH6yGtW",
      "extra_id": null,
      "currency": "usdttrc20",
      "network": "tron",
      "amount": "250",
      "fee": null,
      "fee_currency": "trx",
      "status": "pending-otp-verification",
      "hash": null,
      "error": null,
      "ipn_callback_url": "https://shop.example.com/turmite/payout-ipn",
      "payout_description": null,
      "unique_external_id": "refund-1042",
      "created_at": "2026-10-23T13:00:00.000Z",
      "requested_at": null,
      "updated_at": "2026-10-23T13:00:00.000Z"
    }
  ]
}
```

Response `401`: The API key or the bearer token is missing or not valid.

```json
{
  "status": false,
  "statusCode": 401,
  "code": "AUTH_REQUIRED",
  "message": "Invalid API key."
}
```

Response `404`: Not found, or it belongs to another project.

```json
{
  "status": false,
  "statusCode": 404,
  "code": "NOT_FOUND",
  "message": "not found"
}
```

Response `429`: Too many requests.

```json
{
  "status": false,
  "statusCode": 429,
  "code": "TOO_MANY_REQUESTS",
  "message": "too many requests, try again later"
}
```

Response `500`: The server failed. The message has no details.

```json
{
  "status": false,
  "statusCode": 500,
  "code": "INTERNAL_ERROR",
  "message": "Internal error."
}
```

### POST /v1/payout/validate-address

Validate a payout address

Auth: `apiKey`

| Field | Type | Required | Description |
|---|---|---|---|
| `address` | `string` | yes |  |
| `currency` | `string` | yes |  |

```json
{
  "address": "TQrZ9wBzhXnHdAdKwRBfdMH2RiTjH6yGtW",
  "currency": "usdttrc20"
}
```

Response `200`: The address is valid. The body is the JSON string `"OK"`. An invalid address or an unknown currency returns `400`.

```json
"OK"
```

Response `400`: The request is not valid.

```json
{
  "status": false,
  "statusCode": 400,
  "code": "INVALID_REQUEST_PARAMS",
  "message": "`price_amount` must be greater than 0."
}
```

Response `401`: The API key or the bearer token is missing or not valid.

```json
{
  "status": false,
  "statusCode": 401,
  "code": "AUTH_REQUIRED",
  "message": "Invalid API key."
}
```

Response `429`: Too many requests.

```json
{
  "status": false,
  "statusCode": 429,
  "code": "TOO_MANY_REQUESTS",
  "message": "too many requests, try again later"
}
```

Response `500`: The server failed. The message has no details.

```json
{
  "status": false,
  "statusCode": 500,
  "code": "INTERNAL_ERROR",
  "message": "Internal error."
}
```

### GET /v1/payout/fee

Estimate the network fee of a payout

The fee is an estimate from the chain. Turmite measures the real fee when it sends the payout. The hot
wallet pays the fee in the native coin of the network (`network_fee` in `fee_currency`); the recipient gets
the full amount. `fee` is that cost in `currency`. A Solana token payout to a wallet without a token
account also pays the rent of that account.

Auth: `apiKey`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `currency` | query | `string` | yes |  |
| `amount` | query | `string` | no |  |

Response `200`: The estimated fee in `currency`.

```json
{
  "currency": "usdttrc20",
  "amount": "250",
  "fee": 1.1,
  "network_fee": 4.4,
  "fee_currency": "trx"
}
```

Response `400`: The request is not valid.

```json
{
  "status": false,
  "statusCode": 400,
  "code": "INVALID_REQUEST_PARAMS",
  "message": "`price_amount` must be greater than 0."
}
```

Response `401`: The API key or the bearer token is missing or not valid.

```json
{
  "status": false,
  "statusCode": 401,
  "code": "AUTH_REQUIRED",
  "message": "Invalid API key."
}
```

Response `429`: Too many requests.

```json
{
  "status": false,
  "statusCode": 429,
  "code": "TOO_MANY_REQUESTS",
  "message": "too many requests, try again later"
}
```

Response `500`: The server failed. The message has no details.

```json
{
  "status": false,
  "statusCode": 500,
  "code": "INTERNAL_ERROR",
  "message": "Internal error."
}
```

Response `503`: A service that the request needs is not available, for example the exchange rates. Try again later.

```json
{
  "status": false,
  "statusCode": 503,
  "code": "UNAVAILABLE",
  "message": "Exchange rates are not available. Try again in a minute."
}
```


## Reference

Currencies, minimum amounts, estimates and balances.

### GET /v1/status

Check that the API is up

Response `200`: The API is up.

```json
{
  "message": "OK"
}
```

Response `429`: Too many requests.

```json
{
  "status": false,
  "statusCode": 429,
  "code": "TOO_MANY_REQUESTS",
  "message": "too many requests, try again later"
}
```

Response `500`: The server failed. The message has no details.

```json
{
  "status": false,
  "statusCode": 500,
  "code": "INTERNAL_ERROR",
  "message": "Internal error."
}
```

### GET /v1/currencies

List enabled tickers

With a key, the tickers enabled for the key's project. Without a key, the tickers that at least one project takes.

Auth: `apiKey`

Response `200`: Enabled tickers.

```json
{
  "currencies": [
    "btc",
    "eth",
    "usdttrc20",
    "sol"
  ]
}
```

Response `401`: The API key or the bearer token is missing or not valid.

```json
{
  "status": false,
  "statusCode": 401,
  "code": "AUTH_REQUIRED",
  "message": "Invalid API key."
}
```

Response `429`: Too many requests.

```json
{
  "status": false,
  "statusCode": 429,
  "code": "TOO_MANY_REQUESTS",
  "message": "too many requests, try again later"
}
```

Response `500`: The server failed. The message has no details.

```json
{
  "status": false,
  "statusCode": 500,
  "code": "INTERNAL_ERROR",
  "message": "Internal error."
}
```

### GET /v1/full-currencies

List currencies with details

One entry for each ticker that the server has turned on. `enable` tells if the project takes it.

Auth: `apiKey`

Response `200`: One entry per ticker.

```json
{
  "currencies": [
    {
      "id": 7,
      "code": "USDTTRC20",
      "name": "Tether USD",
      "enable": true,
      "wallet_regex": null,
      "priority": 7,
      "extra_id_exists": false,
      "extra_id_regex": null,
      "logo_url": null,
      "track": false,
      "cg_id": "tether",
      "is_maxlimit": false,
      "network": "tron",
      "smart_contract": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
      "network_precision": "6",
      "ticker": "usdttrc20"
    }
  ]
}
```

Response `401`: The API key or the bearer token is missing or not valid.

```json
{
  "status": false,
  "statusCode": 401,
  "code": "AUTH_REQUIRED",
  "message": "Invalid API key."
}
```

Response `429`: Too many requests.

```json
{
  "status": false,
  "statusCode": 429,
  "code": "TOO_MANY_REQUESTS",
  "message": "too many requests, try again later"
}
```

Response `500`: The server failed. The message has no details.

```json
{
  "status": false,
  "statusCode": 500,
  "code": "INTERNAL_ERROR",
  "message": "Internal error."
}
```

### GET /v1/merchant/coins

List the tickers enabled for the project

Auth: `apiKey`

Response `200`: Tickers enabled for the key's project.

```json
{
  "selectedCurrencies": [
    "btc",
    "usdttrc20"
  ]
}
```

Response `401`: The API key or the bearer token is missing or not valid.

```json
{
  "status": false,
  "statusCode": 401,
  "code": "AUTH_REQUIRED",
  "message": "Invalid API key."
}
```

Response `429`: Too many requests.

```json
{
  "status": false,
  "statusCode": 429,
  "code": "TOO_MANY_REQUESTS",
  "message": "too many requests, try again later"
}
```

Response `500`: The server failed. The message has no details.

```json
{
  "status": false,
  "statusCode": 500,
  "code": "INTERNAL_ERROR",
  "message": "Internal error."
}
```

### GET /v1/min-amount

Get the minimum payment amount

No API key is necessary. Turmite ignores a key that you send.

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `currency_from` | query | `string` | yes | The ticker the customer pays with. |
| `currency_to` | query | `string` | no | Turmite does not convert. It copies this value to the response. The default is `currency_from`. |
| `fiat_equivalent` | query | `string` | no | A fiat code. When set and a rate is available, the response has `fiat_equivalent`. |

Response `200`: The minimum payment amount in `currency_from`.

```json
{
  "currency_from": "btc",
  "currency_to": "btc",
  "min_amount": 0.00001,
  "fiat_equivalent": 1.07
}
```

Response `400`: The request is not valid.

```json
{
  "status": false,
  "statusCode": 400,
  "code": "INVALID_REQUEST_PARAMS",
  "message": "`price_amount` must be greater than 0."
}
```

Response `429`: Too many requests.

```json
{
  "status": false,
  "statusCode": 429,
  "code": "TOO_MANY_REQUESTS",
  "message": "too many requests, try again later"
}
```

Response `500`: The server failed. The message has no details.

```json
{
  "status": false,
  "statusCode": 500,
  "code": "INTERNAL_ERROR",
  "message": "Internal error."
}
```

### GET /v1/estimate

Estimate a price in a coin

No API key is necessary. Turmite ignores a key that you send.

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `amount` | query | `string` | yes | The amount in `currency_from`. |
| `currency_from` | query | `string` | yes | The price currency, such as `usd`. |
| `currency_to` | query | `string` | yes | The ticker the customer pays with. |

Response `200`: The estimate at the current rate.

```json
{
  "currency_from": "usd",
  "amount_from": 100,
  "currency_to": "btc",
  "estimated_amount": "0.00107"
}
```

Response `400`: The request is not valid.

```json
{
  "status": false,
  "statusCode": 400,
  "code": "INVALID_REQUEST_PARAMS",
  "message": "`price_amount` must be greater than 0."
}
```

Response `429`: Too many requests.

```json
{
  "status": false,
  "statusCode": 429,
  "code": "TOO_MANY_REQUESTS",
  "message": "too many requests, try again later"
}
```

Response `500`: The server failed. The message has no details.

```json
{
  "status": false,
  "statusCode": 500,
  "code": "INTERNAL_ERROR",
  "message": "Internal error."
}
```

Response `503`: A service that the request needs is not available, for example the exchange rates. Try again later.

```json
{
  "status": false,
  "statusCode": 503,
  "code": "UNAVAILABLE",
  "message": "Exchange rates are not available. Try again in a minute."
}
```

### GET /v1/balance

Get the project balance per ticker

Auth: `apiKey`

Response `200`: One entry per ticker.

```json
{
  "btc": {
    "amount": 0.0153,
    "pendingAmount": 0.0012
  },
  "usdttrc20": {
    "amount": 1250.5,
    "pendingAmount": 0
  }
}
```

Response `401`: The API key or the bearer token is missing or not valid.

```json
{
  "status": false,
  "statusCode": 401,
  "code": "AUTH_REQUIRED",
  "message": "Invalid API key."
}
```

Response `429`: Too many requests.

```json
{
  "status": false,
  "statusCode": 429,
  "code": "TOO_MANY_REQUESTS",
  "message": "too many requests, try again later"
}
```

Response `500`: The server failed. The message has no details.

```json
{
  "status": false,
  "statusCode": 500,
  "code": "INTERNAL_ERROR",
  "message": "Internal error."
}
```


## Auth

Bearer tokens for the payout endpoints.

### POST /v1/auth

Get a bearer token for the payout endpoints

Use the email and password of a dashboard user. The token is valid for 5 minutes.

| Field | Type | Required | Description |
|---|---|---|---|
| `email` | `string (email)` | yes |  |
| `password` | `string (password)` | yes |  |

```json
{
  "email": "ops@shop.example.com",
  "password": "correct-horse-battery-staple"
}
```

Response `200`: The token.

```json
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIn0.c2lnbmF0dXJl"
}
```

Response `400`: The request is not valid.

```json
{
  "status": false,
  "statusCode": 400,
  "code": "INVALID_REQUEST_PARAMS",
  "message": "`price_amount` must be greater than 0."
}
```

Response `401`: The API key or the bearer token is missing or not valid.

```json
{
  "status": false,
  "statusCode": 401,
  "code": "AUTH_REQUIRED",
  "message": "Invalid API key."
}
```

Response `429`: Too many requests.

```json
{
  "status": false,
  "statusCode": 429,
  "code": "TOO_MANY_REQUESTS",
  "message": "too many requests, try again later"
}
```

Response `500`: The server failed. The message has no details.

```json
{
  "status": false,
  "statusCode": 500,
  "code": "INTERNAL_ERROR",
  "message": "Internal error."
}
```


## Webhooks

### POST payment

A payment changed

Turmite sends the payment object to the `ipn_callback_url` of the payment, or of its invoice. It sends
one on each status change, one for each new confirmation count while the payment is `confirming`, and one
on each extra credit after `finished`. Ship the order only on `finished`. In this body `payment_id` is a
number and `purchase_id` is a string.

Check the signature before you use the body. The signature header (default `x-turmite-sig`) holds the hex
HMAC-SHA512 of the key-sorted JSON body. The key is the project IPN secret. To get the key-sorted JSON from
the raw body:

1. Sort the members of each object by key. Compare keys as UTF-8 bytes. Keep the order of arrays.
2. Write compact JSON with no whitespace.
3. Copy each number exactly as it is in the raw body, for example `0.5` or `1e-6`.
4. In strings, escape only `"`, `\` and control characters. Do not escape `/` or non-ASCII characters.

`x-turmite-signature` holds `v1=` and the hex HMAC-SHA256 of `<x-turmite-timestamp>.<raw body>`, with the
same secret. Refuse a timestamp more than 5 minutes from your clock. Use `x-turmite-delivery` to ignore a
delivery that you already processed.

A 2xx response acknowledges the delivery. Progress events get 3 attempts: now, after 10 s and after 60 s.
Final events get attempts now and after 10 s, 1 m, 5 m, 30 m, 1 h, 2 h, 4 h, 8 h, 24 h and 48 h. Then the
delivery is dead. You can retry it from the dashboard.

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `x-turmite-sig` | header | `string` | yes | Hex HMAC-SHA512 of the key-sorted JSON body. The key is the project IPN secret for an `ipn_callback_url`, or the endpoint secret for a project endpoint. The header name is a project setting. The default is `x-turmite-sig`. |
| `x-turmite-timestamp` | header | `string` | yes | Unix time in seconds when Turmite signed this attempt. |
| `x-turmite-signature` | header | `string` | yes | `v1=` and the hex HMAC-SHA256 of `<x-turmite-timestamp>.<raw body>`. The key is the same as for the signature header. |
| `x-turmite-event` | header | `string` | yes | The event name, such as `payment.finished`, `payout.sent` or `test`. |
| `x-turmite-delivery` | header | `string` | yes | The delivery id. Retries of a delivery use the same id. Use it to ignore duplicates. |
| `user-agent` | header | `string` | yes |  |

| Field | Type | Required | Description |
|---|---|---|---|
| `invoice_id` | `integer (int64) \| null` | yes |  |
| `payment_status` | `string` | yes | `waiting`: no deposit yet. `confirming`: a deposit is on the chain with too few confirmations. `confirmed`: the deposit has enough confirmations. `sending`: Turmite moves the funds to the wallet of the project. `partially_paid`: the customer sent less than `pay_amount`. `finished`: paid in full. Ship the order now. `failed`, `refunded` and `expired` are final. One of: `waiting`, `confirming`, `confirmed`, `sending`, `partially_paid`, `finished`, `failed`, `refunded`, `expired`. |
| `pay_address` | `string` | yes | The deposit address. |
| `payin_extra_id` | `string \| null` | yes | The memo or tag the customer must add, on networks that use one. |
| `price_amount` | `number` | yes | The price in `price_currency`. |
| `price_currency` | `string` | yes | Lowercase. |
| `pay_amount` | `number` | yes | The amount to pay in `pay_currency`. |
| `actually_paid` | `number` | yes | The amount received so far. |
| `pay_currency` | `string` | yes |  |
| `order_id` | `string \| null` | yes |  |
| `order_description` | `string \| null` | yes |  |
| `outcome_amount` | `number \| null` | yes | The amount credited to the project after network fees. |
| `outcome_currency` | `string` | yes |  |
| `network` | `string` | yes | The network code. One of `btc`, `xmr`, `eth`, `base`, `polygon`, `bsc`, `tron`, `sol`, `ton`. |
| `network_precision` | `integer` | yes | The decimals of `pay_currency`. |
| `smart_contract` | `string \| null` | yes | The token contract. Null for a native coin. |
| `payin_hash` | `string \| null` | yes | The transaction hash of the latest deposit. |
| `payout_hash` | `string \| null` | yes | The transaction hash of the latest transfer from the deposit address to the project wallet. |
| `payment_url` | `string (uri)` | yes | The hosted payment page, `<site URL>/pay/<token>`. The token is random. |
| `created_at` | `string (date-time)` | yes |  |
| `updated_at` | `string (date-time)` | yes |  |
| `expiration_estimate_date` | `string (date-time)` | yes | When the payment expires if no deposit comes. |
| `parent_payment_id` | `null` | yes | Always null. |
| `type` | `string` | yes | Always `crypto2crypto`. One of: `crypto2crypto`. |
| `time_limit` | `null` | yes | Always null. |
| `burning_percent` | `null` | yes | Always null. |
| `payment_extra_ids` | `null` | yes | Always null. |
| `amount_received` | `number` | yes | The amount the project gets in `outcome_currency`. `outcome_amount` once funds are credited, else `pay_amount`. |
| `actually_paid_at_fiat` | `number` | yes | `actually_paid` in `price_currency`, at the rate of the payment. A payment with a fixed `pay_amount` uses `price_amount / pay_amount` as the rate. |
| `fee` | `object` | yes | Turmite takes no fee. `withdrawalFee` is the network fee of the sweeps to the project wallet. |
| `fee.currency` | `string` | yes | The same as `pay_currency`. |
| `fee.depositFee` | `number` | yes | Always 0. |
| `fee.withdrawalFee` | `number` | yes |  |
| `fee.serviceFee` | `number` | yes | Always 0. |
| `payment_id` | `integer (int64)` | yes |  |
| `purchase_id` | `string` | yes | The `purchase_id` of the request, or numeric text that Turmite makes. |

```json
{
  "payment_id": 5077125051,
  "invoice_id": 4522625843,
  "payment_status": "finished",
  "pay_address": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
  "payin_extra_id": null,
  "price_amount": 100,
  "price_currency": "usd",
  "pay_amount": 0.00123,
  "actually_paid": 0.00123,
  "pay_currency": "btc",
  "order_id": "1042",
  "order_description": "Order #1042",
  "purchase_id": "6084815412",
  "outcome_amount": 0.00123,
  "outcome_currency": "btc",
  "network": "btc",
  "network_precision": 8,
  "smart_contract": null,
  "payin_hash": "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b",
  "payout_hash": null,
  "payment_url": "https://pay.example.com/pay/Q7mX2pK9vT4nB8cR1sW6yZ3h",
  "created_at": "2026-10-23T12:00:00.000Z",
  "updated_at": "2026-10-23T12:14:03.512Z",
  "expiration_estimate_date": "2026-10-23T12:15:00.000Z",
  "parent_payment_id": null,
  "type": "crypto2crypto",
  "time_limit": null,
  "burning_percent": null,
  "payment_extra_ids": null,
  "amount_received": 0.00123,
  "actually_paid_at_fiat": 100,
  "fee": {
    "currency": "btc",
    "depositFee": 0,
    "withdrawalFee": 0,
    "serviceFee": 0
  }
}
```

Response `200`: Any 2xx status acknowledges the delivery.

Response `400`: Any other status makes Turmite retry on the schedule.

### POST payout

A payout changed

Turmite sends the payout object to the `ipn_callback_url` of the withdrawal (or of its batch) on each status
change, with the event `payout.<status>` in `x-turmite-event`. The key is the project IPN secret. The headers,
signatures and retries are the same as for the payment webhook. `sent`, `failed`, `rejected` and `cancelled`
are final events.

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `x-turmite-sig` | header | `string` | yes | Hex HMAC-SHA512 of the key-sorted JSON body. The key is the project IPN secret for an `ipn_callback_url`, or the endpoint secret for a project endpoint. The header name is a project setting. The default is `x-turmite-sig`. |
| `x-turmite-timestamp` | header | `string` | yes | Unix time in seconds when Turmite signed this attempt. |
| `x-turmite-signature` | header | `string` | yes | `v1=` and the hex HMAC-SHA256 of `<x-turmite-timestamp>.<raw body>`. The key is the same as for the signature header. |
| `x-turmite-event` | header | `string` | yes | The event name, such as `payment.finished`, `payout.sent` or `test`. |
| `x-turmite-delivery` | header | `string` | yes | The delivery id. Retries of a delivery use the same id. Use it to ignore duplicates. |
| `user-agent` | header | `string` | yes |  |

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | `string` | yes |  |
| `batch_withdrawal_id` | `string` | yes |  |
| `address` | `string` | yes |  |
| `extra_id` | `string \| null` | yes |  |
| `currency` | `string` | yes |  |
| `network` | `string` | yes | The network code in lower case, such as `eth` or `tron`. |
| `amount` | `string` | yes | A decimal number as text. |
| `fee` | `string \| null` | yes | The network fee in `fee_currency`, when known. The hot wallet pays it. |
| `fee_currency` | `string` | yes | The native coin of the network, such as `eth` or `trx`. |
| `status` | `string` | yes | `pending-otp-verification`: waits for a one-time code. An API batch waits for `verify`, or the admin who clicked Approve in the dashboard must enter the code that Turmite emailed. `pending-approval`: above a project limit; waits for an admin in the dashboard. `pending`: cleared to send. `signing`: signed and stored, no node took it yet. `initiated`: a node took the transaction. `sent`: on the chain with the network's confirmations. `sent`, `failed`, `rejected` and `cancelled` are final. `needs-review`: Turmite cannot tell whether the payout went out (its nonce is used by another transaction). An admin checks the chain and records the result. One of: `pending-approval`, `pending-otp-verification`, `pending`, `signing`, `initiated`, `sent`, `failed`, `rejected`, `cancelled`, `needs-review`. |
| `hash` | `string \| null` | yes | The transaction hash. |
| `error` | `string \| null` | yes |  |
| `ipn_callback_url` | `string (uri) \| null` | yes |  |
| `payout_description` | `string \| null` | yes |  |
| `unique_external_id` | `string \| null` | yes |  |
| `created_at` | `string (date-time)` | yes |  |
| `requested_at` | `string (date-time) \| null` | yes |  |
| `updated_at` | `string (date-time)` | yes |  |

```json
{
  "id": "5000248325",
  "batch_withdrawal_id": "5000000713",
  "address": "TQrZ9wBzhXnHdAdKwRBfdMH2RiTjH6yGtW",
  "extra_id": null,
  "currency": "usdttrc20",
  "network": "tron",
  "amount": "250",
  "fee": "4.4",
  "fee_currency": "trx",
  "status": "sent",
  "hash": "9f4c0e5b2a7d1c3e8f6a0b4d2c9e7f1a3b5d8c0e2f4a6b8d0c2e4f6a8b0d2c4e",
  "error": null,
  "ipn_callback_url": "https://shop.example.com/turmite/payout-ipn",
  "payout_description": "Refund for order 1042",
  "unique_external_id": "refund-1042",
  "created_at": "2026-10-23T13:00:00.000Z",
  "requested_at": "2026-10-23T13:01:10.000Z",
  "updated_at": "2026-10-23T13:02:45.000Z"
}
```

Response `200`: Any 2xx status acknowledges the delivery.

Response `400`: Any other status makes Turmite retry on the schedule.

### POST event

An event for a project endpoint

Turmite sends this envelope to each endpoint that you add under Webhooks in the dashboard. `event` is
`payment.<status>` with a payment in `data`, `payout.<status>` with a payout in `data`, or `test` with a
message in `data`.

Each endpoint has its own secret. The dashboard shows it. Turmite signs both signature headers with it.
The headers, the signature rules and the retries are the same as for the payment webhook.

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `x-turmite-sig` | header | `string` | yes | Hex HMAC-SHA512 of the key-sorted JSON body. The key is the project IPN secret for an `ipn_callback_url`, or the endpoint secret for a project endpoint. The header name is a project setting. The default is `x-turmite-sig`. |
| `x-turmite-timestamp` | header | `string` | yes | Unix time in seconds when Turmite signed this attempt. |
| `x-turmite-signature` | header | `string` | yes | `v1=` and the hex HMAC-SHA256 of `<x-turmite-timestamp>.<raw body>`. The key is the same as for the signature header. |
| `x-turmite-event` | header | `string` | yes | The event name, such as `payment.finished`, `payout.sent` or `test`. |
| `x-turmite-delivery` | header | `string` | yes | The delivery id. Retries of a delivery use the same id. Use it to ignore duplicates. |
| `user-agent` | header | `string` | yes |  |

| Field | Type | Required | Description |
|---|---|---|---|
| `event` | `string` | yes | `payment.<status>`, `payout.<status>` or `test`. |
| `delivery_id` | `string` | yes | The same value as the `x-turmite-delivery` header. |
| `created_at` | `integer (int64)` | yes | Unix time in seconds when Turmite made the event. |
| `data` | `any \| object \| object` | yes |  |

```json
{
  "event": "payment.finished",
  "delivery_id": "dK4rT8wQ2mN6xP1vB9cL5zH7",
  "created_at": 1761221644,
  "data": {
    "payment_id": 5077125051,
    "invoice_id": 4522625843,
    "payment_status": "finished",
    "pay_address": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
    "payin_extra_id": null,
    "price_amount": 100,
    "price_currency": "usd",
    "pay_amount": 0.00123,
    "actually_paid": 0.00123,
    "pay_currency": "btc",
    "order_id": "1042",
    "order_description": "Order #1042",
    "purchase_id": "6084815412",
    "outcome_amount": 0.00123,
    "outcome_currency": "btc",
    "network": "btc",
    "network_precision": 8,
    "smart_contract": null,
    "payin_hash": "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b",
    "payout_hash": null,
    "payment_url": "https://pay.example.com/pay/Q7mX2pK9vT4nB8cR1sW6yZ3h",
    "created_at": "2026-10-23T12:00:00.000Z",
    "updated_at": "2026-10-23T12:14:03.512Z",
    "expiration_estimate_date": "2026-10-23T12:15:00.000Z",
    "parent_payment_id": null,
    "type": "crypto2crypto",
    "time_limit": null,
    "burning_percent": null,
    "payment_extra_ids": null,
    "amount_received": 0.00123,
    "actually_paid_at_fiat": 100,
    "fee": {
      "currency": "btc",
      "depositFee": 0,
      "withdrawalFee": 0,
      "serviceFee": 0
    }
  }
}
```

Response `200`: Any 2xx status acknowledges the delivery.

Response `400`: Any other status makes Turmite retry on the schedule.

