# Bifrost Crypto API v1

> Markdown version of the public documentation of the Bifrost Crypto API v1, for AI agents and code generators. Same content as the HTML pages, generated from the same source, without navigation or styling.
>
> HTML version: https://bifrostcrypto.com/en/docs
> Related files: [ai.md](https://bifrostcrypto.com/ai.md) · [Postman](https://bifrostcrypto.com/postman-collection.json) · [OpenAPI](https://bifrostcrypto.com/openapi.json) · [llms.txt](https://bifrostcrypto.com/llms.txt)
> Other languages: [Português](https://bifrostcrypto.com/ai/pt-BR.md) · [Español](https://bifrostcrypto.com/ai/es.md) · [Français](https://bifrostcrypto.com/ai/fr.md)
> API v1 · contract last changed on 2026-10-10

## Quick summary for agents

- Base URL `https://bridge.bifrostcrypto.com`. Every path starts with `/v1`. Request and response bodies are JSON (`Content-Type: application/json`).
- Send the API key in `X-Bifrost-Invoke` on every request. There is no login and no token to refresh. Missing key: 401 `api_key_missing`; unknown, revoked or expired key: 401 `api_key_invalid`.
- A key can be created with request signing. Its requests also carry `X-Bifrost-Timestamp` (Unix seconds), `X-Bifrost-Nonce` (16 to 80 characters, accepted once) and `X-Bifrost-Signature`: the HMAC-SHA256 in hex, keyed by the signing secret, of METHOD, PATH (with `/v1`), the raw QUERY, TIMESTAMP, NONCE and the SHA-256 hex of the exact body bytes, joined by `\n`. Sign the bytes you send and never serialize the body again after signing.
- Each key carries scopes. A call outside them returns 403 `scope_denied` with `details.required_scopes`. A key created without a scope list is read-only.
- Send `crypto_amount` and `fiat_amount` as plain decimal strings such as `"100.50"`: digits and an optional dot, without scientific notation, signs or thousands separators. Amounts in responses are strings whose number of decimals varies: parse them with a decimal library, not as floats.
- Exception: `price` in `GET /v1/get-price` uses a comma as decimal separator and a dot as thousands separator (`"65.432,10"`). Remove the dots, then replace the comma with a dot before parsing.
- Date-time fields are `YYYY-MM-DD HH:MM:SS` in UTC−03:00, without an offset in the value, and date filters are read in the same offset. Exceptions: the daily payout limit of a key resets at 00:00 UTC, and fixed-address webhooks send `timestamp` in ISO 8601 with the offset.
- `network` takes the canonical names listed under Supported Networks (`Ethereum`, `BinanceSmartChain`, `Tron`…). Create endpoints also accept aliases (`erc20`, `bsc`, `trc20`…); responses, webhooks and list filters always use the canonical name, and list filters are case-sensitive. The tokens of each network come from `GET /v1/summary/tokens`: do not hard-code them.
- `GET /v1/balances` has one key per token (`balance_<name>`), shared by every network; the key of each token is `balance_key` in `GET /v1/summary/tokens`. Iterate the keys instead of hard-coding them. When `available` has a key, use it: the rest is held by deposits that are not final yet.
- `POST /v1/payout` takes one object or a list of up to 50 and always answers with `summary` and `results[]`, one entry per item in the order sent: 200 when every item was created, 207 when some were refused, 400 when none was created. A refused item carries `results[].error.code`. Give each payout its own `reference_id` (a reused one is refused with `duplicate_payment_reference`, never paid twice) and send an `Idempotency-Key` per request (kept for 24 hours).
- `POST /v1/payin` has no Idempotency-Key: the unique `reference_id` prevents a second payin. After a timeout, resend with the same `reference_id`; `duplicate_reference_id` means the first one was created, and `GET /v1/list-payin?reference_id=…` returns it.
- Store the `internal_reference` returned by every create: the check endpoints take it in the path, and webhooks carry it.
- A payin is `completed` once `received_amount` reaches `minimum_payment` percent of `crypto_amount`, which can be less than the full amount. Compare `received_amount` with `crypto_amount` before delivering.
- Every error is `error.code`, `error.message` and, when useful, `error.details`. Branch on `error.code`, never on the message. 4xx: change the request before sending it again. 5xx: the same request may be retried. 429: wait the seconds in `Retry-After`.
- Webhooks are POSTs with a JSON body to your `webhook_url`. Before trusting the body, verify `X-Bifrost-Signature`: `v1=` plus the HMAC-SHA256 hex of `v1`, `X-Bifrost-Timestamp`, `X-Bifrost-Delivery` and the SHA-256 hex of the raw body, joined by `\n`, keyed by the webhook key of the account (in the panel). Reject a timestamp more than 300 seconds off, compare in constant time, answer 2xx within 30 seconds and process afterwards.
- Branch on the `event` field of the body, not on `X-Bifrost-Webhook-Type`. Payouts: `payment_processed`, `payment_failed`. Payins: `payment_detected` (`funds_available` false, not final), `payment_confirmed` (final, funds available), `payment_reversed`. The same notice can arrive more than once: deduplicate on `data.internal_reference` together with `event`.
- List and check endpoints return only payouts and payins created through the API; those made in the panel are not returned.
- Each key has its own rate limit, 500 requests per 60 seconds by default, reported in `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`.
- There is no public sandbox: every call acts on the real account of the key. Test with small amounts.
- Contract changes, breaking ones included, are listed in the Changelog, newest first.

### Endpoints

| Method | Path | Scope | Idempotency-Key | Endpoint |
| --- | --- | --- | --- | --- |
| POST | `/v1/payout` | `payout:write` | optional | Create Payout Single (https://bifrostcrypto.com/ai/en/create-payout-single.md) |
| POST | `/v1/payout` | `payout:write` | optional | Create Payout Multiple (https://bifrostcrypto.com/ai/en/create-payout-multiple.md) |
| GET | `/v1/list-payout` | `payout:read` | - | List Payouts (https://bifrostcrypto.com/ai/en/list-payouts.md) |
| GET | `/v1/check-payout/:internal_reference` | `payout:read` | - | Check Specific Payout (https://bifrostcrypto.com/ai/en/check-payout.md) |
| POST | `/v1/payin` | `payin:write` | - | Create Payin (https://bifrostcrypto.com/ai/en/create-payin-single.md) |
| GET | `/v1/list-payin` | `payin:read` | - | List Payins (https://bifrostcrypto.com/ai/en/list-payins.md) |
| GET | `/v1/check-payin/:internal_reference` | `payin:read` | - | Check Specific Payin (https://bifrostcrypto.com/ai/en/check-payin.md) |
| GET | `/v1/balances` | `balances:read` | - | Get Balances (https://bifrostcrypto.com/ai/en/get-balances.md) |
| GET | `/v1/statements` | `statements:read` | - | List Statements (https://bifrostcrypto.com/ai/en/list-statements.md) |
| GET | `/v1/statements/summary` | `statements:read` | - | Statements Summary (https://bifrostcrypto.com/ai/en/statements-summary.md) |
| GET | `/v1/summary` | `statements:read` | - | Complete Summary (https://bifrostcrypto.com/ai/en/complete-summary.md) |
| GET | `/v1/summary/tokens` | `prices:read` / `statements:read` | - | Accepted Tokens (https://bifrostcrypto.com/ai/en/accepted-tokens.md) |
| GET | `/v1/summary/fiats` | `prices:read` / `statements:read` | - | Accepted Fiats (https://bifrostcrypto.com/ai/en/accepted-fiats.md) |
| GET | `/v1/get-price` | `prices:read` | - | Get Price (https://bifrostcrypto.com/ai/en/get-price.md) |
| GET | `/v1/get-currencies` | `prices:read` | - | List Available Currencies (https://bifrostcrypto.com/ai/en/get-currencies.md) |
| GET | `/v1/fixed-wallets/supported` | `wallets:read` | - | Supported Networks (https://bifrostcrypto.com/ai/en/fixed-wallets-supported.md) |
| GET | `/v1/fixed-wallets/config` | `wallets:read` | - | Get Config (https://bifrostcrypto.com/ai/en/fixed-wallets-config-get.md) |
| PUT | `/v1/fixed-wallets/config` | `wallets:write` | - | Update Config (https://bifrostcrypto.com/ai/en/fixed-wallets-config-update.md) |
| GET | `/v1/fixed-wallets` | `wallets:read` | - | List Fixed Wallets (https://bifrostcrypto.com/ai/en/list-fixed-wallets.md) |
| POST | `/v1/fixed-wallets` | `wallets:write` | - | Create Fixed Wallet (https://bifrostcrypto.com/ai/en/create-fixed-wallet.md) |

## Contents

- Bifrost Crypto API: https://bifrostcrypto.com/ai/en/introduction.md
- Authentication & Access Control: https://bifrostcrypto.com/ai/en/authentication.md
- Accept a payment: https://bifrostcrypto.com/ai/en/guide-payin.md
- Send a payout: https://bifrostcrypto.com/ai/en/guide-payout.md
- Webhook verification: https://bifrostcrypto.com/ai/en/webhooks.md
- Payout webhooks: https://bifrostcrypto.com/ai/en/webhooks-payments.md
- Deposit webhooks: https://bifrostcrypto.com/ai/en/webhooks-receipts.md
- Error Codes: https://bifrostcrypto.com/ai/en/error-codes.md
- API reference · Payments Payout
- API reference · Deposits Payin
- API reference · Queries
- API reference · Prices
- API reference · Fixed Wallets
- Changelog: https://bifrostcrypto.com/ai/en/changelog.md
- Integration recipes (step by step for agents)

---

## Bifrost Crypto API

Complete reference for the Bifrost Crypto API V1. Integrate multi-chain crypto payments into your platform in minutes.

### Authentication

All routes require the header with your private API key: `X-Bifrost-Invoke`

### Base URL

All requests are made to: `https://bridge.bifrostcrypto.com`

### Format

All request and response bodies use JSON format: `Content-Type: application/json`

### Quick Start

Every API request must include your private key in the header:

```bash
curl -X GET "https://bridge.bifrostcrypto.com/v1/balances" \
  -H "X-Bifrost-Invoke: YOUR_API_KEY"
```

### OpenAPI specification

The whole reference as an OpenAPI 3.1 file. Import it into Postman or Insomnia to try the endpoints, or generate a client in your language. It is built from the same source as this documentation.

https://bifrostcrypto.com/openapi.json

### Dates and times

Date-time fields (created_at, updated_at, expires_at and the others) are YYYY-MM-DD HH:MM:SS in UTC−03:00, without an offset in the value. Date filters (date_from, date_to) are read in the same offset. Two exceptions: the daily payout limit of an API key resets at 00:00 UTC, and fixed-address webhooks send timestamp in ISO 8601 with the offset included.

### Supported Networks

Values accepted in the network field, case-insensitive on create. On create, common aliases are also accepted (erc20, bsc, trc20, matic…) and converted to these names; see Network aliases on the create endpoints. Responses, webhooks and list filters always use the names below. Which tokens are available on each network is returned by GET /v1/summary/tokens.

`Bitcoin`, `Ethereum`, `BinanceSmartChain`, `Tron`, `Solana`, `Ripple`, `Cardano`, `Dogecoin`, `Polygon`, `Avalanche`, `ArbitrumOne`, `Celo`, `Optimism`, `Arbitrum`, `Cronos`, `Base`, `BitcoinCash`, `BitcoinSV`, `BitcoinGold`, `Monero`, `OpBnb`, `Xlayer`, `Litecoin`, `TON`

### HTTP Status Codes

| Code | Meaning |
| --- | --- |
| `200` | Success |
| `201` | Created - fixed address assigned |
| `207` | Multi-Status - payout batch with some items refused |
| `400` | Bad Request - invalid parameters |
| `401` | Unauthorized - missing or invalid API key or signature |
| `403` | Forbidden - no permission for this resource |
| `404` | Not Found - resource or route not found |
| `409` | Conflict - request still in progress or limit reached |
| `422` | Unprocessable - type not supported |
| `429` | Too Many Requests - quota or failed-auth limit |
| `500` | Server Error |
| `502` | Bad Gateway - a service we depend on failed. Retry later. |
| `503` | Service Unavailable - try again later |

### API Categories

- **Payments Payout** (3 endpoints): Create and manage crypto withdrawals. Up to 50 per batch.
- **Deposits Payin** (3 endpoints): Generate deposit wallets and track incoming payments.
- **Queries** (6 endpoints): Check balances, statements, tokens, and account info.
- **Prices** (2 endpoints): Get real-time conversion rates and available currencies.
- **Fixed Wallets** (5 endpoints): Assign and manage permanent deposit addresses per customer.

## Authentication & Access Control

Every /v1 request is authenticated by API Key. Keys can additionally be restricted by scope, by source IP and by request signature.

### API Key

Send your key in the X-Bifrost-Invoke header on every request. There is no login step and no token to refresh: the key is the credential.

```bash
curl -X GET "https://bridge.bifrostcrypto.com/v1/balances" \
  -H "X-Bifrost-Invoke: YOUR_API_KEY"
```

Access is re-checked on every request. Revoking a key, letting it expire, or having API access disabled for the account takes effect on the very next request, without waiting for any cache to expire.

- Missing header: 401 api_key_missing. Unknown, revoked or expired key: 401 api_key_invalid.
- API access disabled for the account: 403 api_access_denied.
- Account e-mail not verified: 403 email_not_verified. Verify the e-mail in the panel; the key starts working on the next request.
- Suspended account: the keys are kept but answer 401 api_key_invalid while the suspension lasts, and work again when the account is reactivated.

### Scopes

A scope is a permission to reach a group of endpoints. When you create a key you can pick which scopes it carries, so a key used only to read balances cannot create a payout even if it leaks.

> A key created without a scope list is read-only: payout:read, payin:read, balances:read, statements:read and prices:read. It has no write scope and no fixed-wallet scope.

| Scope | Grants access to |
| --- | --- |
| `payout:read` | List payouts and check the status of a payout |
| `payout:write` | Create payouts |
| `payin:read` | List payins and check the status of a payin |
| `payin:write` | Create payins |
| `balances:read` | Read account balances |
| `statements:read` | Read statements, summaries, accepted tokens and accepted fiats |
| `prices:read` | Read quotes and the currency catalog |
| `wallets:read` | List fixed wallets and read their configuration |
| `wallets:write` | Create fixed wallets and change their configuration |

Calling an endpoint outside the key's scopes returns 403 stating which scopes would work, so the fix is visible without guesswork. Some endpoints accept more than one, and holding any of them is enough:

```json
{
  "error": {
    "code": "scope_denied",
    "message": "This API key does not have permission for this endpoint",
    "details": {
      "required_scopes": ["payout:write"],
      "granted_scopes": ["balances:read", "payin:read"]
    }
  }
}
```

On keys carrying a write scope (payout:write, payin:write, wallets:write) it is worth adding a second layer on top of the scope: an IP whitelist if your integration calls from a fixed address, or request signing if it does not. Neither is mandatory, but both exist because a credential that moves funds deserves more than a static secret.

### IP Whitelist

> The whitelist is optional, on any scope. Without it the key is accepted from any source, and nothing you already run needs to change. It is an extra layer for callers with a fixed address; if your integration has no fixed IP, use require_signature, which protects a leaked key without depending on the source.

A key can be restricted to a set of source addresses. Individual addresses and CIDR ranges are both accepted, in IPv4 and IPv6:

```json
["203.0.113.10", "198.51.100.0/24", "2001:db8::/32"]
```

A request arrives from one address family at a time. If your infrastructure reaches us over both IPv4 and IPv6, declare both, because an entry for one family does not authorize the other. Requests from outside the list are refused with 403 ip_not_authorized.

### Request Signature (HMAC)

> Signing is optional and is turned on per key, at creation time. Keys that do not require it keep authenticating with X-Bifrost-Invoke alone, with no extra headers, and nothing you already run is affected.

Optional per key. The API Key alone proves that the caller knows a static secret, but it does not prove the request is recent, that the body is intact, or that the call has not already happened. With signing enabled, each request carries a proof valid once and only for a short window.

| Header | Description |
| --- | --- |
| `X-Bifrost-Timestamp` | Unix time in whole seconds, digits only, when the request was built. Refused if it differs from server time by more than 300 seconds. |
| `X-Bifrost-Nonce` | Value unique per request, 16 to 80 characters from A-Z, a-z, 0-9, underscore and hyphen. Accepted once per key. |
| `X-Bifrost-Signature` | HMAC-SHA256 of the canonical string, keyed by your signing secret, in hexadecimal. Send it in lowercase; uppercase is also accepted. |

#### The canonical string

Build these six fields joined by a single newline character, in this exact order:

```text
METHOD
PATH
QUERY
TIMESTAMP
NONCE
SHA256_HEX(BODY)
```

METHOD is uppercase. PATH is the path exactly as it appears in your request line, with its leading slash and the /v1 prefix (for example /v1/check-payout/f47ac10b-58cc-4372-a567-0e02b2c3d479): it is not URL-decoded and a trailing slash is kept. QUERY is the raw query string as sent, with no leading question mark, not sorted and not re-encoded, and is empty when there are no parameters. The last field is the hex SHA-256 of the exact body bytes you send. For requests without a body, hash the empty string, which yields e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.

#### Signing a request

```javascript
const crypto = require("crypto");

const secret = process.env.BIFROST_SIGNING_SECRET;
const method = "POST";
const path = "/v1/payout";
const query = "";
const body = JSON.stringify({ /* ... */ });

const timestamp = Math.floor(Date.now() / 1000).toString();
const nonce = crypto.randomBytes(16).toString("hex");
const bodyHash = crypto.createHash("sha256").update(body).digest("hex");

const canonical = [method, path, query, timestamp, nonce, bodyHash].join("\n");
const signature = crypto
  .createHmac("sha256", secret)
  .update(canonical)
  .digest("hex");

await fetch("https://bridge.bifrostcrypto.com" + path, {
  method,
  headers: {
    "X-Bifrost-Invoke": process.env.BIFROST_API_KEY,
    "X-Bifrost-Timestamp": timestamp,
    "X-Bifrost-Nonce": nonce,
    "X-Bifrost-Signature": signature,
    "Content-Type": "application/json",
  },
  body,
});
```

#### What to keep in mind

- The signing secret is shown once, when the key is created. It is never returned again, so store it before closing the panel.
- The secret is separate from the API Key. Both are needed: the key identifies you, the signature proves the request.
- Hash the same bytes you actually transmit. Serializing the body twice, or reserializing after signing, changes the digest and invalidates the signature.
- Each nonce is accepted once per key. Replaying a request verbatim returns signature_replayed even when everything else is correct.
- Clock drift is the most common cause of signature_timestamp_out_of_range. Keep your servers on NTP.

### Rate Limits

Each key has its own quota over a fixed window aligned to the clock: by default 500 requests per 60 seconds, set when the key is created. Responses carry the current state, so you can pace your usage instead of discovering the limit by hitting it:

```http
X-RateLimit-Limit: 500
X-RateLimit-Remaining: 487
X-RateLimit-Reset: 1760054460
```

- X-RateLimit-Limit is the quota, X-RateLimit-Remaining what is left in the window, and X-RateLimit-Reset the Unix time when the window resets.
- The headers come on every response that passed authentication and the scope check, including 4xx and 5xx answers from the endpoint.
- They are absent on authentication and scope refusals, and on keys configured without a quota, which are not rate limited.

Past the quota, requests return 429 rate_limit_exceeded with a Retry-After header until the window resets. There is no extra penalty:

```http
HTTP/1.1 429 Too Many Requests
Retry-After: 24
X-RateLimit-Limit: 500
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1760054460

{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "You have exceeded the allowed number of requests",
    "details": {
      "limit": 500,
      "window_seconds": 60,
      "retry_after_seconds": 24,
      "retry_after_formatted": "24 seconds"
    }
  }
}
```

#### Repeated authentication failures

Independently of the per-key quota, an IP address that keeps failing authentication is blocked for a while. While blocked it receives 429 too_many_failed_authentications with a Retry-After header: wait that many seconds before trying again. Successful requests never count toward this block.

```json
{
  "error": {
    "code": "too_many_failed_authentications",
    "message": "Too many failed authentication attempts from this address",
    "details": {
      "retry_after_seconds": 900,
      "retry_after_formatted": "15 minutes"
    }
  }
}
```

## Accept a payment

From choosing the token to delivering the order, using payins created through the API.

### 1. Choose the token and network

GET /v1/summary/tokens lists the tokens of the platform per network, with min_receipt and max_receipt. The list is not filtered by your account: the payin request confirms whether the token accepts deposits for you on that network.

```bash
curl "https://bridge.bifrostcrypto.com/v1/summary/tokens" \
  -H "X-Bifrost-Invoke: YOUR_API_KEY"
```

### 2. Create the payin

POST /v1/payin with a reference_id that is unique in your system, the amount (process_by crypto_amount or fiat_amount), webhook_url and, if you want, expires_at in seconds (default 4 hours). Keep the internal_reference from the answer and show the customer the wallet, and the memo when the token needs one.

```bash
curl -X POST "https://bridge.bifrostcrypto.com/v1/payin" \
  -H "X-Bifrost-Invoke: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"reference_id":"order_1001","crypto_currency":"USDT","network":"BinanceSmartChain","process_by":"crypto_amount","crypto_amount":"50","webhook_url":"https://your-site.com/webhook","customer_info":"customer_42"}'
```

### 3. Receive the webhook

Verify X-Bifrost-Signature over the raw body, answer 2xx within 30 seconds and process afterwards. The same notice can arrive more than once: deduplicate on data.internal_reference together with event.

### 4. Check the amount before delivering

The payin is completed once received_amount reaches minimum_payment percent of crypto_amount, which can be less than the full amount. Compare data.received_amount with data.crypto_amount and decide what to do with any difference.

### 5. Choose when to deliver

payment_detected means the transfer was seen (funds_available false): delivering now is your decision and carries the risk of a reversal. payment_confirmed means it is final (funds_available true).

### 6. Handle the exceptions

payment_reversed: the network undid the payment and the credit was removed; check-payin now returns status reversed. is_duplicated true: an extra payment or a payment on another network created a new payin with its own references; link it to the order with customer_info.

### 7. Reconcile

GET /v1/check-payin/{internal_reference} returns the payin at any time. GET /v1/statements lists the balance movements: DEPOSIT for the credit, RATE and FEE for the charges.

### What to do when something fails

POST /v1/payin has no Idempotency-Key. The reference_id is what prevents a second payin.

| Situation | What to do |
| --- | --- |
| Timeout or no answer on POST /v1/payin | Resend with the same reference_id. If the answer is duplicate_reference_id, the first request worked: fetch it with GET /v1/list-payin?reference_id=… |
| 400 | Fix the request. Sending it again unchanged gives the same answer. |
| 500, 502 or 503 | Nothing was created. Retry later with the same reference_id. |
| 429 | Wait the seconds in Retry-After. |
| Webhook did not arrive | Deliveries are retried up to 10 times, about 5 minutes apart. Meanwhile, read the payin with GET /v1/check-payin/{internal_reference}. |

> Payins created in the panel are not returned by the API.

## Send a payout

From checking the balance to confirming the transfer, for one payout or a batch of up to 50.

### 1. Check the balance

GET /v1/balances returns one balance per token; the key of each token is the balance_key returned by GET /v1/summary/tokens. When available has a key, use it: the rest is held by deposits that are not final yet. min_withdrawal and max_withdrawal per token come from GET /v1/summary/tokens.

```bash
curl "https://bridge.bifrostcrypto.com/v1/balances" \
  -H "X-Bifrost-Invoke: YOUR_API_KEY"
```

### 2. Create the payout

POST /v1/payout with one object or a list of up to 50. Give each payout its own reference_id and send an Idempotency-Key per request. feetakenfromamount 1 takes the fees from the amount; 0 adds them to the debit.

```bash
curl -X POST "https://bridge.bifrostcrypto.com/v1/payout" \
  -H "X-Bifrost-Invoke: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6f1c2d3e-0000-4000-8000-000000000001" \
  -d '{"reference_id":"withdraw_2001","wallet":"0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0","crypto_currency":"USDT","network":"Ethereum","process_by":"crypto_amount","crypto_amount":"100","feetakenfromamount":0,"webhook_url":"https://your-site.com/webhook"}'
```

### 3. Read the result of each item

200 means every item was created, 207 some were, 400 none. results has one entry per item, in the order sent, with its index. Keep the internal_reference of each created payout.

### 4. Receive the webhook

payment_processed (data.status success, with the transaction hash in data.transactions) when the transfer is done, or payment_failed when it cannot be completed. Verify the signature and deduplicate on data.internal_reference together with event.

### 5. Reconcile

GET /v1/check-payout/{internal_reference} returns the payout at any time. GET /v1/statements lists WITHDRAWAL for the debit and RATE and FEE for the charges.

### What to do when something fails

Two things protect you from paying twice: the Idempotency-Key of the request and the reference_id of each payout.

| Situation | What to do |
| --- | --- |
| Timeout or no answer | Resend the same body with the same Idempotency-Key within 24 hours: you get the original answer and nothing is processed again. |
| 409 idempotency_key_in_progress | The first request is still running. Wait and retry with the same key. |
| 207 | Fix and resend only the refused items, with a new Idempotency-Key. The created ones stay created. |
| 500 with summary.not_processed | Resend the items that were not processed. A reference_id already used is refused with duplicate_payment_reference, never paid twice. |
| duplicate_payment_reference | The payout already exists. Use the internal_reference and original_status in the result. |
| 429 | Wait the seconds in Retry-After. |

> Payouts created in the panel are not returned by the API.

## Webhook verification

Confirm that a notification was sent by Bifrost before you trust the body.

### Headers Sent in Webhooks

All webhooks are sent with special headers for validation and identification:

```http
POST /your-webhook-url HTTP/1.1
Host: your-api.example.com
Content-Type: application/json
User-Agent: Bifrost/1.0
X-Bifrost-Webhook-Type: payment_processed
X-Bifrost-Timestamp: 1728138000
X-Bifrost-Delivery: 9f2b1c745ae34d1890c61e7d34b0af52
X-Bifrost-Signature: v1=6f3a...c81d
X-Bifrost-Version: 1.0
```

X-Bifrost-Signature, X-Bifrost-Timestamp and X-Bifrost-Delivery accompany every delivery. They are what proves where the message came from.

### Webhook types

The event field of the body says what happened. These are all the values it can take:

| event | Description |
| --- | --- |
| `payment_processed` | Payout completed. data.status is success. X-Bifrost-Webhook-Type: payment_processed. |
| `payment_failed` | Payout could not be completed. data carries only status (failed), reference_id and internal_reference. X-Bifrost-Webhook-Type: payment_processed. |
| `payment_detected` | Deposit seen on-chain; funds_available is false. X-Bifrost-Webhook-Type: payment_confirmed. |
| `payment_confirmed` | Deposit final; funds_available is true. X-Bifrost-Webhook-Type: payment_confirmed. |
| `payment_reversed` | A reorganisation undid the deposit and the credit was removed. data.status is reversed, and check-payin returns status reversed from then on. X-Bifrost-Webhook-Type: payment_confirmed. |
| `fixed_address_pre_revoke · fixed_address_revoked` | Fixed-address lifecycle, described at the end of this page. X-Bifrost-Webhook-Type: fixed_address_lifecycle. |

X-Bifrost-Webhook-Type only names the family of the notice: every deposit event, including payment_detected and payment_reversed, arrives with payment_confirmed in that header, and a failed payout with payment_processed. Always branch on the event field of the body.

### Delivery and retries

Each notice is a POST with a JSON body to the webhook_url you registered. What counts as delivered, and what happens when it is not:

- Answer with any 2xx status within 30 seconds (the connection itself must open within 10 seconds). Any other status, including 3xx, a timeout or a connection error counts as a failure.
- Redirects are not followed. The TLS certificate is verified, HTTPS is required in production, and the connection is made over IPv4 to the address validated for your host.
- A failed delivery is retried about 5 minutes later, up to 10 attempts in total. After the tenth failure the notice is marked failed and can be resent from the panel.
- Every attempt carries the same body, a new X-Bifrost-Delivery and X-Bifrost-Timestamp, and a signature computed for them.
- The same notice can therefore reach you more than once. Deduplicate on data.internal_reference together with event (for fixed-address notices, wallet together with event).
- Reply first and process afterwards: doing slow work before answering is the usual way to hit the 30-second limit.

### Heimdall's Seal

How to confirm the message really came across the bridge

The address receiving your webhooks is public: anyone who discovers the URL can send a request to it. Without verification, a forged POST announcing an approved payment is indistinguishable from a real one, and it usually turns into goods released without the money ever arriving.

#### The signing key

Every account has a dedicated key, generated automatically and never transmitted. We use our copy to sign each delivery; you fetch yours once and store it. Because it does not travel with the webhook, a log leak in your environment does not hand over the key, and that is what makes the signature prove anything at all.

#### Getting the key

The key already exists on your account and lives in the panel, under Settings, in the webhooks area. Displaying it requires your second factor. Copy the value to your server and store it as you would a database password: in an environment variable or a secrets vault, never in versioned code.

> The panel also lets you replace the key, and replacing it invalidates the previous one immediately. Between generating the new one and updating your copy, webhooks are signed with a key you do not have yet, and your verification will fail during that window.

#### Signature headers

| Field | Description |
| --- | --- |
| `X-Bifrost-Signature` | v1= followed by the HMAC-SHA256 in hexadecimal. |
| `X-Bifrost-Timestamp` | Unix timestamp of when the webhook was issued. |
| `X-Bifrost-Delivery` | 32 lowercase hexadecimal characters, unique for this delivery attempt. It changes on every retry. |

#### The signed string

The HMAC is not computed over the raw body, but over this string, joined with line breaks between the fields:

```text
v1
{X-Bifrost-Timestamp}
{X-Bifrost-Delivery}
SHA256_HEX(RAW_BODY)
```

The timestamp and the delivery identifier are part of the calculation on purpose. Were only the body signed, those two headers could be rewritten by anyone without invalidating the signature, and a webhook captured today could be replayed months later looking freshly issued.

#### Verifying

**PHP**

```php
function verifyBifrostWebhook(string $rawBody, array $headers, string $key): bool
{
    $signature = $headers['X-Bifrost-Signature'] ?? '';
    $timestamp = $headers['X-Bifrost-Timestamp'] ?? '';
    $delivery  = $headers['X-Bifrost-Delivery'] ?? '';

    if ($signature === '' || $timestamp === '' || $delivery === '') {
        return false;
    }

    // Reject stale messages. Five minutes is a comfortable margin
    // for clock drift between servers.
    if (abs(time() - (int) $timestamp) > 300) {
        return false;
    }

    $canonical = implode("\n", [
        'v1',
        $timestamp,
        $delivery,
        hash('sha256', $rawBody),
    ]);

    $expected = 'v1=' . hash_hmac('sha256', $canonical, $key);

    // hash_equals, not ==: the comparison must take the same time
    // regardless of where the strings diverge.
    return hash_equals($expected, $signature);
}
```

**Node.js**

```javascript
const crypto = require('crypto');

function verifyBifrostWebhook(rawBody, headers, key) {
  const signature = headers['x-bifrost-signature'];
  const timestamp = headers['x-bifrost-timestamp'];
  const delivery = headers['x-bifrost-delivery'];

  if (!signature || !timestamp || !delivery) return false;

  // Reject stale messages.
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;

  const bodyHash = crypto.createHash('sha256').update(rawBody).digest('hex');
  const canonical = ['v1', timestamp, delivery, bodyHash].join('\n');
  const expected =
    'v1=' + crypto.createHmac('sha256', key).update(canonical).digest('hex');

  const a = Buffer.from(expected);
  const b = Buffer.from(signature);

  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
```

**Python**

```python
import hashlib
import hmac
import time


def verify_bifrost_webhook(raw_body: bytes, headers: dict, key: str) -> bool:
    signature = headers.get("X-Bifrost-Signature", "")
    timestamp = headers.get("X-Bifrost-Timestamp", "")
    delivery = headers.get("X-Bifrost-Delivery", "")

    if not signature or not timestamp or not delivery:
        return False

    # Reject stale messages.
    if abs(time.time() - int(timestamp)) > 300:
        return False

    body_hash = hashlib.sha256(raw_body).hexdigest()
    canonical = "\n".join(["v1", timestamp, delivery, body_hash])
    expected = "v1=" + hmac.new(
        key.encode(), canonical.encode(), hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(expected, signature)
```

#### Pitfalls

- Use the raw body. Deserialising the JSON and serialising it again changes whitespace and key order, and the hash stops matching. Capture the bytes exactly as they arrived, before any parsing.
- Compare in constant time. hash_equals, timingSafeEqual and compare_digest exist for this: an ordinary string comparison leaks, through response time, how many leading characters are correct.
- Handle retries. A delivery that does not receive a 2xx is attempted again, with a different X-Bifrost-Delivery each time. The body has no event identifier: deduplicate on data.internal_reference together with event, so the same payment is not processed twice.
- HTTPS is mandatory in production. http:// URLs are refused both when registering and when sending.

#### Webhook with no signature

If no X-Bifrost-Signature header arrives, do not process the message and contact support.

### Fixed-address lifecycle

Fixed addresses that receive nothing for a long time are taken back. Before and when that happens, a notice is sent to the fixed-address webhook URL set in the panel, not to a per-payment URL; without that URL no notice is sent. Addresses of type evm are permanent and never receive these events. The body is flat, with no data wrapper, and is signed like every other webhook.

| event | Description |
| --- | --- |
| `fixed_address_pre_revoke` | After 120 days without a transaction. revoke_at says when the address will be revoked (about 30 days later) unless it receives a transaction first. |
| `fixed_address_revoked` | After 150 days without a transaction. The address no longer belongs to your account; status is revoked and revoke_at is null. |

```json
{
  "event": "fixed_address_pre_revoke",
  "wallet": "TQ5n3n1Yd8k9QeXbUuG5mJb4Lw2Rr7Vx9P",
  "network": "Tron",
  "token": null,
  "provider": "internal",
  "customer_info": "customer_12345",
  "status": "STATUS_ASSIGNED",
  "last_transaction_at": "2026-05-01 10:00:00",
  "assigned_at": "2026-02-10 09:30:00",
  "revoke_at": "2026-09-28 10:00:00",
  "message": "Address inactive; it will be revoked soon if no transaction is received.",
  "timestamp": "2026-08-29T10:00:00-03:00"
}
```

| Field | Description |
| --- | --- |
| `event` | fixed_address_pre_revoke or fixed_address_revoked |
| `wallet` | The fixed address |
| `network · token` | Network and token of the address; token can be null |
| `provider` | Reserved. Do not rely on it. |
| `customer_info` | The customer_info you gave when requesting the address, or null |
| `status` | Address status at the time of the notice (STATUS_ASSIGNED on pre-revoke), or revoked |
| `last_transaction_at · assigned_at` | Last transaction received and assignment date; inactivity is counted from last_transaction_at, or from assigned_at when the address has never received a transaction |
| `revoke_at` | Expected revocation date on fixed_address_pre_revoke; null on fixed_address_revoked |
| `message` | Human-readable explanation in English |
| `timestamp` | When the notice was generated, in ISO 8601 |

## Payout webhooks

Notifications for a payout created through the API.

### `payment_processed` · Payout Confirmed

Sent when the payout is processed. status is success. The body carries the wallet, the amounts, the network and customer_info.

```json
{
  "data": {
    "status": "success",
    "wallet": "0xBeef0000000000000000000000000000000000b2",
    "network": "BinanceSmartChain",
    "created_at": "2026-03-01 13:00:00",
    "fiat_amount": "40.00",
    "network_fee": "0.00000400",
    "confirmed_at": "2026-03-01 13:00:06",
    "percent_rate": "0.50",
    "reference_id": "33333333-3333-4333-8333-333333333333",
    "transactions": "0xdef0000000000000000000000000000000000000000000000000000000000abc",
    "crypto_amount": "40.00000000",
    "customer_info": "customer@example.com",
    "fiat_currency": "USD",
    "crypto_currency": "USDT",
    "internal_reference": "44444444-4444-4444-8444-444444444444"
  },
  "event": "payment_processed",
  "timestamp": "2026-03-01 13:00:16"
}
```

### `payment_failed` · Payout Failed

Sent when the payout cannot be completed. The body is only status, reference_id and internal_reference.

```json
{
  "data": {
    "status": "failed",
    "reference_id": "55555555-5555-4555-8555-555555555555",
    "internal_reference": "66666666-6666-4666-8666-666666666666"
  },
  "event": "payment_failed",
  "timestamp": "2026-03-01 13:05:00"
}
```

> Your integration must handle this notice: find the payout by reference_id or internal_reference and decide what to tell your customer. To pay again, create a new payout with a new reference_id.

**`customer_info`** (Payin & Payout)

Your own identifier for the customer, such as a user ID, e-mail or order ID. It is returned in webhooks and lists, so you can tell whose transaction it is, including payins Bifrost creates on its own (additional payments and fixed-address deposits), whose reference_id is generated by Bifrost.

`"customer_info": "user@email.com" or "user_12345_order_67890"`

## Deposit webhooks

Notifications for a payin, from the moment the transfer is seen on-chain until the amount is available in your balance.

### Partial payments

A deposit is marked completed, and payment_detected / payment_confirmed are sent, once received_amount reaches minimum_payment percent of crypto_amount. minimum_payment is set per account; below 100 the customer may have paid less than the invoice. Before delivering, compare data.received_amount with data.crypto_amount (both in data.crypto_currency) and decide what to do with the difference.

### `payment_detected` · Payment detected

The transaction appeared on the network and the payin is completed, but this is not the final status. funds_available is false and finality_status is pending_finality while the block can still be reorganised. If you need to move faster, you may release the product on this notice. The amount is held and is not part of the spendable balance until the network reaches the confirmations required.

> Releasing the product at this stage is your decision. A payout of that amount is refused until payment_confirmed. To this day, no payment detected by Bifrost has been reversed; see payment_reversed.

```json
{
  "data": {
    "status": "success",
    "wallet": "0xDeaD0000000000000000000000000000000000a1",
    "network": "BinanceSmartChain",
    "paid_with": [
      {
        "amount": "25.000000000000000000",
        "currency": "USDT"
      }
    ],
    "created_at": "2026-03-01 12:00:00",
    "cross_token": false,
    "fiat_amount": "25.00",
    "network_fee": "0.00000500",
    "confirmed_at": null,
    "percent_rate": "0.50",
    "reference_id": "11111111-1111-4111-8111-111111111111",
    "transactions": "[\"0xabc0000000000000000000000000000000000000000000000000000000000def\"]",
    "confirmations": null,
    "crypto_amount": "25.00000000",
    "customer_info": "customer@example.com",
    "fiat_currency": "USD",
    "is_duplicated": false,
    "crypto_currency": "USDT",
    "finality_status": "pending_finality",
    "funds_available": false,
    "is_wallet_fixed": false,
    "received_amount": "25.00000000",
    "reversal_reason": null,
    "internal_reference": "22222222-2222-4222-8222-222222222222",
    "received_amount_fiat": "25.00",
    "estimated_release_seconds": null,
    "received_amount_fiat_current": "25.00"
  },
  "event": "payment_detected",
  "timestamp": "2026-03-01 12:02:00"
}
```

### `payment_confirmed` · Payment confirmed

The block is final. funds_available is true and the amount is available in your balance. Credit the customer for good on this event.

```json
{
  "data": {
    "status": "success",
    "wallet": "0xDeaD0000000000000000000000000000000000a1",
    "network": "BinanceSmartChain",
    "paid_with": [
      {
        "amount": "25.000000000000000000",
        "currency": "USDT"
      }
    ],
    "created_at": "2026-03-01 12:00:00",
    "cross_token": false,
    "fiat_amount": "25.00",
    "network_fee": "0.00000500",
    "confirmed_at": null,
    "percent_rate": "0.50",
    "reference_id": "11111111-1111-4111-8111-111111111111",
    "transactions": "[\"0xabc0000000000000000000000000000000000000000000000000000000000def\"]",
    "confirmations": null,
    "crypto_amount": "25.00000000",
    "customer_info": "customer@example.com",
    "fiat_currency": "USD",
    "is_duplicated": false,
    "crypto_currency": "USDT",
    "finality_status": "final",
    "funds_available": true,
    "is_wallet_fixed": false,
    "received_amount": "25.00000000",
    "reversal_reason": null,
    "internal_reference": "22222222-2222-4222-8222-222222222222",
    "received_amount_fiat": "25.00",
    "estimated_release_seconds": null,
    "received_amount_fiat_current": "25.00"
  },
  "event": "payment_confirmed",
  "timestamp": "2026-03-01 12:03:00"
}
```

### `payment_reversed` · Payment reversed

Rest assured: to this day, no payment received by Bifrost has ever been reversed.

This notice exists only because of L2 networks such as Arbitrum, Optimism and Base. On them, a payment is detected as soon as it enters an L2 block (payment_detected), within seconds, and becomes final when that block is finalized on the main network (payment_confirmed), a few minutes later. In theory, a transaction can be undone in that window. It is extremely rare, but since the possibility exists, we document it.

If it happens, the credit of that deposit is removed from the balance, data.status is reversed and reversal_reason says why. From then on, check-payin and list-payin also return status reversed.

If you release the product only on payment_confirmed, this notice does not affect you. If you release it on payment_detected, link this notice to the order by internal_reference.

```json
{
  "data": {
    "status": "reversed",
    "wallet": "0xDeaD0000000000000000000000000000000000a1",
    "network": "BinanceSmartChain",
    "paid_with": [
      {
        "amount": "25.000000000000000000",
        "currency": "USDT"
      }
    ],
    "created_at": "2026-03-01 12:00:00",
    "cross_token": false,
    "fiat_amount": "25.00",
    "network_fee": "0.00000500",
    "confirmed_at": null,
    "percent_rate": "0.50",
    "reference_id": "11111111-1111-4111-8111-111111111111",
    "transactions": "[\"0xabc0000000000000000000000000000000000000000000000000000000000def\"]",
    "confirmations": null,
    "crypto_amount": "25.00000000",
    "customer_info": "customer@example.com",
    "fiat_currency": "USD",
    "is_duplicated": false,
    "crypto_currency": "USDT",
    "finality_status": "reorged",
    "funds_available": false,
    "is_wallet_fixed": false,
    "received_amount": "25.00000000",
    "reversal_reason": "reorg",
    "internal_reference": "22222222-2222-4222-8222-222222222222",
    "received_amount_fiat": "25.00",
    "estimated_release_seconds": null,
    "received_amount_fiat_current": "25.00"
  },
  "event": "payment_reversed",
  "timestamp": "2026-03-01 12:04:00"
}
```

### When detection is already final

On some networks the payment is final as soon as it is detected. payment_confirmed is then sent right after payment_detected; payment_detected already carries finality_status final, and payment_confirmed may carry finality_status null. Decide from event and funds_available: not every payment_detected holds the amount.

### Receipt fields

| Field | Type | Description |
| --- | --- | --- |
| `funds_available` | `boolean` | false while the amount is held. true when it can be withdrawn. |
| `finality_status` | `string \| null` | pending_finality while confirmations are still running, final when the block is settled, reorged when the deposit was undone. Can be null on a payment_confirmed for a payment that was final on detection. |
| `confirmations` | `number \| null` | Reserved for the confirmation count. Currently always null; do not rely on it. |
| `estimated_release_seconds` | `number \| null` | Reserved for an estimate of the wait until release. Currently always null; do not rely on it. |
| `reversal_reason` | `string \| null` | Why the credit was removed. Null on payment_detected and payment_confirmed. |
| `received_amount` | `string` | Crypto amount credited on the receipt. |
| `received_amount_fiat` | `string` | Fiat value of the received amount at the invoice rate. |
| `received_amount_fiat_current` | `string` | Fiat value at the rate when the webhook was built. Falls back to the invoice rate if the live price is unavailable. |
| `cross_token` | `boolean` | true when the customer paid in a token other than the invoice token. |
| `paid_with` | `array` | Tokens actually received on-chain. Each item has currency and amount. |
| `is_wallet_fixed` | `boolean` | true when the deposit landed on a fixed address. |

**`is_duplicated`** (Payin Only)

true when Bifrost created this payin itself, for a payment that did not match an open payin: an extra payment to an address whose payin was already completed, or a payment on a different network from the invoice. It is a new payin with its own internal_reference and a reference_id generated by Bifrost. The amount is credited to your balance, and the notice goes to the webhook_url of the original payin.

> When is_duplicated is true, reference_id is not one of yours. Link the payment to your order with customer_info or wallet. The payin is also returned by List Payins and Check Specific Payin.

**`customer_info`**

Your own identifier for the customer, such as a user ID, e-mail or order ID. It is returned in webhooks and lists, so you can tell whose transaction it is, including payins Bifrost creates on its own (additional payments and fixed-address deposits), whose reference_id is generated by Bifrost.

## Error Codes

Every error code the API returns, grouped by kind, with what to do about each one.

### Error format

Every /v1 error has the same shape, with the HTTP status of the table below. Compare error.code, which is stable snake_case. error.message is free English text for your logs. error.details appears only when there is useful data, such as invalid fields, limits or scopes.

```json
{
  "error": {
    "code": "insufficient_total_balance",
    "message": "Insufficient balance to cover all payouts",
    "details": {
      "crypto_currency": "USDT",
      "available": "150.00000000",
      "required": "420.50000000"
    }
  }
}
```

In a POST /v1/payout batch, each refused item carries the same object in results[].error, next to its index and reference_id. The response is 200 when every item succeeded, 207 when some succeeded and some failed, and 400 when none succeeded:

```json
{
  "message": "payment_processing_completed",
  "summary": {
    "total_requested": 3,
    "successful": 1,
    "failed": 2
  },
  "results": [
    {
      "index": 0,
      "reference_id": "order_1001",
      "internal_reference": "BIFROST_REF_123456",
      "status": "created"
    },
    {
      "index": 1,
      "reference_id": "order_1002",
      "error": {
        "code": "invalid_parameters",
        "message": "Invalid parameters provided",
        "details": {
          "fields": {
            "wallet": "Invalid wallet format"
          }
        }
      }
    },
    {
      "index": 2,
      "reference_id": "order_1003",
      "error": {
        "code": "daily_payout_limit_exceeded",
        "message": "Daily payout limit for this API key exceeded"
      }
    }
  ]
}
```

- Validation messages always live in error.details.fields, keyed by field name, both at the top level and inside a batch item.
- A duplicate payout item counts as a failure: it carries error.code duplicate_payment_reference plus duplicate, original_status and original_created_at at item level. A batch made only of duplicates answers 400.
- Branch on error.code, never on error.message: messages can be reworded, codes do not change.
- 4xx means the request must change before it is sent again. 5xx means the failure was on our side and the same request may be retried; on POST /v1/payout, the reference_id still protects you from a double payout.
- An unknown /v1 path returns 404 route_not_found as JSON. A client that asks for text/html in Accept receives an HTML page instead.

### Authentication and Access Errors

| HTTP Code | Error | Description |
| --- | --- | --- |
| `401` | `api_key_missing` | The X-Bifrost-Invoke header was not sent. |
| `401` | `api_key_invalid` | Unknown, revoked or expired API Key. Also returned while the account is suspended. |
| `403` | `api_access_denied` | API access is disabled for this account. Checked on every request. |
| `403` | `email_not_verified` | The account e-mail address has not been verified yet. |
| `403` | `ip_not_authorized` | The source IP is not in the key's whitelist. |
| `400` | `invalid_ip` | The client IP address could not be determined. |
| `401` | `signature_headers_missing` | The key requires a signature and X-Bifrost-Signature, X-Bifrost-Timestamp or X-Bifrost-Nonce is missing. |
| `401` | `signature_timestamp_invalid` | X-Bifrost-Timestamp is not made of digits only (Unix seconds). |
| `401` | `signature_timestamp_out_of_range` | The timestamp differs from server time by more than 300 seconds. |
| `401` | `signature_nonce_length_invalid` | The nonce is shorter than 16 or longer than 80 characters. |
| `401` | `signature_nonce_charset_invalid` | The nonce has characters outside A-Z, a-z, 0-9, underscore and hyphen. |
| `401` | `signature_invalid` | The signature does not match the canonical string. |
| `401` | `signature_replayed` | This nonce was already used by this key. |
| `401` | `signature_secret_unavailable` | The key requires a signature but has no usable secret. Contact support. |
| `403` | `scope_denied` | The key lacks the scope this endpoint requires. details.required_scopes lists what works and details.granted_scopes what the key has. |
| `429` | `rate_limit_exceeded` | The key's quota for the current window is used up. Retry-After and details.retry_after_seconds say when to retry. |
| `429` | `too_many_failed_authentications` | This IP is temporarily blocked after repeated failed authentications. Sent with Retry-After. |
| `401` | `user_not_authenticated` | The request could not be tied to your API key. Retry it; if it happens again, contact support. |

### Request Errors

| HTTP Code | Error | Description |
| --- | --- | --- |
| `404` | `route_not_found` | No /v1 route matches the path and method. Answered before authentication. |
| `400` | `json_parse_exception` | The request body is not valid JSON. Any /v1 route. |
| `400` | `json_parse_failed` | POST /v1/payout: the body decoded to null or false. |
| `400` | `invalid_parameters` | Invalid or missing parameters. details.fields maps each field to its message (POST /v1/payout with every item invalid, POST /v1/payin, fixed wallets, statements filters). |
| `400` | `invalid_payment_list` | POST /v1/payout: the body is empty or is neither a payout object nor a list of payouts. |
| `400` | `max_payouts_exceeded` | POST /v1/payout: more than 50 payouts in one request. |
| `409` | `idempotency_key_in_progress` | A request with this Idempotency-Key is still running. Wait and retry with the same key. |
| `400` | `crypto_amount_required` | POST /v1/payin: process_by is crypto_amount and crypto_amount is missing. |
| `400` | `fiat_amount_required` | POST /v1/payin: process_by is fiat_amount and fiat_amount is missing. |
| `400` | `fiat_currency_required` | POST /v1/payin: process_by is fiat_amount and fiat_currency is missing. |
| `400` | `invalid_page_number` | list-payout / list-payin: page below 1. |
| `400` | `invalid_per_page_limit` | list-payout / list-payin: per_page outside 1 to 100. |
| `400` | `invalid_status_filter` | list-payout / list-payin: unknown status value. |
| `400` | `invalid_network_filter` | list-payout / list-payin: unknown network. |
| `400` | `invalid_date_from` | list-payout / list-payin: date_from is not a date. |
| `400` | `invalid_date_to` | list-payout / list-payin: date_to is not a date. |
| `400` | `invalid_reference_id` | check-payout / check-payin: reference shorter than 10 or longer than 756 characters. Statements: reference_id longer than 255. |
| `400` | `invalid_pagination` | Statements: page or per_page out of range. |
| `400` | `invalid_format` | Statements: date_from or date_to is not a date (details.field). |
| `400` | `invalid_date_range` | Statements: date_from is after date_to. |
| `400` | `date_range_too_wide` | Statements: the range is wider than allowed. details.max_days states the ceiling. |
| `400` | `invalid_amount` | Statements: amount_from or amount_to is invalid, or amount_from is above amount_to. |
| `400` | `missing_parameters` | get-price: currency1 or currency2 is missing. |
| `400` | `invalid_currency` | get-price: currency1 or currency2 is not a valid currency code. |
| `400` | `invalid_webhook_url` | PUT /v1/fixed-wallets/config: webhook_url points to a disallowed destination. |

### Business Errors

| HTTP Code | Error | Description |
| --- | --- | --- |
| `400` | `insufficient_total_balance` | POST /v1/payout: the items of one currency add up to more than the available balance. Nothing is debited. details carries crypto_currency, available and required. |
| `404` | `user_not_found` | POST /v1/payout: the account setup is incomplete. Contact support. |
| `404` | `record_not_found` | The payout or payin looked up by check-payout / check-payin does not exist on this account. Also returned when account data the request needs is missing. |
| `400` | `duplicate_reference_id` | POST /v1/payin: a payin with this reference_id already exists on the account. |
| `400` | `invalid_network_or_deposits_disabled` | POST /v1/payin: unknown network, or deposits are disabled on it. |
| `400` | `unsupported_token_or_deposits_disabled` | POST /v1/payin: unsupported token, or deposits are disabled for it. |
| `400` | `token_network_mismatch` | POST /v1/payin: the token is not available on this network. |
| `400` | `memo_required` | POST /v1/payin: this token requires a memo. |
| `400` | `amount_out_of_limits` | POST /v1/payin: the crypto amount is outside the token's limits. details carries min_allowed and max_allowed. |
| `400` | `value_below_minimum` | POST /v1/payin with fiat_amount: the converted amount is below the minimum. |
| `400` | `value_above_maximum` | POST /v1/payin with fiat_amount: the converted amount is above the maximum. |
| `400` | `token_not_supported_or_inactive` | POST /v1/payin with fiat_amount: the token is not supported or is inactive for conversion. |
| `400` | `crypto_amount_does_not_correspond_to_fiat_amount_provided` | POST /v1/payin: crypto_amount and fiat_amount were both sent and do not match the current rate. |
| `400` | `invalid_parameters_for_process_by` | POST /v1/payin: the amounts sent do not fit the chosen process_by. |
| `404` | `price_not_found` | get-price: there is no price for this currency pair. |
| `403` | `feature_disabled` | Fixed wallets are not enabled for the account (config, config update and create). |
| `422` | `type_not_supported` | POST /v1/fixed-wallets: this type is not available for fixed addresses. |
| `403` | `limit_not_allowed` | POST /v1/fixed-wallets: the account's limit for this type is zero. |
| `409` | `limit_reached` | POST /v1/fixed-wallets: the account already holds the maximum number of fixed addresses. |
| `409` | `no_wallet_available` | POST /v1/fixed-wallets: no address of that type is available right now. Retry later. |

### Payout Batch Item Errors (results[].error)

| HTTP Code | Error | Description |
| --- | --- | --- |
| `207/400` | `invalid_parameters` | The item failed validation. error.details.fields maps each field to its message. |
| `207/400` | `missing_required_fields` | A required field of the item is missing. |
| `207/400` | `duplicate_reference_id_in_request` | The same reference_id appears earlier in the same batch. |
| `207/400` | `duplicate_payment_reference` | The reference_id was already used on this account. The item carries duplicate, original_status, original_created_at and the original internal_reference; no second transfer is made. |
| `207/400` | `invalid_crypto_amount` | process_by is crypto_amount and crypto_amount is missing or invalid. |
| `207/400` | `invalid_fiat_amount` | process_by is fiat_amount and fiat_amount is missing or invalid. |
| `207/400` | `fiat_currency_required` | process_by is fiat_amount and fiat_currency is missing. |
| `207/400` | `fiat_currency_not_accepted` | The fiat_currency is not accepted. |
| `207/400` | `memo_required` | This token requires a memo. |
| `207/400` | `invalid_network_or_withdrawals_disabled` | Unknown network, or withdrawals are disabled on it. |
| `207/400` | `unsupported_token_or_withdrawals_disabled` | Unsupported token, or withdrawals are disabled for it. |
| `207/400` | `token_network_mismatch` | The token is not available on this network. |
| `207/400` | `token_not_active` | The token is not active for payouts or for conversion. |
| `207/400` | `invalid_format` | The wallet address is not valid for the network. |
| `207/400` | `limit_exceeded` | The amount is outside the token's minimum and maximum (also when the fiat amount converts outside them). |
| `207/400` | `amount_does_not_cover_fees` | Fees consume the whole amount, so the recipient would receive nothing. |
| `207/400` | `crypto_amount_does_not_correspond_to_fiat_amount_provided` | crypto_amount and fiat_amount do not match the current rate. |
| `207/400` | `price_unavailable` | No price to convert the amount, or the key has a daily limit and the token has no USD quote to count the payout. |
| `207/400` | `insufficient_balance` | The available balance does not cover this item. |
| `207/400` | `daily_payout_limit_exceeded` | The item would exceed the daily limit configured for the key. |
| `207/400` | `daily_payout_limit_unavailable` | The key has a daily limit but the spend could not be recorded. Retry the item. |
| `207/400` | `error_fetching_active_tokens · error_fetching_active_currencies` | The token or currency catalog could not be loaded. Retry the item. |
| `207/400` | `user_not_found · record_not_found` | Account data the payout needs is missing. |
| `207/400` | `internal_error · database_error · transaction_failed · network_unavailable · duplicate_record` | Recording the payout failed on our side and was rolled back. Nothing was debited for this item; check it with check-payout or list-payout before resending. |

### System Errors

| HTTP Code | Error | Description |
| --- | --- | --- |
| `500` | `internal_error` | Unexpected failure. On POST /v1/payout, a failure mid-batch also returns summary (with not_processed) and results at the top level, so you can resend only what is missing. |
| `500` | `database_error` | Statements: the query failed. |
| `500` | `error_fetching_active_currencies` | POST /v1/payout and POST /v1/payin: the token or fiat catalog could not be loaded. Nothing was processed. |
| `500` | `token_type_not_found · receipt_creation_failed · database_transaction_failed` | POST /v1/payin: the payin could not be created on our side. |
| `502` | `external_processor_error` | POST /v1/payin and POST /v1/fixed-wallets: the deposit address could not be created right now. Nothing was created; retry later. |
| `503` | `no_processor_configured · no_wallet_available` | POST /v1/payin: no processor or no deposit address is available for this token and network right now. Retry later. |
| `503` | `price_not_available_for_conversion · price_not_available_for_validation` | POST /v1/payin: no price is available to convert or check the amount. Retry later. |
| `503` | `service_unavailable` | POST /v1/payin and summary endpoints: a dependency is temporarily unavailable. |
| `500` | `config_update_failed` | PUT /v1/fixed-wallets/config: the configuration could not be saved. |
| `500` | `assign_failed` | POST /v1/fixed-wallets: the address could not be assigned. |

### Best Practices

#### Security

**Do**

- Store your API Key in environment variables
- Configure IP Whitelist
- Verify the X-Bifrost-Signature of every webhook
- Use HTTPS in all requests
- Implement request timeouts

**Don't**

- Share API Key publicly
- Commit API Keys to Git
- Use the same API Key in multiple environments

#### Performance

- Statements and summary data can be up to 5 minutes old; avoid polling them more often than that
- Implement proper pagination
- Avoid excessive requests (respect rate limit)
- Process webhooks asynchronously

#### Idempotency

- Use a unique, meaningful reference_id: it is the lock against a double payout, and presenting it again returns the original payment instead of creating another
- Send an Idempotency-Key on batches: if the connection drops, retrying with the same key returns the original response instead of reprocessing
- POST /v1/payin has no Idempotency-Key: after a timeout resend with the same reference_id, and on duplicate_reference_id fetch the payin with GET /v1/list-payin?reference_id=…
- Handle 207 and 500 by reading results: both carry what was already created, so you resend only what is missing
- Handle webhook retries by deduplicating on data.internal_reference together with event, so the same notice is not processed twice

---

## API reference · Payments Payout

Endpoints for creating and tracking payouts (withdrawals). POST /v1/payout takes one payout object or a list of up to 50, and always answers with a summary and one result per item.

### Create Payout Single

`POST https://bridge.bifrostcrypto.com/v1/payout`

Required scope: `payout:write` (a new key with no list is read-only)

Creates one payout (withdrawal) to a destination wallet.

The body is a single payout object, and the response has the same shape as a batch of one: 200 when the payout is created, 400 when it is refused. A refusal after validation comes in results[0].error, not at the top level; only request-level problems (invalid parameters, balance pre-check, malformed body) answer with a top-level error. In a created result, crypto_amount is the net amount the recipient receives (lower than what you sent when feetakenfromamount is 1), fiat_amount is its value in fiat_currency, network_fee is the network fee for the transfer (reported even when your account does not pay it) and memo is null unless the token uses one.

#### Headers

| Key | Value | Description |
| --- | --- | --- |
| `X-Bifrost-Invoke` | `{{api_key}}` | Private API key |
| `Content-Type` | `application/json` | - |
| `Idempotency-Key` | `{{uuid}}` | Optional, up to 255 characters (a longer key is ignored and the request runs without idempotency). Scoped to your account and kept for 24 hours: resending the same key returns the original status and body instead of processing again, even if the body differs. While the first request is still running (up to 15 minutes) a resend gets 409 idempotency_key_in_progress. Not stored when the request fails before any payout is attempted (top-level 4xx or 500), so a retry with the same key is processed normally. |

#### Body Parameters

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `wallet` | `string` | required | Destination address, 20 to 120 characters (surrounding spaces are trimmed). A length outside that range fails validation (invalid_parameters). The format is then checked against the network: EVM is 0x plus 40 hex characters; Tron, Bitcoin and the other networks use their own formats. A mismatch returns invalid_format on that item. |
| `crypto_currency` | `string` | required | Token symbol, case-sensitive (e.g.: USDT, BTC, ETH). Must be active for withdrawals; usdt is refused. |
| `process_by` | `string` | required | crypto_amount or fiat_amount. Selects which amount is used; the other amount, if sent, is ignored. |
| `reference_id` | `string` | required | Your reference for this payout, 1 to 255 characters (string or integer). Unique per account across all payouts. Sending one that already exists creates nothing: the item is refused with duplicate_payment_reference and duplicate: true, carrying the original payout's references. The duplicate is detected only after the amount, balance and daily-limit checks, so a resend can also be refused by one of those. |
| `network` | `string` | required | One of: Bitcoin, Ethereum, BinanceSmartChain, Tron, Solana, Ripple, Cardano, Dogecoin, Polygon, Avalanche, ArbitrumOne, Celo, Optimism, Arbitrum, Cronos, Base, BitcoinCash, BitcoinSV, BitcoinGold, Monero, OpBnb, Xlayer, Litecoin, TON. Case-insensitive; the aliases in Network aliases are also accepted (erc20, bsc, trc20...) and the payout is stored and returned with the canonical name. Withdrawals must be enabled for the token on that network, otherwise the item returns invalid_network_or_withdrawals_disabled, unsupported_token_or_withdrawals_disabled or token_network_mismatch. |
| `feetakenfromamount` | `number` | required | 0 or 1 (number or string). 1 = fees deducted from the amount (the recipient receives less; the debit is the amount you sent). 0 = fees added on top of the debit (the recipient receives the full amount). When your account pays the network fee and holds enough of the network's native coin (e.g. ETH on Ethereum), that fee is debited from the native balance instead of the token. |
| `webhook_url` | `string` | required | Callback URL for status updates, up to 756 characters. Must be https:// and resolve to a public address; a private, local or blocked destination fails validation (invalid_parameters on webhook_url). |
| `fiat_currency` | `string` | - | Required when process_by is fiat_amount (otherwise the item returns fiat_currency_required). Fiat code, case-insensitive (e.g. USD, BRL). When omitted with crypto_amount, your account's preferred currency is used, or USD, to compute fiat_amount in the response. |
| `crypto_amount` | `string` | - | Required when process_by is crypto_amount (otherwise the item returns invalid_crypto_amount). Plain decimal only: digits with an optional dot, string or JSON number. Scientific notation, hex, thousands separators, signs and surrounding spaces are rejected. Decimals beyond the token's precision are truncated. |
| `fiat_amount` | `string` | - | Required when process_by is fiat_amount (otherwise the item returns invalid_fiat_amount). Converted to crypto at the current price. Same format rules as crypto_amount. |
| `customer_info` | `string` | - | Your customer identifier, up to 200 characters, no whitespace. |
| `memo` | `string` | - | Required for tokens that need a destination memo, such as TON (otherwise the item returns memo_required). When sent it must be 8 to 15 digits, for any token; it is discarded for tokens that do not use one. |

#### Request Body

```json
{
  "wallet": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0",
  "crypto_currency": "USDT",
  "fiat_currency": "USD",
  "crypto_amount": "100.50",
  "process_by": "crypto_amount",
  "reference_id": "order_12345",
  "network": "Ethereum",
  "feetakenfromamount": 0,
  "webhook_url": "https://your-site.com/webhook",
  "customer_info": "customer123"
}
```

#### cURL Example

```bash
curl -X POST "https://bridge.bifrostcrypto.com/v1/payout" \
  -H "X-Bifrost-Invoke: YOUR_API_KEY" \
  -H "Idempotency-Key: 3f1c8b2a-9d54-4f6e-8c31-7b0a2e5d9411" \
  -H "Content-Type: application/json" \
  -d '{
  "wallet": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0",
  "crypto_currency": "USDT",
  "fiat_currency": "USD",
  "crypto_amount": "100.50",
  "process_by": "crypto_amount",
  "reference_id": "order_12345",
  "network": "Ethereum",
  "feetakenfromamount": 0,
  "webhook_url": "https://your-site.com/webhook",
  "customer_info": "customer123"
}'
```

#### Response Example

```json
{
  "message": "payment_processing_completed",
  "summary": {
    "total_requested": 1,
    "successful": 1,
    "failed": 0
  },
  "results": [
    {
      "index": 0,
      "reference_id": "order_12345",
      "internal_reference": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
      "crypto_amount": "100.5",
      "fiat_amount": "100.50000000",
      "crypto_currency": "USDT",
      "fiat_currency": "USD",
      "network_fee": "0.00100000",
      "wallet": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0",
      "memo": null,
      "network": "Ethereum",
      "process_by": "crypto_amount",
      "status": "created"
    }
  ]
}
```

#### Network aliases

On create, network also accepts these aliases. Case, spaces, hyphens and underscores are ignored (ERC-20, erc_20 and erc20 are the same). Responses, webhooks and listings always return the canonical name. bnb means BinanceSmartChain (for opBNB send OpBnb) and arb means Arbitrum (for ArbitrumOne send ArbitrumOne).

| Network | Aliases |
| --- | --- |
| `Ethereum` | `eth, eth-mainnet, erc20` |
| `Tron` | `trx, trc20` |
| `BinanceSmartChain` | `bnb, bsc, bep20, bnbchain, bnb-smart-chain` |
| `Arbitrum` | `arb, arbitrum-evm` |
| `Polygon` | `matic, pol, polygon-pos` |
| `Optimism` | `op, op-mainnet` |
| `Avalanche` | `avax, avaxc, avalanche-c` |
| `Solana` | `sol` |
| `Bitcoin` | `btc` |
| `Litecoin` | `ltc` |
| `Monero` | `xmr` |
| `Xlayer` | `okb` |

#### Item refusal codes

Codes an entry can carry in results[].error.code. The rest of the batch continues and nothing is debited for the refused item. Codes marked No in reference_id come back without it: match those items by index.

| Code | When | reference_id | What to do |
| --- | --- | --- | --- |
| `invalid_parameters` | A field failed validation; details.fields names it. | Yes | Fix the field and resend the item. |
| `duplicate_payment_reference` | reference_id was already used by an earlier payout. | Yes | Use a new reference_id, or check the earlier payout with GET /v1/list-payout. |
| `duplicate_reference_id_in_request` | Two items of the same request share a reference_id. | No | Give each item its own reference_id. |
| `invalid_crypto_amount · invalid_fiat_amount · fiat_currency_required` | The amount or currency required by process_by is missing or invalid. | No | Fix the item and resend it. |
| `invalid_network_or_withdrawals_disabled · unsupported_token_or_withdrawals_disabled · token_network_mismatch` | The token or network does not accept withdrawals for your account. | No | Choose another token or network (GET /v1/summary/tokens). |
| `token_not_active` | The token is not active. | Yes | Choose another token. |
| `memo_required` | The token needs a memo. | No | Send memo. |
| `invalid_format` | The address does not match the network. | Yes | Check the address and network. |
| `limit_exceeded` | Below the token minimum after fees, or above the maximum. | Yes | Adjust the amount. |
| `amount_does_not_cover_fees` | With feetakenfromamount = 1, fees consume the whole amount. | Yes | Increase the amount. |
| `insufficient_balance` | The balance does not cover the item and its fees. | Yes | Top up the balance. |
| `daily_payout_limit_exceeded` | The key's daily payout limit (USD, UTC day) would be exceeded. | Yes | Wait for the next UTC day or ask for a higher limit. |
| `price_unavailable · daily_payout_limit_unavailable` | A quote needed for the item was not available. | Yes | Retry the item later. |
| `error_fetching_active_tokens` | The token catalogue could not be loaded. | No | Retry the item later. |
| `database_error · internal_error` | Failure on our side. | Yes | Retry the item later. |
| `record_not_found · user_not_found` | The account setup is incomplete. | Yes | Contact support. |

#### Errors

Every error has the shape error.code, error.message and, when useful, error.details. Authentication, scope and rate-limit errors (401, 403, 429) apply to every endpoint and are described under Authentication.

| Status | Code | When | What to do |
| --- | --- | --- | --- |
| `400` | `json_parse_exception · json_parse_failed` | The body is missing or is not valid JSON. | Fix the request before sending it again. |
| `400` | `invalid_payment_list` | The body is neither a payout object nor a non-empty list. | Fix the request before sending it again. |
| `400` | `max_payouts_exceeded` | More than 50 payouts in one request. | Split into requests of up to 50. |
| `400` | `invalid_parameters` | Every item failed validation; details.fields is keyed by index.field. | Fix the request before sending it again. |
| `400` | `insufficient_total_balance` | The items of one currency add up to more than the available balance. Nothing is debited. | Top up the balance or send fewer payouts. |
| `200 · 207 · 400` | `results[].error.code` | An item was refused after validation (see the Item refusal codes table). | Fix and resend only the refused items, with a new Idempotency-Key. |
| `404` | `user_not_found · record_not_found` | The account setup is incomplete. | Contact support. |
| `409` | `idempotency_key_in_progress` | A request with this Idempotency-Key is still running. | Wait and retry with the same key. |
| `500` | `error_fetching_active_currencies` | The token catalogue could not be loaded. Nothing was processed. | Nothing was changed. Retry later. |
| `500` | `internal_error` | Unexpected failure; summary and results show what was created. | Resend only the items counted in summary.not_processed. |

#### Other responses

Responses that are not the happy path and still need handling in your integration.

##### 400 · Payout refused

The payout passed validation but was refused. The HTTP status is 400 and the reason is in results[0].error.code. The codes and what each one means are listed under Item refusal codes in Create Payout Multiple. Nothing is debited.

```json
{
  "message": "payment_processing_completed",
  "summary": {
    "total_requested": 1,
    "successful": 0,
    "failed": 1
  },
  "results": [
    {
      "index": 0,
      "reference_id": "order_12345",
      "error": {
        "code": "insufficient_balance",
        "message": "Insufficient balance for this operation"
      }
    }
  ]
}
```

##### 400 · reference_id already used

No second payout is created. The result carries the original payout's internal_reference, its current status (pending, completed, failed or refunded) and when it was created.

```json
{
  "message": "payment_processing_completed",
  "summary": {
    "total_requested": 1,
    "successful": 0,
    "failed": 1
  },
  "results": [
    {
      "index": 0,
      "reference_id": "order_12345",
      "internal_reference": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
      "duplicate": true,
      "original_status": "pending",
      "original_created_at": "2026-01-16 10:00:00",
      "error": {
        "code": "duplicate_payment_reference",
        "message": "A payout with this reference_id already exists"
      }
    }
  ]
}
```

##### 400 · Invalid parameters

A field failed validation. details.fields is keyed by index.field (always index 0 here) with the reason for each field.

```json
{
  "error": {
    "code": "invalid_parameters",
    "message": "Invalid parameters provided",
    "details": {
      "fields": {
        "0.crypto_amount": "Crypto amount must be a positive decimal number",
        "0.network": "Invalid network"
      }
    }
  }
}
```

##### 400 · Not enough balance

The amount selected by process_by exceeds the available balance after holds. Checked before anything is debited.

```json
{
  "error": {
    "code": "insufficient_total_balance",
    "message": "Insufficient balance to cover all payouts",
    "details": {
      "crypto_currency": "USDT",
      "available": "50.00000000",
      "required": "100.500000000000000000"
    }
  }
}
```

##### 409 · Idempotency-Key still running

The first request with this key has not finished yet. Wait and retry with the same key.

```json
{
  "error": {
    "code": "idempotency_key_in_progress",
    "message": "A request with this Idempotency-Key is still being processed"
  }
}
```

##### 400 / 404 / 500 · Request-level errors

Nothing is processed. The codes are listed in the Errors table.

```json
{
  "error": {
    "code": "json_parse_exception",
    "message": "Exception while parsing JSON payload"
  }
}
```

### Create Payout Multiple

`POST https://bridge.bifrostcrypto.com/v1/payout`

Required scope: `payout:write` (a new key with no list is read-only)

Creates up to 50 payouts in one request.

Send an array of payout objects with the same fields as Create Payout Single. Items are processed one by one, each in its own transaction: a refused item is reported on its own index and the rest of the batch still goes out. The HTTP status is 200 when every item was created, 207 when some were created and some refused, and 400 when every item was refused; in all three cases the body carries summary and one entry per item in results, in the order sent. If every item fails validation, the answer is instead a top-level invalid_parameters with no results. More than 50 items returns 400 max_payouts_exceeded and nothing is processed.

#### Headers

| Key | Value | Description |
| --- | --- | --- |
| `X-Bifrost-Invoke` | `{{api_key}}` | Private API key |
| `Content-Type` | `application/json` | - |
| `Idempotency-Key` | `{{uuid}}` | Optional, up to 255 characters (a longer key is ignored and the batch runs without idempotency). Scoped to your account and kept for 24 hours: resending the same key returns the original status and body verbatim instead of reprocessing, even if the body differs, so use a new key for a new batch. While the first request is still running (up to 15 minutes) a resend gets 409 idempotency_key_in_progress. Not stored when the request fails before any payout is attempted (top-level 4xx) or is interrupted (500), so a retry with the same key is processed again; reference_id uniqueness still prevents a second payout for items already created. |

#### Request Body

```json
[
  {
    "wallet": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0",
    "crypto_currency": "USDT",
    "fiat_currency": "USD",
    "crypto_amount": "100.50",
    "process_by": "crypto_amount",
    "reference_id": "order_12345",
    "network": "Ethereum",
    "feetakenfromamount": 0,
    "webhook_url": "https://your-site.com/webhook",
    "customer_info": "customer123"
  },
  {
    "wallet": "0x8Ba1f109551bD432803012645Ac136ddd64DBA72",
    "crypto_currency": "BNB",
    "crypto_amount": "0.5",
    "process_by": "crypto_amount",
    "reference_id": "order_12346",
    "network": "BinanceSmartChain",
    "feetakenfromamount": 1,
    "webhook_url": "https://your-site.com/webhook"
  }
]
```

#### cURL Example

```bash
curl -X POST "https://bridge.bifrostcrypto.com/v1/payout" \
  -H "X-Bifrost-Invoke: YOUR_API_KEY" \
  -H "Idempotency-Key: 3f1c8b2a-9d54-4f6e-8c31-7b0a2e5d9411" \
  -H "Content-Type: application/json" \
  -d '[
  {
    "wallet": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0",
    "crypto_currency": "USDT",
    "fiat_currency": "USD",
    "crypto_amount": "100.50",
    "process_by": "crypto_amount",
    "reference_id": "order_12345",
    "network": "Ethereum",
    "feetakenfromamount": 0,
    "webhook_url": "https://your-site.com/webhook",
    "customer_info": "customer123"
  },
  {
    "wallet": "0x8Ba1f109551bD432803012645Ac136ddd64DBA72",
    "crypto_currency": "BNB",
    "crypto_amount": "0.5",
    "process_by": "crypto_amount",
    "reference_id": "order_12346",
    "network": "BinanceSmartChain",
    "feetakenfromamount": 1,
    "webhook_url": "https://your-site.com/webhook"
  }
]'
```

#### Response Example

```json
{
  "message": "payment_processing_completed",
  "summary": {
    "total_requested": 2,
    "successful": 2,
    "failed": 0
  },
  "results": [
    {
      "index": 0,
      "reference_id": "order_12345",
      "internal_reference": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
      "crypto_amount": "100.5",
      "fiat_amount": "100.50000000",
      "crypto_currency": "USDT",
      "fiat_currency": "USD",
      "network_fee": "0.00100000",
      "wallet": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0",
      "memo": null,
      "network": "Ethereum",
      "process_by": "crypto_amount",
      "status": "created"
    },
    {
      "index": 1,
      "reference_id": "order_12346",
      "internal_reference": "9b2e4c1a-7d3f-4e8b-a1c6-5f0d2e9b7a43",
      "crypto_amount": "0.4924",
      "fiat_amount": "300.00000000",
      "crypto_currency": "BNB",
      "fiat_currency": "USD",
      "network_fee": "0.00010000",
      "wallet": "0x8Ba1f109551bD432803012645Ac136ddd64DBA72",
      "memo": null,
      "network": "BinanceSmartChain",
      "process_by": "crypto_amount",
      "status": "created"
    }
  ]
}
```

#### Network aliases

On create, network also accepts these aliases. Case, spaces, hyphens and underscores are ignored (ERC-20, erc_20 and erc20 are the same). Responses, webhooks and listings always return the canonical name. bnb means BinanceSmartChain (for opBNB send OpBnb) and arb means Arbitrum (for ArbitrumOne send ArbitrumOne).

| Network | Aliases |
| --- | --- |
| `Ethereum` | `eth, eth-mainnet, erc20` |
| `Tron` | `trx, trc20` |
| `BinanceSmartChain` | `bnb, bsc, bep20, bnbchain, bnb-smart-chain` |
| `Arbitrum` | `arb, arbitrum-evm` |
| `Polygon` | `matic, pol, polygon-pos` |
| `Optimism` | `op, op-mainnet` |
| `Avalanche` | `avax, avaxc, avalanche-c` |
| `Solana` | `sol` |
| `Bitcoin` | `btc` |
| `Litecoin` | `ltc` |
| `Monero` | `xmr` |
| `Xlayer` | `okb` |

#### Item refusal codes

Codes an entry can carry in results[].error.code. The rest of the batch continues and nothing is debited for the refused item. Codes marked No in reference_id come back without it: match those items by index.

| Code | When | reference_id | What to do |
| --- | --- | --- | --- |
| `invalid_parameters` | A field failed validation; details.fields names it. | Yes | Fix the field and resend the item. |
| `duplicate_payment_reference` | reference_id was already used by an earlier payout. | Yes | Use a new reference_id, or check the earlier payout with GET /v1/list-payout. |
| `duplicate_reference_id_in_request` | Two items of the same request share a reference_id. | No | Give each item its own reference_id. |
| `invalid_crypto_amount · invalid_fiat_amount · fiat_currency_required` | The amount or currency required by process_by is missing or invalid. | No | Fix the item and resend it. |
| `invalid_network_or_withdrawals_disabled · unsupported_token_or_withdrawals_disabled · token_network_mismatch` | The token or network does not accept withdrawals for your account. | No | Choose another token or network (GET /v1/summary/tokens). |
| `token_not_active` | The token is not active. | Yes | Choose another token. |
| `memo_required` | The token needs a memo. | No | Send memo. |
| `invalid_format` | The address does not match the network. | Yes | Check the address and network. |
| `limit_exceeded` | Below the token minimum after fees, or above the maximum. | Yes | Adjust the amount. |
| `amount_does_not_cover_fees` | With feetakenfromamount = 1, fees consume the whole amount. | Yes | Increase the amount. |
| `insufficient_balance` | The balance does not cover the item and its fees. | Yes | Top up the balance. |
| `daily_payout_limit_exceeded` | The key's daily payout limit (USD, UTC day) would be exceeded. | Yes | Wait for the next UTC day or ask for a higher limit. |
| `price_unavailable · daily_payout_limit_unavailable` | A quote needed for the item was not available. | Yes | Retry the item later. |
| `error_fetching_active_tokens` | The token catalogue could not be loaded. | No | Retry the item later. |
| `database_error · internal_error` | Failure on our side. | Yes | Retry the item later. |
| `record_not_found · user_not_found` | The account setup is incomplete. | Yes | Contact support. |

#### Errors

Every error has the shape error.code, error.message and, when useful, error.details. Authentication, scope and rate-limit errors (401, 403, 429) apply to every endpoint and are described under Authentication.

| Status | Code | When | What to do |
| --- | --- | --- | --- |
| `400` | `json_parse_exception · json_parse_failed` | The body is missing or is not valid JSON. | Fix the request before sending it again. |
| `400` | `invalid_payment_list` | The body is neither a payout object nor a non-empty list. | Fix the request before sending it again. |
| `400` | `max_payouts_exceeded` | More than 50 payouts in one request. | Split into requests of up to 50. |
| `400` | `invalid_parameters` | Every item failed validation; details.fields is keyed by index.field. | Fix the request before sending it again. |
| `400` | `insufficient_total_balance` | The items of one currency add up to more than the available balance. Nothing is debited. | Top up the balance or send fewer payouts. |
| `200 · 207 · 400` | `results[].error.code` | An item was refused after validation (see the Item refusal codes table). | Fix and resend only the refused items, with a new Idempotency-Key. |
| `404` | `user_not_found · record_not_found` | The account setup is incomplete. | Contact support. |
| `409` | `idempotency_key_in_progress` | A request with this Idempotency-Key is still running. | Wait and retry with the same key. |
| `500` | `error_fetching_active_currencies` | The token catalogue could not be loaded. Nothing was processed. | Nothing was changed. Retry later. |
| `500` | `internal_error` | Unexpected failure; summary and results show what was created. | Resend only the items counted in summary.not_processed. |

#### Other responses

Responses that are not the happy path and still need handling in your integration.

##### 207 · Partial success

Returned when at least one item was created and at least one was refused; when every item is refused the same body comes with 400. Every entry in results carries its index, so you can match a failure back to the object you sent; a refused entry has error with code and message, and an item that failed validation lists the reason per field in error.details.fields. A reference_id already used by an earlier payout comes back with duplicate: true, the original payout's internal_reference, its current status (pending, completed, failed or refunded) and its creation time, and no second transfer is made. The same reference_id twice in one request is refused on the later index with duplicate_reference_id_in_request. Created entries carry the full fields shown in the 200 example (shortened here).

```json
{
  "message": "payment_processing_completed",
  "summary": {
    "total_requested": 5,
    "successful": 2,
    "failed": 3
  },
  "results": [
    {
      "index": 0,
      "reference_id": "order_12345",
      "internal_reference": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
      "status": "created"
    },
    {
      "index": 1,
      "reference_id": "order_12346",
      "error": {
        "code": "invalid_parameters",
        "message": "Invalid parameters provided",
        "details": {
          "fields": {
            "crypto_amount": "Crypto amount must be a positive decimal number"
          }
        }
      }
    },
    {
      "index": 2,
      "reference_id": "order_12347",
      "internal_reference": "3c9d1e7f-2a4b-4f6c-8e0d-b5a7c3f1e902",
      "status": "created"
    },
    {
      "index": 3,
      "reference_id": "order_11999",
      "internal_reference": "6e1b8f2d-4c7a-4d9e-b3f0-a2c5e8d1f764",
      "duplicate": true,
      "original_status": "pending",
      "original_created_at": "2026-01-16 10:00:00",
      "error": {
        "code": "duplicate_payment_reference",
        "message": "A payout with this reference_id already exists"
      }
    },
    {
      "index": 4,
      "error": {
        "code": "duplicate_reference_id_in_request",
        "message": "This reference_id appears more than once in the request"
      }
    }
  ]
}
```

##### 400 · Every item invalid

When every item fails validation nothing is processed and there are no results: the reasons come at the top level, in details.fields keyed by index.field. An entry that is not a JSON object is reported as index.item.

```json
{
  "error": {
    "code": "invalid_parameters",
    "message": "Invalid parameters provided",
    "details": {
      "fields": {
        "0.webhook_url": "Webhook URL is required",
        "1.crypto_currency": "Invalid crypto currency"
      }
    }
  }
}
```

##### 400 / 404 / 500 · Request-level errors

Nothing is processed. The codes are listed in the Errors table.

```json
{
  "error": {
    "code": "max_payouts_exceeded",
    "message": "Too many payouts in a single request"
  }
}
```

##### 409 · Idempotency-Key still running

The first request with this key has not finished yet. Wait and retry with the same key; do not send the batch again without it. After 15 minutes without an answer the key is freed and the next request with it is processed.

```json
{
  "error": {
    "code": "idempotency_key_in_progress",
    "message": "A request with this Idempotency-Key is still being processed"
  }
}
```

##### 400 · Not enough balance for the batch

Checked before anything is debited. Valid items of the same currency are summed using the amount selected by process_by (fees not included), and compared with the available balance after holds. Nothing is created. Fees are checked per item afterwards, so an item can still be refused with insufficient_balance.

```json
{
  "error": {
    "code": "insufficient_total_balance",
    "message": "Insufficient balance to cover all payouts",
    "details": {
      "crypto_currency": "USDT",
      "available": "150.00000000",
      "required": "420.500000000000000000"
    }
  }
}
```

##### 207 / 400 (item) · Item refusal codes

Returned on the refused entry; the rest of the batch continues and nothing is debited for it. Every code, and whether it carries reference_id, is in the Item refusal codes table.

```json
{
  "index": 2,
  "reference_id": "order_12347",
  "error": {
    "code": "limit_exceeded",
    "message": "Amount is outside the allowed limits"
  }
}
```

##### 207 / 400 (item) · Daily payout limit reached

Only when the key has a daily payout limit configured (in USD). Each payout counts its total debit converted to USD, and the limit covers the UTC calendar day (00:00 to 23:59 UTC), unlike the other dates of the API, which are in UTC−03:00. The item is refused on its index; the rest of the batch continues.

```json
{
  "index": 3,
  "reference_id": "order_12348",
  "error": {
    "code": "daily_payout_limit_exceeded",
    "message": "Daily payout limit for this API key exceeded"
  }
}
```

##### 207 / 400 (item) · Amount does not cover fees

Only with feetakenfromamount = 1: fees consume the whole amount, so the recipient would receive nothing. The item is refused on its index; the rest of the batch continues.

```json
{
  "index": 1,
  "reference_id": "order_12346",
  "error": {
    "code": "amount_does_not_cover_fees",
    "message": "Amount does not cover the fees"
  }
}
```

##### 207 / 400 (item) · Daily limit cannot be accounted

Only when the key has a daily limit configured and the spend record could not be written. The item is refused on its index; the rest of the batch continues. Safe to resend later with the same reference_id.

```json
{
  "index": 3,
  "reference_id": "order_12348",
  "error": {
    "code": "daily_payout_limit_unavailable",
    "message": "Daily payout limit could not be checked right now"
  }
}
```

##### 207 / 400 (item) · Price unavailable

Returned on the item when a conversion price is missing (fiat_amount conversion, or the network fee converted into the token), and also when the key has a daily limit but the token has no USD quote to count the payout. The rest of the batch continues.

```json
{
  "index": 2,
  "reference_id": "order_12347",
  "error": {
    "code": "price_unavailable",
    "message": "Price information temporarily unavailable"
  }
}
```

##### 500 · Interrupted mid-batch

An unexpected failure stops the loop, but the items already created are returned: summary and results stay at the top level next to error. not_processed tells you how many never got that far, so you can resend only those, and the uniqueness of reference_id still protects you if you resend more than you should. The Idempotency-Key is not stored for this response.

```json
{
  "error": {
    "code": "internal_error",
    "message": "An unexpected error occurred during payment processing"
  },
  "summary": {
    "total_requested": 50,
    "successful": 31,
    "failed": 1,
    "not_processed": 18
  },
  "results": [
    "..."
  ]
}
```

### List Payouts

`GET https://bridge.bifrostcrypto.com/v1/list-payout`

Required scope: `payout:read` (a new key with no list is read-only)

Lists the payouts created through the API (payouts made in the panel are not included), newest first (created_at descending), with filters and pagination.

Each payout carries the same fields as Check Specific Payout; crypto_amount is the net amount sent to the wallet and status is pending, completed, failed or refunded. summary counts pending, completed and failed payouts matching the same filters, across all pages. meta.filters echoes the filters applied, with status in its public value; dates are not echoed.

#### Headers

| Key | Value | Description |
| --- | --- | --- |
| `X-Bifrost-Invoke` | `{{api_key}}` | Private API key |

#### Query Parameters

| Parameter | Example | Required | Description |
| --- | --- | --- | --- |
| `page` | `1` | - | Page number, default 1. 0 or a non-numeric value is treated as 1; a negative number returns invalid_page_number. |
| `per_page` | `50` | - | Items per page, 1 to 100, default 50. 0 or a non-numeric value is treated as 50; above 100 or negative returns invalid_per_page_limit. |
| `status` | `pending` | - | pending, completed or failed (case-insensitive). Any other value returns invalid_status_filter. Refunded payouts are listed only without this filter. |
| `crypto_currency` | `USDT` | - | Filter by token symbol |
| `network` | `Ethereum` | - | Filter by a network name from the catalog, exact and case-sensitive (e.g. Ethereum, BinanceSmartChain, Tron). An unknown name returns invalid_network_filter. |
| `date_from` | `2026-01-01` | - | Start date (YYYY-MM-DD), inclusive from 00:00:00. An unreadable date returns invalid_date_from. Read in UTC−03:00. |
| `date_to` | `2026-01-31` | - | End date (YYYY-MM-DD), inclusive until 23:59:59. An unreadable date returns invalid_date_to. Read in UTC−03:00. |
| `reference_id` | `order_12345` | - | Filter by your reference_id (exact match) |

#### cURL Example

```bash
curl -X GET "https://bridge.bifrostcrypto.com/v1/list-payout?page=1&per_page=50&status=pending" \
  -H "X-Bifrost-Invoke: YOUR_API_KEY"
```

#### Response Example

```json
{
  "message": "payments_retrieved_success",
  "payments": [
    {
      "internal_reference": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
      "reference_id": "order_12345",
      "wallet": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0",
      "crypto_currency": "USDT",
      "fiat_currency": "USD",
      "crypto_amount": "100.500000000000000000",
      "fiat_amount": "100.50",
      "process_by": "crypto_amount",
      "percent_rate": "1.50",
      "network_fee": "0.00100000",
      "network": "Ethereum",
      "status": "pending",
      "txid": null,
      "webhook_url": "https://your-site.com/webhook",
      "feetakenfromamount": "0",
      "created_at": "2026-01-16 10:00:00",
      "updated_at": "2026-01-16 10:00:00"
    }
  ],
  "summary": {
    "total_pending": 1,
    "total_completed": 0,
    "total_failed": 0
  },
  "meta": {
    "current_page": 1,
    "per_page": 50,
    "total": 1,
    "total_pages": 1,
    "filters": {
      "status": "pending",
      "crypto_currency": "USDT"
    }
  }
}
```

#### Errors

Every error has the shape error.code, error.message and, when useful, error.details. Authentication, scope and rate-limit errors (401, 403, 429) apply to every endpoint and are described under Authentication.

| Status | Code | When | What to do |
| --- | --- | --- | --- |
| `400` | `invalid_page_number` | page is negative. | Fix the request before sending it again. |
| `400` | `invalid_per_page_limit` | per_page is negative or above 100. | Fix the request before sending it again. |
| `400` | `invalid_status_filter` | status is not one of the accepted values. | Fix the request before sending it again. |
| `400` | `invalid_network_filter` | network is not a known network name (case-sensitive). | Fix the request before sending it again. |
| `400` | `invalid_date_from · invalid_date_to` | A date could not be read. | Send YYYY-MM-DD. |
| `500` | `internal_error` | Unexpected failure. | Nothing was changed. Retry later. |

#### Other responses

Responses that are not the happy path and still need handling in your integration.

##### 400 · Invalid query parameter

A filter value was not accepted. The codes are listed in the Errors table and described on each parameter.

```json
{
  "error": {
    "code": "invalid_per_page_limit",
    "message": "Invalid per_page value"
  }
}
```

### Check Specific Payout

`GET https://bridge.bifrostcrypto.com/v1/check-payout/{internal_reference}`

Required scope: `payout:read` (a new key with no list is read-only)

Returns one payout created through the API, by the internal_reference returned when it was created.

status is pending, completed, failed or refunded, and txid is null while there is no transaction hash. crypto_amount is the net amount sent to the wallet. A payout made in the panel, or belonging to another account, returns 404.

#### Headers

| Key | Value | Description |
| --- | --- | --- |
| `X-Bifrost-Invoke` | `{{api_key}}` | Private API key |

#### Path Parameters

| Parameter | Example | Description |
| --- | --- | --- |
| `{internal_reference}` | `f47ac10b-58cc-4372-a567-0e02b2c3d479` | internal_reference of the payout (a UUID). Fewer than 10 or more than 756 characters returns invalid_reference_id. |

#### cURL Example

```bash
curl -X GET "https://bridge.bifrostcrypto.com/v1/check-payout/f47ac10b-58cc-4372-a567-0e02b2c3d479" \
  -H "X-Bifrost-Invoke: YOUR_API_KEY"
```

#### Response Example

```json
{
  "message": "payment_retrieved_success",
  "payment": {
    "internal_reference": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "reference_id": "order_12345",
    "wallet": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0",
    "crypto_currency": "USDT",
    "fiat_currency": "USD",
    "crypto_amount": "100.500000000000000000",
    "fiat_amount": "100.50",
    "process_by": "crypto_amount",
    "percent_rate": "1.50",
    "network_fee": "0.00100000",
    "network": "Ethereum",
    "status": "completed",
    "txid": "0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060",
    "webhook_url": "https://your-site.com/webhook",
    "feetakenfromamount": "0",
    "created_at": "2026-01-16 10:00:00",
    "updated_at": "2026-01-16 10:05:00"
  }
}
```

#### Errors

Every error has the shape error.code, error.message and, when useful, error.details. Authentication, scope and rate-limit errors (401, 403, 429) apply to every endpoint and are described under Authentication.

| Status | Code | When | What to do |
| --- | --- | --- | --- |
| `400` | `invalid_reference_id` | internal_reference is shorter than 10 or longer than 756 characters. | Fix the request before sending it again. |
| `404` | `record_not_found` | No payout created through the API with this internal_reference exists on your account. | Check the internal_reference returned on creation. Items created in the panel are not returned. |

#### Other responses

Responses that are not the happy path and still need handling in your integration.

##### 404 · Payout not found

No payout created through the API with this internal_reference exists on your account.

```json
{
  "error": {
    "code": "record_not_found",
    "message": "Record not found",
    "details": {
      "resource": "payment"
    }
  }
}
```

##### 400 · Invalid reference

The internal_reference is shorter than 10 or longer than 756 characters.

```json
{
  "error": {
    "code": "invalid_reference_id",
    "message": "Invalid reference ID provided"
  }
}
```

## API reference · Deposits Payin

Endpoints for managing deposits and receipts. POST /v1/payin accepts one object, or an array with a single item, and rejects a batch with 400 invalid_parameters (details.reason batch_not_allowed_use_single_object). List and check only return payins created through the API. Response status values are lowercase: pending, completed, failed, reversed (a completed deposit the network later undid; the credit was removed). A payin is marked completed once the amount received reaches minimum_payment percent of crypto_amount, so a completed payin can still be partially paid: before delivering, compare received_amount with crypto_amount (both in crypto_currency).

### Create Payin

`POST https://bridge.bifrostcrypto.com/v1/payin`

Required scope: `payin:write` (a new key with no list is read-only)

Creates a single deposit and answers 200 OK with a wallet address for the customer to send funds to.

The payin becomes completed when the amount received reaches minimum_payment percent of crypto_amount (minimum_payment is set per account and returned by check-payin and GET /v1/summary), which may be less than the full amount: confirm received_amount before delivering. The success body is one object under data, not a batch summary. crypto_amount in the response is cut (not rounded) to the token's decimals, with trailing zeros removed ("50.00" comes back as "50"). fiat_amount is the value you sent; when you did not send one it is crypto_amount × price with 8 decimals, or null if no price is available. The response has no expires_at: read it from check-payin. This endpoint has no Idempotency-Key: reference_id is what prevents a second payin. If a request times out, resend it with the same reference_id; a duplicate_reference_id answer means the first one was created, and GET /v1/list-payin?reference_id=… returns it. The error codes are listed under Errors.

#### Headers

| Key | Value | Description |
| --- | --- | --- |
| `X-Bifrost-Invoke` | `{{api_key}}` | Private API key |
| `Content-Type` | `application/json` | - |

#### Body Parameters

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `crypto_currency` | `string` | required | Token symbol, case-sensitive (USDT, not usdt). It must be enabled on your account and active for deposits on the chosen network; otherwise invalid_parameters (details.fields.crypto_currency), unsupported_token_or_deposits_disabled or token_network_mismatch. |
| `process_by` | `string` | required | Which amount drives the deposit: crypto_amount or fiat_amount. |
| `reference_id` | `string` | required | Your reference for this deposit, 1 to 255 characters, trimmed. Must be unique on your account; a repeat returns duplicate_reference_id. |
| `network` | `string` | required | One of: Bitcoin, Ethereum, BinanceSmartChain, Tron, Solana, Ripple, Cardano, Dogecoin, Polygon, Avalanche, ArbitrumOne, Celo, Optimism, Arbitrum, Cronos, Base, BitcoinCash, BitcoinSV, BitcoinGold, Monero, OpBnb, Xlayer, Litecoin, TON. Case-insensitive here (binancesmartchain works), and the aliases in Network aliases are also accepted (erc20, bsc, trc20...); the payin is stored and returned with the canonical name. A network without active deposits returns invalid_network_or_deposits_disabled. |
| `webhook_url` | `string` | required | Callback URL for status updates. Up to 756 characters, https://, no user:password in the URL, on a standard port, and the host must resolve to a public IPv4 address. A refused URL returns 400 invalid_parameters with details.field webhook_url and details.reason unsafe_url. |
| `fiat_currency` | `string` | - | Fiat code from the accepted fiats, case-insensitive. Required when process_by is fiat_amount (fiat_currency_required). When omitted, your account's preferred currency is used, then USD. |
| `crypto_amount` | `string` | - | Required when process_by is crypto_amount (crypto_amount_required). Greater than 0, digits on both sides of an optional dot (".5" and "5." are refused). Extra decimals beyond the token's precision are cut. Ignored and recalculated when process_by is fiat_amount. A JSON number is accepted; a string is recommended. |
| `fiat_amount` | `string` | - | Required when process_by is fiat_amount (fiat_amount_required); same format as crypto_amount. When process_by is crypto_amount and you also send fiat_amount, crypto_amount must be within ±30% of fiat_amount ÷ current price, or the request fails with crypto_amount_does_not_correspond_to_fiat_amount_provided (or 503 price_not_available_for_validation without a price). |
| `customer_info` | `string` | - | Your customer identifier. Up to 200 characters, no whitespace. |
| `expires_at` | `number` | - | Lifetime of the deposit in seconds, integer from 1 to 86400. Default 14400 (4 hours). The resulting date is returned as expires_at (Y-m-d H:i:s) by check-payin and list-payin. |
| `memo` | `string` | - | Required for tokens configured to need a memo, such as TON (memo_required otherwise). Numeric string of 8 to 15 digits; this format is checked whenever memo is sent, and the value is ignored on tokens that do not use one. |

#### Request Body

```json
{
  "crypto_currency": "USDT",
  "fiat_currency": "USD",
  "crypto_amount": "50.00",
  "process_by": "crypto_amount",
  "reference_id": "invoice_67890",
  "network": "BinanceSmartChain",
  "webhook_url": "https://your-site.com/webhook-deposit",
  "customer_info": "customer456",
  "expires_at": 14400
}
```

#### cURL Example

```bash
curl -X POST "https://bridge.bifrostcrypto.com/v1/payin" \
  -H "X-Bifrost-Invoke: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "crypto_currency": "USDT",
  "fiat_currency": "USD",
  "crypto_amount": "50.00",
  "process_by": "crypto_amount",
  "reference_id": "invoice_67890",
  "network": "BinanceSmartChain",
  "webhook_url": "https://your-site.com/webhook-deposit",
  "customer_info": "customer456",
  "expires_at": 14400
}'
```

#### Response Example

```json
{
  "message": "deposit_created_successfully",
  "data": {
    "reference_id": "invoice_67890",
    "internal_reference": "f531c319-6957-45c6-a154-d4944ca3eee3",
    "wallet": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0",
    "crypto_amount": "50",
    "fiat_amount": "50.00000000",
    "crypto_currency": "USDT",
    "fiat_currency": "USD",
    "network": "BinanceSmartChain",
    "status": "created"
  }
}
```

#### Network aliases

On create, network also accepts these aliases. Case, spaces, hyphens and underscores are ignored (ERC-20, erc_20 and erc20 are the same). Responses, webhooks and listings always return the canonical name. bnb means BinanceSmartChain (for opBNB send OpBnb) and arb means Arbitrum (for ArbitrumOne send ArbitrumOne).

| Network | Aliases |
| --- | --- |
| `Ethereum` | `eth, eth-mainnet, erc20` |
| `Tron` | `trx, trc20` |
| `BinanceSmartChain` | `bnb, bsc, bep20, bnbchain, bnb-smart-chain` |
| `Arbitrum` | `arb, arbitrum-evm` |
| `Polygon` | `matic, pol, polygon-pos` |
| `Optimism` | `op, op-mainnet` |
| `Avalanche` | `avax, avaxc, avalanche-c` |
| `Solana` | `sol` |
| `Bitcoin` | `btc` |
| `Litecoin` | `ltc` |
| `Monero` | `xmr` |
| `Xlayer` | `okb` |

#### Errors

Every error has the shape error.code, error.message and, when useful, error.details. Authentication, scope and rate-limit errors (401, 403, 429) apply to every endpoint and are described under Authentication.

| Status | Code | When | What to do |
| --- | --- | --- | --- |
| `400` | `invalid_parameters` | A field failed validation (details.fields), the body is empty (details.reason invalid_or_empty_data), a batch was sent (batch_not_allowed_use_single_object) or webhook_url is not allowed (details.reason unsafe_url). | Fix the request before sending it again. |
| `400` | `json_parse_exception` | The body is not valid JSON. | Fix the request before sending it again. |
| `400` | `duplicate_reference_id` | A payin with this reference_id already exists. | If it was a resend after a timeout, fetch it with GET /v1/list-payin?reference_id=… |
| `400` | `crypto_amount_required · fiat_amount_required · fiat_currency_required` | The amount or currency required by process_by is missing. | Fix the request before sending it again. |
| `400` | `memo_required` | The token needs a memo. | Fix the request before sending it again. |
| `400` | `invalid_network_or_deposits_disabled · unsupported_token_or_deposits_disabled · token_network_mismatch · token_not_supported_or_inactive` | The token or network does not accept deposits for your account. | Choose another token or network. |
| `400` | `amount_out_of_limits · value_below_minimum · value_above_maximum` | The amount is outside the token limits; details carries them. | Fix the request before sending it again. |
| `400` | `crypto_amount_does_not_correspond_to_fiat_amount_provided` | crypto_amount and fiat_amount differ by more than 30% at the current price. | Fix the request before sending it again. |
| `404` | `record_not_found` | The account setup is incomplete. | Contact support. |
| `500` | `internal_error · error_fetching_active_currencies · token_type_not_found · receipt_creation_failed · database_transaction_failed` | Failure on our side. | Retry later with the same reference_id. |
| `502` | `external_processor_error` | The deposit address could not be created right now. | Retry later with the same reference_id. |
| `503` | `no_processor_configured · no_wallet_available · price_not_available_for_conversion · price_not_available_for_validation · service_unavailable` | Temporarily unavailable. | Retry later with the same reference_id. |

#### Other responses

Responses that are not the happy path and still need handling in your integration.

##### 400 · Duplicate reference_id

A refused deposit answers with a single error object. There is no summary or results array on this endpoint. If this answer comes from resending after a timeout, the first request created the payin: fetch it with GET /v1/list-payin?reference_id=….

```json
{
  "error": {
    "code": "duplicate_reference_id",
    "message": "A payin with this reference_id already exists"
  }
}
```

##### 400 · Invalid parameters

Field validation failures are listed in details.fields. A batch of 2 or more items returns the same code with details.reason batch_not_allowed_use_single_object; an empty body with invalid_or_empty_data; an unsafe webhook_url with details.field webhook_url and details.reason unsafe_url.

```json
{
  "error": {
    "code": "invalid_parameters",
    "message": "Invalid parameters provided",
    "details": {
      "fields": {
        "crypto_currency": "The crypto_currency field must be one of: USDT,BTC,ETH."
      }
    }
  }
}
```

##### 400 · Amount outside the token limits

With process_by crypto_amount the code is amount_out_of_limits. With process_by fiat_amount it is value_below_minimum or value_above_maximum, with details reason, crypto_currency, fiat_currency, fiat_amount, crypto_amount, min_crypto, max_crypto, min_fiat and max_fiat.

```json
{
  "error": {
    "code": "amount_out_of_limits",
    "message": "Amount is outside the allowed limits",
    "details": {
      "message": "Amount below minimum limit",
      "crypto_amount": "0.5",
      "crypto_currency": "USDT",
      "min_allowed": "1.000000000000000000",
      "max_allowed": "unlimited",
      "process_by": "crypto_amount"
    }
  }
}
```

##### 503 · Temporarily unavailable

Our side could not serve the deposit right now. Nothing was created; retry later with the same reference_id. 500 and 502 follow the same rule. The codes are listed in the Errors table.

```json
{
  "error": {
    "code": "no_wallet_available",
    "message": "No deposit address is available right now"
  }
}
```

### List Payins

`GET https://bridge.bifrostcrypto.com/v1/list-payin`

Required scope: `payin:read` (a new key with no list is read-only)

Lists deposits created through the API (not those created in the panel), newest first (created_at descending), with filters and pagination.

Answers 200 OK. summary counts pending, completed, failed and reversed deposits matching the same filters. minimum_payment is the percentage of crypto_amount that marks the payin completed; received_amount is what actually arrived, and payment_status says partial, exact or overpaid (null before any payment). A reversed deposit carries reversal with reversed_at, partial (true when part of the payin is still credited) and amounts (balance key and amount removed); reversal is null otherwise. meta.filters echoes the filters applied, with status in its public value. Fields that need explanation are described below.

#### Headers

| Key | Value | Description |
| --- | --- | --- |
| `X-Bifrost-Invoke` | `{{api_key}}` | Private API key |

#### Query Parameters

| Parameter | Example | Required | Description |
| --- | --- | --- | --- |
| `page` | `1` | - | Page number (default: 1). 0 or a non-numeric value falls back to 1; a negative value returns invalid_page_number. |
| `per_page` | `50` | - | Items per page, 1-100 (default: 50). 0 or a non-numeric value falls back to 50; above 100 or negative returns invalid_per_page_limit. |
| `status` | `completed` | - | Filter by status: pending, completed, failed or reversed (any case). completed excludes reversed deposits. Anything else returns invalid_status_filter. |
| `crypto_currency` | `USDT` | - | Filter by crypto currency symbol (exact match) |
| `network` | `BinanceSmartChain` | - | Filter by a network name from the catalog, exact and case-sensitive (BinanceSmartChain, not binancesmartchain). An unknown name returns invalid_network_filter. |
| `date_from` | `2026-01-01` | - | Start date (YYYY-MM-DD), inclusive from 00:00:00. An unparseable date returns invalid_date_from. No maximum range. Read in UTC−03:00. |
| `date_to` | `2026-01-31` | - | End date (YYYY-MM-DD), inclusive until 23:59:59. An unparseable date returns invalid_date_to. Read in UTC−03:00. |
| `reference_id` | `invoice_67890` | - | Filter by reference_id (exact match) |

#### cURL Example

```bash
curl -X GET "https://bridge.bifrostcrypto.com/v1/list-payin?page=1&per_page=50&status=completed" \
  -H "X-Bifrost-Invoke: YOUR_API_KEY"
```

#### Response Example

```json
{
  "message": "deposits_retrieved_success",
  "deposits": [
    {
      "internal_reference": "f531c319-6957-45c6-a154-d4944ca3eee3",
      "reference_id": "invoice_67890",
      "wallet": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0",
      "crypto_currency": "USDT",
      "invoice_currency": "USDT",
      "invoice_amount": "50.00",
      "fiat_currency": "USD",
      "crypto_amount": "50.00",
      "received_amount": "0.00000000",
      "fiat_amount": "50.00",
      "process_by": "crypto_amount",
      "percent_rate": "2.50",
      "network_fee": "0.00000000",
      "minimum_payment": "10.00",
      "network": "BinanceSmartChain",
      "status": "pending",
      "payment_status": null,
      "txid": null,
      "payments": [],
      "webhook_url": "https://your-site.com/webhook-deposit",
      "is_wallet_fixed": false,
      "reversal": null,
      "created_at": "2026-01-16 10:00:00",
      "updated_at": "2026-01-16 10:00:00",
      "expires_at": "2026-01-16 14:00:00"
    }
  ],
  "summary": {
    "total_pending": 1,
    "total_completed": 0,
    "total_failed": 0,
    "total_reversed": 0
  },
  "meta": {
    "current_page": 1,
    "per_page": 50,
    "total": 1,
    "total_pages": 1,
    "filters": {
      "status": "pending",
      "crypto_currency": "USDT"
    }
  }
}
```

#### Deposit fields

Fields of each entry in deposits that are not obvious from their name.

| Field | Meaning |
| --- | --- |
| `invoice_currency · invoice_amount` | Token and amount of the payin as created (the same as crypto_currency and crypto_amount). |
| `received_amount` | Amount actually received, converted to crypto_currency when the customer paid with another token. |
| `minimum_payment` | Percentage of crypto_amount that marks the payin completed. |
| `payment_status` | partial, exact or overpaid, comparing received_amount with crypto_amount; null before any payment. |
| `payments[]` | One entry per transfer received: token, amount, network, txid and timestamp. converted_to (token and amount) appears when the transfer was in a token other than the invoice token. |
| `txid` | Text holding a JSON list of transaction hashes, e.g. "[\"0xabc...\"]". Decode it as JSON; null before any payment. |
| `is_wallet_fixed` | true when the payin came from a fixed address. |
| `reversal` | null, or reversed_at, partial and amounts when the network undid a credited payment. |

#### Errors

Every error has the shape error.code, error.message and, when useful, error.details. Authentication, scope and rate-limit errors (401, 403, 429) apply to every endpoint and are described under Authentication.

| Status | Code | When | What to do |
| --- | --- | --- | --- |
| `400` | `invalid_page_number` | page is negative. | Fix the request before sending it again. |
| `400` | `invalid_per_page_limit` | per_page is negative or above 100. | Fix the request before sending it again. |
| `400` | `invalid_status_filter` | status is not one of the accepted values. | Fix the request before sending it again. |
| `400` | `invalid_network_filter` | network is not a known network name (case-sensitive). | Fix the request before sending it again. |
| `400` | `invalid_date_from · invalid_date_to` | A date could not be read. | Send YYYY-MM-DD. |
| `500` | `internal_error` | Unexpected failure. | Nothing was changed. Retry later. |

#### Other responses

Responses that are not the happy path and still need handling in your integration.

##### 400 · Invalid filter

```json
{
  "error": {
    "code": "invalid_network_filter",
    "message": "Invalid network filter"
  }
}
```

### Check Specific Payin

`GET https://bridge.bifrostcrypto.com/v1/check-payin/{internal_reference}`

Required scope: `payin:read` (a new key with no list is read-only)

Checks the status of a specific deposit by internal_reference and answers 200 OK.

Only deposits created through the API are found. status is pending, completed, failed or reversed. completed means received_amount reached minimum_payment percent of crypto_amount, not necessarily the full amount: compare received_amount with crypto_amount before delivering. reversed means the network undid a payment that had been credited; reversal then gives reversed_at, partial and the amounts removed per balance key (null when there was no reversal). Unlike the list, this object does not include payments, payment_status, invoice_currency or invoice_amount. Errors: 400 invalid_reference_id when internal_reference is shorter than 10 or longer than 756 characters; 404 record_not_found (details.resource receipt) when it does not exist on your account.

#### Headers

| Key | Value | Description |
| --- | --- | --- |
| `X-Bifrost-Invoke` | `{{api_key}}` | Private API key |

#### Path Parameters

| Parameter | Example | Description |
| --- | --- | --- |
| `{internal_reference}` | `f531c319-6957-45c6-a154-d4944ca3eee3` | Internal deposit reference |

#### cURL Example

```bash
curl -X GET "https://bridge.bifrostcrypto.com/v1/check-payin/f531c319-6957-45c6-a154-d4944ca3eee3" \
  -H "X-Bifrost-Invoke: YOUR_API_KEY"
```

#### Response Example

```json
{
  "message": "receipt_retrieved_success",
  "receipt": {
    "internal_reference": "f531c319-6957-45c6-a154-d4944ca3eee3",
    "reference_id": "invoice_67890",
    "wallet": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0",
    "crypto_currency": "USDT",
    "fiat_currency": "USD",
    "crypto_amount": "50.00000000",
    "received_amount": "0.00000000",
    "fiat_amount": "50.00",
    "process_by": "crypto_amount",
    "percent_rate": "2.50",
    "network_fee": "0.00000000",
    "minimum_payment": "10.00",
    "network": "BinanceSmartChain",
    "status": "pending",
    "txid": null,
    "webhook_url": "https://your-site.com/webhook-deposit",
    "is_wallet_fixed": false,
    "reversal": null,
    "created_at": "2026-01-16 10:00:00",
    "updated_at": "2026-01-16 10:00:00",
    "expires_at": "2026-01-16 14:00:00"
  }
}
```

#### Errors

Every error has the shape error.code, error.message and, when useful, error.details. Authentication, scope and rate-limit errors (401, 403, 429) apply to every endpoint and are described under Authentication.

| Status | Code | When | What to do |
| --- | --- | --- | --- |
| `400` | `invalid_reference_id` | internal_reference is shorter than 10 or longer than 756 characters. | Fix the request before sending it again. |
| `404` | `record_not_found` | No payin created through the API with this internal_reference exists on your account. | Check the internal_reference returned on creation. Items created in the panel are not returned. |

#### Other responses

Responses that are not the happy path and still need handling in your integration.

##### 200 · Partially paid

With minimum_payment 10, a payin of 50 USDT is completed once 5 USDT arrive. status alone does not mean the full amount was paid: compare received_amount with crypto_amount.

```json
{
  "message": "receipt_retrieved_success",
  "receipt": {
    "internal_reference": "f531c319-6957-45c6-a154-d4944ca3eee3",
    "reference_id": "invoice_67890",
    "crypto_currency": "USDT",
    "crypto_amount": "50.00000000",
    "received_amount": "20.00000000",
    "minimum_payment": "10.00",
    "status": "completed",
    "reversal": null,
    "...": "..."
  }
}
```

##### 200 · Reversed deposit

The network undid a payment after it was credited and the amount was removed from your balance. The webhook payment_reversed is sent at the same time.

```json
{
  "message": "receipt_retrieved_success",
  "receipt": {
    "internal_reference": "f531c319-6957-45c6-a154-d4944ca3eee3",
    "reference_id": "invoice_67890",
    "status": "reversed",
    "reversal": {
      "reversed_at": "2026-01-16 10:12:40",
      "partial": false,
      "amounts": [
        {
          "balance": "balance_tether",
          "amount": "48.75000000"
        }
      ]
    },
    "...": "..."
  }
}
```

##### 404 · Payin not found

```json
{
  "error": {
    "code": "record_not_found",
    "message": "Record not found",
    "details": {
      "resource": "receipt"
    }
  }
}
```

## API reference · Queries

Endpoints for querying balances, statements, and account information.

### Get Balances

`GET https://bridge.bifrostcrypto.com/v1/balances`

Required scope: `balances:read` (a new key with no list is read-only)

Returns gross balances plus amounts held by deposits that are not final yet, with 200 OK.

balances has one key per supported currency (balance_<name>). Which key holds each token is returned as balance_key by GET /v1/summary/tokens: there is one balance per token, shared by every network, so USDT received on any network adds up in the same key. each a decimal string with 8 places; an account that never moved funds gets the same keys at "0.00000000". The key set grows when a currency is added, so iterate the keys instead of hard-coding them. available and locked are always objects and only include currencies that currently have a hold; when nothing is held both are {} and the spendable amount is balances. available is balances minus locked, never below zero. A payout is checked against available when a hold exists.

#### Headers

| Key | Value | Description |
| --- | --- | --- |
| `X-Bifrost-Invoke` | `{{api_key}}` | Private API key |

#### cURL Example

```bash
curl -X GET "https://bridge.bifrostcrypto.com/v1/balances" \
  -H "X-Bifrost-Invoke: YOUR_API_KEY"
```

#### Response Example

```json
{
  "message": "balances_retrieved_success",
  "balances": {
    "balance_bitcoin": "0.00000000",
    "balance_avalanche": "5.00000000",
    "balance_ethereum": "0.50000000",
    "balance_tether": "1000.00000000",
    "balance_usdcoin": "500.00000000",
    "balance_brz": "0.00000000",
    "balance_binancecoin": "2.00000000",
    "balance_cardano": "0.00000000",
    "balance_solana": "10.00000000",
    "balance_polygon": "100.00000000",
    "balance_tron": "50.00000000",
    "balance_brl1": "0.00000000",
    "balance_ethenausde": "0.00000000",
    "balance_dai": "0.00000000",
    "balance_paypalusd": "0.00000000",
    "balance_usdd": "0.00000000",
    "balance_eurc": "0.00000000",
    "balance_trueusd": "0.00000000",
    "balance_usd1": "0.00000000",
    "balance_tethergold": "0.00000000",
    "balance_paxgold": "0.00000000",
    "balance_pleasurecoin": "0.00000000",
    "balance_dolz": "0.00000000",
    "balance_floki": "0.00000000",
    "balance_shibainu": "0.00000000",
    "balance_pepe": "0.00000000",
    "balance_dogecoin": "0.00000000",
    "balance_ton": "0.00000000",
    "balance_monero": "0.00000000",
    "balance_litecoin": "0.00000000",
    "balance_cronos": "0.00000000",
    "balance_okb": "0.00000000",
    "balance_usdg": "0.00000000"
  },
  "available": {
    "balance_tether": "900.00000000"
  },
  "locked": {
    "balance_tether": "100.00000000"
  }
}
```

#### Errors

Every error has the shape error.code, error.message and, when useful, error.details. Authentication, scope and rate-limit errors (401, 403, 429) apply to every endpoint and are described under Authentication.

| Status | Code | When | What to do |
| --- | --- | --- | --- |
| `400` | `invalid_ip` | The client IP address could not be determined. | Contact support. |

### List Statements

`GET https://bridge.bifrostcrypto.com/v1/statements`

Required scope: `statements:read` (a new key with no list is read-only)

Lists the balance movements caused by API operations (payins, payouts, their fees, payout refunds and deposit reversals), newest first.

Movements made only in the panel are not returned. Without dates, the last 30 days are returned; the widest range accepted is 90 days (inclusive). filters echoes the window actually used and adds date_range_defaulted: true when you did not send both dates. amount is a string with 8 decimals. origin is null for manual operations; on incoming lines only origin.type is kept (reference, sequence and total are null). Data can be up to 5 minutes old.

#### Headers

| Key | Value | Description |
| --- | --- | --- |
| `X-Bifrost-Invoke` | `{{api_key}}` | Private API key |

#### Query Parameters

| Parameter | Example | Required | Description |
| --- | --- | --- | --- |
| `page` | `1` | - | Page number (default: 1) |
| `per_page` | `50` | - | Items per page (default: 50, max: 50). Above 50 the request is refused with invalid_pagination; it is not clamped. |
| `direction` | `out` | - | Direction: in (incoming) or out (outgoing). Lowercase, case-sensitive. |
| `type` | `WITHDRAWAL` | - | Type: DEPOSIT (payin credit), WITHDRAWAL (payout debit), RATE (percentage fee), FEE (network fee), WITHDRAWAL_REFUND (amount of a failed payout returned to your balance), DEPOSIT_REVERSAL (credit removed after the network undid a deposit). Uppercase, case-sensitive. Any other value returns 400 invalid_parameters. |
| `balance` | `balance_tether` | - | Filter by balance. Accepts any key returned by Get Balances (the balance_key of each token in GET /v1/summary/tokens, e.g. balance_tether for USDT); exact, case-sensitive match. Anything else is refused with 400 invalid_parameters (details.field = "balance"). |
| `date_from` | `2026-01-01` | - | Start date (YYYY-MM-DD, inclusive). Omitted, it is 29 days before date_to (a 30-day inclusive window). Read in UTC−03:00. |
| `date_to` | `2026-01-31` | - | End date (YYYY-MM-DD, inclusive). Omitted, it is today when date_from is also omitted, otherwise date_from + 29 days. Read in UTC−03:00. |
| `reference_id` | `order_12345` | - | Exact reference ID (max 255 characters) |
| `amount_from` | `10.00` | - | Minimum amount (numeric, >= 0) |
| `amount_to` | `1000.00` | - | Maximum amount (numeric, >= 0, not lower than amount_from) |
| `origin_type` | `recurring_transfer` | - | Filter by the origin of the statement line. recurring_transfer selects lines created by a recurring transfer. |

#### cURL Example

```bash
curl -X GET "https://bridge.bifrostcrypto.com/v1/statements?page=1&per_page=50&direction=out" \
  -H "X-Bifrost-Invoke: YOUR_API_KEY"
```

#### Response Example

```json
{
  "message": "statements_retrieved_success",
  "statements": [
    {
      "direction": "out",
      "type": "WITHDRAWAL",
      "amount": "150.50000000",
      "balance": "balance_tether",
      "internal_reference": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
      "origin": {
        "type": "recurring_transfer",
        "reference": "RT-1a2b3c4d",
        "sequence": 3,
        "total": 12
      },
      "created_at": "2026-01-20 14:32:10",
      "updated_at": "2026-01-20 14:32:10",
      "reference_id": "order_12345"
    },
    {
      "direction": "in",
      "type": "DEPOSIT",
      "amount": "1000.00000000",
      "balance": "balance_tether",
      "internal_reference": "f531c319-6957-45c6-a154-d4944ca3eee3",
      "origin": null,
      "created_at": "2026-01-18 09:05:44",
      "updated_at": "2026-01-18 09:05:44",
      "reference_id": "invoice_998"
    }
  ],
  "filters": {
    "balance": "balance_tether",
    "date_from": "2026-01-01",
    "date_to": "2026-01-31"
  },
  "meta": {
    "current_page": 1,
    "per_page": 50,
    "total": 2,
    "total_pages": 1
  }
}
```

#### Errors

Every error has the shape error.code, error.message and, when useful, error.details. Authentication, scope and rate-limit errors (401, 403, 429) apply to every endpoint and are described under Authentication.

| Status | Code | When | What to do |
| --- | --- | --- | --- |
| `400` | `invalid_pagination` | page below 1 or per_page above 50. | Fix the request before sending it again. |
| `400` | `invalid_parameters` | direction, type, balance or origin_type is not an accepted value; details.field names it. | Fix the request before sending it again. |
| `400` | `invalid_format` | A date is not YYYY-MM-DD; details.field names it. | Fix the request before sending it again. |
| `400` | `invalid_date_range` | date_from is after date_to. | Fix the request before sending it again. |
| `400` | `date_range_too_wide` | The window is longer than details.max_days (90). | Query the period in smaller windows. |
| `400` | `invalid_amount` | amount_from or amount_to is not a number ≥ 0, or amount_from is above amount_to. | Fix the request before sending it again. |
| `400` | `invalid_reference_id` | reference_id is longer than 255 characters. | Fix the request before sending it again. |
| `500` | `database_error` | The query failed. | Nothing was changed. Retry later. |

#### Other responses

Responses that are not the happy path and still need handling in your integration.

##### 400 · Date range too wide

Ask for a window longer than max_days and the request is refused rather than served slowly. Page through the period in chunks.

```json
{
  "error": {
    "code": "date_range_too_wide",
    "message": "The requested date range exceeds the maximum allowed",
    "details": {
      "max_days": 90
    }
  }
}
```

##### 400 · Invalid filter

Other 400 codes: invalid_pagination (page below 1 or per_page above 50), invalid_parameters (direction, type, balance or origin_type outside the accepted values; details.field names it), invalid_format (date not in YYYY-MM-DD; details.field), invalid_date_range (date_from after date_to), invalid_amount (amount_from/amount_to not numeric or negative, details.field; or amount_from above amount_to, details.reason = invalid_range), invalid_reference_id (reference_id longer than 255). A database failure answers 500 database_error.

```json
{
  "error": {
    "code": "invalid_parameters",
    "message": "Invalid parameters provided",
    "details": {
      "field": "balance"
    }
  }
}
```

### Statements Summary

`GET https://bridge.bifrostcrypto.com/v1/statements/summary`

Required scope: `statements:read` (a new key with no list is read-only)

Returns counts and aggregated amounts of the same movements List Statements returns, grouped by balance (currency).

Only balances, directions and types with records in the period are included in the response. Without dates, the last 30 days are summarised; the widest range accepted is 90 days (inclusive). total_amount is a string with 8 decimals. With no records in the period, by_balance is an empty array ([]), not an object. Data can be up to 5 minutes old.

#### Headers

| Key | Value | Description |
| --- | --- | --- |
| `X-Bifrost-Invoke` | `{{api_key}}` | Private API key |

#### Query Parameters

| Parameter | Example | Required | Description |
| --- | --- | --- | --- |
| `date_from` | `2026-01-01` | - | Start date of the period (inclusive). Format: YYYY-MM-DD. Omitted, it is 29 days before date_to (a 30-day inclusive window). Read in UTC−03:00. |
| `date_to` | `2026-03-31` | - | End date of the period (inclusive). Format: YYYY-MM-DD. Omitted, it is today when date_from is also omitted, otherwise date_from + 29 days. Read in UTC−03:00. |

#### cURL Example

```bash
curl -X GET "https://bridge.bifrostcrypto.com/v1/statements/summary?date_from=2026-01-01&date_to=2026-03-31" \
  -H "X-Bifrost-Invoke: YOUR_API_KEY"
```

#### Response Example

```json
{
  "message": "statement_summary_retrieved_success",
  "filters": {
    "date_from": "2026-01-01",
    "date_to": "2026-03-31"
  },
  "summary": {
    "by_balance": {
      "balance_bitcoin": {
        "total_count": 16,
        "by_direction": {
          "in": {
            "count": 12,
            "total_amount": "0.05000000"
          },
          "out": {
            "count": 4,
            "total_amount": "0.02000000"
          }
        },
        "by_type": {
          "DEPOSIT": {
            "count": 10,
            "total_amount": "0.04000000"
          },
          "WITHDRAWAL": {
            "count": 4,
            "total_amount": "0.02000000"
          },
          "FEE": {
            "count": 1,
            "total_amount": "0.00100000"
          },
          "RATE": {
            "count": 1,
            "total_amount": "0.00050000"
          }
        }
      },
      "balance_ethereum": {
        "total_count": 8,
        "by_direction": {
          "in": {
            "count": 5,
            "total_amount": "1.20000000"
          },
          "out": {
            "count": 3,
            "total_amount": "0.50000000"
          }
        },
        "by_type": {
          "DEPOSIT": {
            "count": 5,
            "total_amount": "1.20000000"
          },
          "WITHDRAWAL": {
            "count": 3,
            "total_amount": "0.50000000"
          }
        }
      }
    },
    "totals": {
      "total_statements": 24,
      "balances_used": [
        "balance_bitcoin",
        "balance_ethereum"
      ]
    }
  }
}
```

#### Errors

Every error has the shape error.code, error.message and, when useful, error.details. Authentication, scope and rate-limit errors (401, 403, 429) apply to every endpoint and are described under Authentication.

| Status | Code | When | What to do |
| --- | --- | --- | --- |
| `400` | `invalid_format` | A date is not YYYY-MM-DD; details.field names it. | Fix the request before sending it again. |
| `400` | `invalid_date_range` | date_from is after date_to. | Fix the request before sending it again. |
| `400` | `date_range_too_wide` | The window is longer than details.max_days (90). | Query the period in smaller windows. |
| `500` | `database_error` | The query failed. | Nothing was changed. Retry later. |

#### Other responses

Responses that are not the happy path and still need handling in your integration.

##### 200 · Called without dates

filters always reports the window that was actually used, and date_range_defaulted marks that you did not choose it. Read those fields instead of assuming the period covers your whole history.

```json
{
  "message": "statement_summary_retrieved_success",
  "filters": {
    "date_from": "2026-08-13",
    "date_to": "2026-09-11",
    "date_range_defaulted": true
  },
  "summary": {
    "...": "..."
  }
}
```

##### 400 · Date range too wide

Ask for a window longer than max_days and the request is refused rather than served slowly. Other 400 codes: invalid_format (date not in YYYY-MM-DD; details.field) and invalid_date_range (date_from after date_to). A database failure answers 500 database_error.

```json
{
  "error": {
    "code": "date_range_too_wide",
    "message": "The requested date range exceeds the maximum allowed",
    "details": {
      "max_days": 90
    }
  }
}
```

### Complete Summary

`GET https://bridge.bifrostcrypto.com/v1/summary`

Required scope: `statements:read` (a new key with no list is read-only)

Returns complete summary including accepted tokens and their configurations, accepted fiats, user information (fees, settings), and user-specific network rates.

user_info.minimum_payment is the percentage of a payin's crypto_amount that must arrive for it to be marked completed (100 = the full amount, also used when the account has no value set). Rates and limits are numeric strings without trailing zeros ("1", "2.5"). network_rates is keyed by network and only lists networks with an override; its user_pays_* flags are null when the network inherits the account setting. With no overrides, network_rates is an empty array ([]), not an object. Data can be up to 5 minutes old. Rates are explained below. accepted_tokens follows the same rules as Accepted Tokens: This is the platform catalogue of active tokens, one entry per token and network. It is not filtered by your account and does not say whether a token accepts deposits, payouts or both on that network. Availability is checked when you create the operation: a payin needs the token enabled on your account and deposits active on that network (otherwise invalid_parameters on crypto_currency, unsupported_token_or_deposits_disabled or token_network_mismatch); a payout needs withdrawals active on that network (otherwise invalid_parameters, unsupported_token_or_withdrawals_disabled or token_network_mismatch). balance_key is the key of the token in GET /v1/balances and in the balance filter of List Statements.

#### Headers

| Key | Value | Description |
| --- | --- | --- |
| `X-Bifrost-Invoke` | `{{api_key}}` | Private API key |

#### cURL Example

```bash
curl -X GET "https://bridge.bifrostcrypto.com/v1/summary" \
  -H "X-Bifrost-Invoke: YOUR_API_KEY"
```

#### Response Example

```json
{
  "message": "summary_retrieved_success",
  "data": {
    "accepted_tokens": [
      {
        "symbol": "USDT",
        "contract_address": "0xdac17f958d2ee523a2206206994597c13d831ec7",
        "network": "Ethereum",
        "variant": null,
        "balance_key": "balance_tether",
        "min_receipt": "10",
        "max_receipt": "unlimited",
        "min_withdrawal": "20",
        "max_withdrawal": "50000",
        "network_fee": "0.001"
      }
    ],
    "accepted_fiats": [
      {
        "code": "USD",
        "name": "US Dollar",
        "symbol": "$"
      }
    ],
    "user_info": {
      "minimum_payment": "10",
      "receipt_rate": "2.5",
      "payment_rate": "1.5",
      "deposit_rate": "2.5",
      "withdrawal_rate": "1.5",
      "user_pays_payment_network_fee": true,
      "user_pays_receipt_network_fee": false,
      "user_pays_deposit_network_fee": false,
      "user_pays_withdrawal_network_fee": true
    },
    "network_rates": {
      "Ethereum": {
        "payment_rate": "1",
        "receipt_rate": "2",
        "deposit_rate": "2",
        "withdrawal_rate": "1",
        "user_pays_payment_network_fee": true,
        "user_pays_receipt_network_fee": null,
        "user_pays_deposit_network_fee": null,
        "user_pays_withdrawal_network_fee": null
      }
    },
    "summary_stats": {
      "total_tokens": 15,
      "total_fiats": 5,
      "total_network_overrides": 1,
      "generated_at": "2026-01-16 10:00:00"
    }
  }
}
```

#### Rates

Percentages charged on each operation. The rate applied to a given payin or payout is returned in its percent_rate field. network_rates overrides these values for the networks it lists.

| Field | Applies to |
| --- | --- |
| `payment_rate` | Payouts created through the API (POST /v1/payout). |
| `receipt_rate` | Payins created through the API (POST /v1/payin) and deposits to fixed addresses created through the API. |
| `withdrawal_rate` | Withdrawals made in the panel. |
| `deposit_rate` | Deposits received through the panel, including fixed addresses created in the panel. |
| `user_pays_payment_network_fee · user_pays_receipt_network_fee` | true when your account pays the network fee of API payouts / API payins. |
| `user_pays_withdrawal_network_fee · user_pays_deposit_network_fee` | The same for panel withdrawals / panel deposits. |
| `minimum_payment` | Percentage of a payin's crypto_amount that must arrive for it to be marked completed (100 = the full amount). |

#### Errors

Every error has the shape error.code, error.message and, when useful, error.details. Authentication, scope and rate-limit errors (401, 403, 429) apply to every endpoint and are described under Authentication.

| Status | Code | When | What to do |
| --- | --- | --- | --- |
| `404` | `record_not_found` | The account setup is incomplete. | Contact support. |
| `500` | `internal_error` | Unexpected failure. | Nothing was changed. Retry later. |
| `503` | `service_unavailable` | The token or fiat catalogue could not be loaded. | Nothing was changed. Retry later. |

#### Other responses

Responses that are not the happy path and still need handling in your integration.

##### 503 · Token or fiat catalogue unavailable

The accepted tokens or fiats could not be loaded. Retry later. A missing account record answers 404 record_not_found (details.resource = user_info); an unexpected failure answers 500 internal_error.

```json
{
  "error": {
    "code": "service_unavailable",
    "message": "Service temporarily unavailable"
  }
}
```

### Accepted Tokens

`GET https://bridge.bifrostcrypto.com/v1/summary/tokens`

Required scope: `prices:read` or `statements:read` (any one of them is enough)

Returns only the list of accepted tokens with their configurations.

This is the platform catalogue of active tokens, one entry per token and network. It is not filtered by your account and does not say whether a token accepts deposits, payouts or both on that network. Availability is checked when you create the operation: a payin needs the token enabled on your account and deposits active on that network (otherwise invalid_parameters on crypto_currency, unsupported_token_or_deposits_disabled or token_network_mismatch); a payout needs withdrawals active on that network (otherwise invalid_parameters, unsupported_token_or_withdrawals_disabled or token_network_mismatch). balance_key is the key of the token in GET /v1/balances and in the balance filter of List Statements. variant is the on-chain contract variant (lowercase) or null. Limits and network_fee are numeric strings without trailing zeros; max_receipt and max_withdrawal mean no ceiling when they are "unlimited", "0" or null. If the catalogue cannot be loaded, the answer is 503 service_unavailable; an unexpected failure is 500 internal_error.

#### Headers

| Key | Value | Description |
| --- | --- | --- |
| `X-Bifrost-Invoke` | `{{api_key}}` | Private API key |

#### cURL Example

```bash
curl -X GET "https://bridge.bifrostcrypto.com/v1/summary/tokens" \
  -H "X-Bifrost-Invoke: YOUR_API_KEY"
```

#### Response Example

```json
{
  "message": "tokens_retrieved_success",
  "tokens": [
    {
      "symbol": "USDT",
      "contract_address": "0xdac17f958d2ee523a2206206994597c13d831ec7",
      "network": "Ethereum",
      "variant": null,
      "balance_key": "balance_tether",
      "min_receipt": "10",
      "max_receipt": "unlimited",
      "min_withdrawal": "20",
      "max_withdrawal": "50000",
      "network_fee": "0.001"
    }
  ],
  "meta": {
    "total": 15,
    "generated_at": "2026-01-16 10:00:00"
  }
}
```

#### Errors

Every error has the shape error.code, error.message and, when useful, error.details. Authentication, scope and rate-limit errors (401, 403, 429) apply to every endpoint and are described under Authentication.

| Status | Code | When | What to do |
| --- | --- | --- | --- |
| `500` | `internal_error` | Unexpected failure. | Nothing was changed. Retry later. |
| `503` | `service_unavailable` | The token catalogue could not be loaded. | Nothing was changed. Retry later. |

### Accepted Fiats

`GET https://bridge.bifrostcrypto.com/v1/summary/fiats`

Required scope: `prices:read` or `statements:read` (any one of them is enough)

Returns only the list of accepted fiat currencies.

If the catalogue cannot be loaded, the answer is 503 service_unavailable; an unexpected failure is 500 internal_error.

#### Headers

| Key | Value | Description |
| --- | --- | --- |
| `X-Bifrost-Invoke` | `{{api_key}}` | Private API key |

#### cURL Example

```bash
curl -X GET "https://bridge.bifrostcrypto.com/v1/summary/fiats" \
  -H "X-Bifrost-Invoke: YOUR_API_KEY"
```

#### Response Example

```json
{
  "message": "fiats_retrieved_success",
  "fiats": [
    {
      "code": "USD",
      "name": "US Dollar",
      "symbol": "$"
    },
    {
      "code": "EUR",
      "name": "Euro",
      "symbol": "€"
    },
    {
      "code": "BRL",
      "name": "Brazilian Real",
      "symbol": "R$"
    }
  ],
  "meta": {
    "total": 5,
    "generated_at": "2026-01-16 10:00:00"
  }
}
```

#### Errors

Every error has the shape error.code, error.message and, when useful, error.details. Authentication, scope and rate-limit errors (401, 403, 429) apply to every endpoint and are described under Authentication.

| Status | Code | When | What to do |
| --- | --- | --- | --- |
| `500` | `internal_error` | Unexpected failure. | Nothing was changed. Retry later. |
| `503` | `service_unavailable` | The fiat catalogue could not be loaded. | Nothing was changed. Retry later. |

## API reference · Prices

Endpoints for querying prices and exchange rates between currencies. Prices are strings in pt-BR number format ("65.432,10").

### Get Price

`GET https://bridge.bifrostcrypto.com/v1/get-price`

Required scope: `prices:read` (a new key with no list is read-only)

Gets the conversion price between two currencies (crypto or fiat).

Conversion types: same_currency, crypto_to_fiat, fiat, crypto_to_crypto, reversed_* (the inverse of a stored quote, e.g. reversed_crypto_to_fiat for USD to USDT). price is always a string in pt-BR number format: comma as decimal separator and dot as thousands separator. Decimals: same_currency is always "1,00"; fiat has 6 decimals; crypto_to_fiat, crypto_to_crypto and reversed_* have 2 decimals when the value is 1 or more ("65.432,10") and 8 significant digits below 1 ("0,000012340000" for a token worth 0.00001234), so low-value tokens never read as zero. Do not parseFloat it directly ("65.432,10" would read as 65.432): remove the dots, then replace the comma with a dot. Identical currency codes always answer same_currency, even for a code that is not listed.

#### Headers

| Key | Value | Description |
| --- | --- | --- |
| `X-Bifrost-Invoke` | `{{api_key}}` | Private API key |

#### Query Parameters

| Parameter | Example | Required | Description |
| --- | --- | --- | --- |
| `currency1` | `USDT` | required | Source currency (crypto or fiat). Letters and digits, 2 to 16 characters; trimmed and uppercased before lookup, so usdt is accepted. |
| `currency2` | `USD` | required | Target currency (crypto or fiat). Same format as currency1. |

#### cURL Example

```bash
curl -X GET "https://bridge.bifrostcrypto.com/v1/get-price?currency1=USDT&currency2=USD" \
  -H "X-Bifrost-Invoke: YOUR_API_KEY"
```

#### Response Example

```json
{
  "success": true,
  "data": {
    "from": "USDT",
    "to": "USD",
    "price": "1,00",
    "type": "crypto_to_fiat",
    "last_updated": "2026-01-16T10:00:00-03:00"
  }
}
```

#### Errors

Every error has the shape error.code, error.message and, when useful, error.details. Authentication, scope and rate-limit errors (401, 403, 429) apply to every endpoint and are described under Authentication.

| Status | Code | When | What to do |
| --- | --- | --- | --- |
| `400` | `missing_parameters` | currency1 or currency2 is missing or empty. | Fix the request before sending it again. |
| `400` | `invalid_parameters` | A parameter was sent more than once. | Fix the request before sending it again. |
| `400` | `invalid_currency` | A code is not 2 to 16 letters and digits. | Fix the request before sending it again. |
| `404` | `price_not_found` | No quote links the two currencies, directly or inverted. | Check the pair with GET /v1/get-currencies. |
| `500` | `internal_error` | Unexpected failure. | Nothing was changed. Retry later. |

#### Other responses

Responses that are not the happy path and still need handling in your integration.

##### 404 · Price not found

No stored quote links the two currencies, directly or inverted. A quote stored as zero counts as missing.

```json
{
  "error": {
    "code": "price_not_found",
    "message": "Price not found for USDT/XYZ"
  }
}
```

##### 400 · Invalid parameters

missing_parameters: currency1 or currency2 absent or empty. invalid_parameters: a parameter sent more than once as an array (currency1[]=...). invalid_currency: a code outside 2 to 16 letters and digits. An unexpected failure answers 500 internal_error.

```json
{
  "error": {
    "code": "invalid_currency",
    "message": "Parameter currency2 is not a valid currency code"
  }
}
```

### List Available Currencies

`GET https://bridge.bifrostcrypto.com/v1/get-currencies`

Required scope: `prices:read` (a new key with no list is read-only)

Lists all available currencies (crypto and fiat) for price queries.

Active fiats come first, then each active crypto symbol once (even when the token runs on several networks), sorted by symbol. For cryptos, name repeats the symbol. An unexpected failure answers 500 internal_error.

#### Headers

| Key | Value | Description |
| --- | --- | --- |
| `X-Bifrost-Invoke` | `{{api_key}}` | Private API key |

#### cURL Example

```bash
curl -X GET "https://bridge.bifrostcrypto.com/v1/get-currencies" \
  -H "X-Bifrost-Invoke: YOUR_API_KEY"
```

#### Response Example

```json
{
  "success": true,
  "data": {
    "currencies": [
      {
        "currency": "USD",
        "name": "US Dollar",
        "type": "fiat"
      },
      {
        "currency": "BTC",
        "name": "BTC",
        "type": "crypto"
      },
      {
        "currency": "USDT",
        "name": "USDT",
        "type": "crypto"
      }
    ],
    "total": 20
  }
}
```

#### Errors

Every error has the shape error.code, error.message and, when useful, error.details. Authentication, scope and rate-limit errors (401, 403, 429) apply to every endpoint and are described under Authentication.

| Status | Code | When | What to do |
| --- | --- | --- | --- |
| `500` | `internal_error` | Unexpected failure. | Nothing was changed. Retry later. |

## API reference · Fixed Wallets

Endpoints for managing fixed (permanent) deposit addresses assigned per customer. Every transfer to a fixed address becomes a completed payin and is notified to the fixed-address webhook URL set in the panel; see Receiving on a fixed address under Create Fixed Wallet.

### Supported Networks

`GET https://bridge.bifrostcrypto.com/v1/fixed-wallets/supported`

Required scope: `wallets:read` (a new key with no list is read-only)

Returns the networks and tokens enabled for fixed addresses.

#### Headers

| Key | Value | Description |
| --- | --- | --- |
| `X-Bifrost-Invoke` | `{{api_key}}` | Private API key |

#### cURL Example

```bash
curl -X GET "https://bridge.bifrostcrypto.com/v1/fixed-wallets/supported" \
  -H "X-Bifrost-Invoke: YOUR_API_KEY"
```

#### Response Example

```json
{
  "message": "fixed_wallets_supported_success",
  "supported": [
    {
      "type": "evm",
      "label": "EVM",
      "networks": [
        "Ethereum",
        "BinanceSmartChain"
      ],
      "tokens": [
        {
          "network": "Ethereum",
          "token": "USDT",
          "symbol": "USDT"
        },
        {
          "network": "BinanceSmartChain",
          "token": "USDT",
          "symbol": "USDT"
        }
      ]
    },
    {
      "type": "tron",
      "label": "Tron",
      "networks": [
        "Tron"
      ],
      "tokens": [
        {
          "network": "Tron",
          "token": "USDT",
          "symbol": "USDT"
        }
      ]
    }
  ]
}
```

#### Errors

Every error has the shape error.code, error.message and, when useful, error.details. Authentication, scope and rate-limit errors (401, 403, 429) apply to every endpoint and are described under Authentication.

This endpoint has no errors of its own.

### Get Config

`GET https://bridge.bifrostcrypto.com/v1/fixed-wallets/config`

Required scope: `wallets:read` (a new key with no list is read-only)

Returns the user's fixed addresses config (limits, enabled status and webhook_url).

Per-type maps are null when the account uses the single global limit. When a per-type map is set it replaces the global limit of that origin, and types missing from the map cannot be created. Returns 403 feature_disabled if the feature is disabled for the user.

#### Headers

| Key | Value | Description |
| --- | --- | --- |
| `X-Bifrost-Invoke` | `{{api_key}}` | Private API key |

#### cURL Example

```bash
curl -X GET "https://bridge.bifrostcrypto.com/v1/fixed-wallets/config" \
  -H "X-Bifrost-Invoke: YOUR_API_KEY"
```

#### Response Example

```json
{
  "message": "fixed_wallets_config_success",
  "config": {
    "enabled": 1,
    "max_wallets_panel": 5,
    "max_wallets_panel_per_type": null,
    "max_wallets_external_api": 1000,
    "max_wallets_external_api_per_type": {
      "evm": 800,
      "tron": 200
    },
    "webhook_url": "https://your-site.com/webhook-fixed-wallets"
  }
}
```

#### Errors

Every error has the shape error.code, error.message and, when useful, error.details. Authentication, scope and rate-limit errors (401, 403, 429) apply to every endpoint and are described under Authentication.

| Status | Code | When | What to do |
| --- | --- | --- | --- |
| `403` | `feature_disabled` | Fixed addresses are not enabled for the account. | Contact support. |

#### Other responses

Responses that are not the happy path and still need handling in your integration.

##### 403 · Feature disabled

Fixed addresses are not enabled for the account. Ask support to enable them.

```json
{
  "error": {
    "code": "feature_disabled",
    "message": "This feature is not enabled for the account"
  }
}
```

### Update Config

`PUT https://bridge.bifrostcrypto.com/v1/fixed-wallets/config`

Required scope: `wallets:write` (a new key with no list is read-only)

Registers, updates or removes the webhook_url for fixed addresses.

The webhook_url field is required: send "" or null to remove the URL. A body without the field is refused with 400 invalid_parameters instead of clearing the stored URL. Returns 403 feature_disabled if the feature is disabled.

#### Headers

| Key | Value | Description |
| --- | --- | --- |
| `X-Bifrost-Invoke` | `{{api_key}}` | Private API key |
| `Content-Type` | `application/json` | - |

#### Body Parameters

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `webhook_url` | `string \| null` | required | Callback URL for fixed address deposit notifications. Must be a valid, public HTTPS URL (max 756 chars); private or reserved hosts and blocked ports are refused. Send "" or null to remove it. |

#### Request Body

```json
{
  "webhook_url": "https://your-site.com/webhook-fixed-wallets"
}
```

#### cURL Example

```bash
curl -X PUT "https://bridge.bifrostcrypto.com/v1/fixed-wallets/config" \
  -H "X-Bifrost-Invoke: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "webhook_url": "https://your-site.com/webhook-fixed-wallets"
}'
```

#### Response Example

```json
{
  "message": "fixed_wallets_config_update_success",
  "webhook_url": "https://your-site.com/webhook-fixed-wallets"
}
```

#### Errors

Every error has the shape error.code, error.message and, when useful, error.details. Authentication, scope and rate-limit errors (401, 403, 429) apply to every endpoint and are described under Authentication.

| Status | Code | When | What to do |
| --- | --- | --- | --- |
| `400` | `invalid_parameters` | webhook_url is missing, not a string or null, not a valid URL, or longer than 756 characters; details.fields explains. | Fix the request before sending it again. |
| `400` | `invalid_webhook_url` | The URL is well formed but not allowed (not HTTPS, private address, non-standard port). | Fix the request before sending it again. |
| `403` | `feature_disabled` | Fixed addresses are not enabled for the account. | Contact support. |
| `500` | `config_update_failed` | The change could not be saved. | Nothing was changed. Retry later. |

#### Other responses

Responses that are not the happy path and still need handling in your integration.

##### 400 · Field missing or invalid

invalid_parameters when webhook_url is absent, is not a string or null, is not a valid URL, or is longer than 756 characters; details.fields.webhook_url explains which. invalid_webhook_url when the URL is well formed but not allowed (not HTTPS, private or reserved address, blocked port). 403 feature_disabled if the feature is off; 500 config_update_failed if the change could not be saved.

```json
{
  "error": {
    "code": "invalid_parameters",
    "message": "Invalid parameters provided",
    "details": {
      "fields": {
        "webhook_url": "The webhook_url field is required; send an empty value to remove it."
      }
    }
  }
}
```

### List Fixed Wallets

`GET https://bridge.bifrostcrypto.com/v1/fixed-wallets`

Required scope: `wallets:read` (a new key with no list is read-only)

Lists the fixed addresses assigned to your account via the external API, with optional filters, newest first.

Addresses created in the panel are not listed. count is the number of items in this page, not a grand total. allowance is the remaining creation budget for the external API origin, in one of two shapes: mode global ({limit, used, remaining, supported_types}, shared by every type) or mode per_type ({items: [{type, label, limit, used, remaining}]}, one entry per type you may create). allowance is null when fixed addresses are not enabled for the account (the list itself still answers 200). In global mode used counts every address ever assigned through the API, revoked ones included; in per_type mode only addresses still in STATUS_ASSIGNED count.

#### Headers

| Key | Value | Description |
| --- | --- | --- |
| `X-Bifrost-Invoke` | `{{api_key}}` | Private API key |

#### Query Parameters

| Parameter | Example | Required | Description |
| --- | --- | --- | --- |
| `type` | `evm` | - | Filter by wallet type, lowercase exact match (e.g.: evm, tron, bitcoin) |
| `status` | `STATUS_ASSIGNED` | - | Filter by status: STATUS_ASSIGNED (active) or STATUS_REVOKED (revoked after inactivity) |
| `customer_info` | `customer123` | - | Filter by customer identifier (exact match) |
| `limit` | `100` | - | Items to return (default: 100). Out-of-range values are clamped to 1..500, not refused. |
| `offset` | `0` | - | Number of items to skip (default: 0). Negative values become 0. |

#### cURL Example

```bash
curl -X GET "https://bridge.bifrostcrypto.com/v1/fixed-wallets?type=evm&status=STATUS_ASSIGNED&customer_info=customer123" \
  -H "X-Bifrost-Invoke: YOUR_API_KEY"
```

#### Response Example

```json
{
  "message": "fixed_wallets_list_success",
  "wallets": [
    {
      "customer_info": "customer123",
      "wallet": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0",
      "type": "evm",
      "status": "STATUS_ASSIGNED",
      "origin": "external_api",
      "last_transaction_at": null,
      "created_at": "2026-01-16 10:00:00",
      "updated_at": "2026-01-16 10:00:00",
      "networks": [
        "Ethereum",
        "BinanceSmartChain"
      ],
      "accepted_tokens": [
        {
          "network": "Ethereum",
          "token": "USDT",
          "symbol": "USDT"
        },
        {
          "network": "BinanceSmartChain",
          "token": "USDT",
          "symbol": "USDT"
        }
      ],
      "pending_revocation": false,
      "revoke_at": null
    }
  ],
  "count": 1,
  "allowance": {
    "mode": "global",
    "limit": 1000,
    "used": 1,
    "remaining": 999,
    "supported_types": [
      {
        "type": "evm",
        "label": "EVM"
      },
      {
        "type": "tron",
        "label": "Tron"
      }
    ]
  }
}
```

#### Receiving on a fixed address

Each transfer to a fixed address creates a new payin, already completed, and sends payment_detected / payment_confirmed to the fixed-address webhook URL. That URL is set in the panel (it can also be set with PUT /v1/fixed-wallets/config); without it no notice is sent. The payin has a generated reference_id (equal to internal_reference), is_wallet_fixed: true and the customer_info of the address, so match the deposit to your customer by customer_info or wallet. Payins from addresses created through the API are also returned by List Payins and Check Specific Payin. Transfers worth less than 0.01 in fiat are ignored.

```json
{
  "event": "payment_confirmed",
  "timestamp": "2026-03-01 12:03:00",
  "data": {
    "status": "success",
    "funds_available": true,
    "finality_status": "final",
    "wallet": "0x742d35cc6634c0532925a3b844bc9e7595f0beb0",
    "customer_info": "customer123",
    "network": "BinanceSmartChain",
    "crypto_currency": "USDT",
    "crypto_amount": "25.00000000",
    "received_amount": "25.00000000",
    "reference_id": "8d1f3a52-6c0e-4b7a-9f21-3e5d7c9a0b14",
    "internal_reference": "8d1f3a52-6c0e-4b7a-9f21-3e5d7c9a0b14",
    "is_wallet_fixed": true,
    "is_duplicated": false,
    "...": "..."
  }
}
```

#### Errors

Every error has the shape error.code, error.message and, when useful, error.details. Authentication, scope and rate-limit errors (401, 403, 429) apply to every endpoint and are described under Authentication.

This endpoint has no errors of its own.

#### Other responses

Responses that are not the happy path and still need handling in your integration.

##### 200 · Per-type allowance

When the account has a per-type limit map for the external API, allowance lists one item per allowed type instead of a shared limit. Types not listed cannot be created.

```json
{
  "message": "fixed_wallets_list_success",
  "wallets": [],
  "count": 0,
  "allowance": {
    "mode": "per_type",
    "items": [
      {
        "type": "evm",
        "label": "EVM",
        "limit": 800,
        "used": 12,
        "remaining": 788
      },
      {
        "type": "tron",
        "label": "Tron",
        "limit": 200,
        "used": 0,
        "remaining": 200
      }
    ]
  }
}
```

### Create Fixed Wallet

`POST https://bridge.bifrostcrypto.com/v1/fixed-wallets`

Required scope: `wallets:write` (a new key with no list is read-only)

Assigns a fixed (permanent) address to your account for the given chain family (type).

A single address serves every network of the same family (e.g. an evm address works across all EVM networks). A new assignment answers 201 with fixed_wallets_assign_success. If customer_info is provided and already has an active (STATUS_ASSIGNED) address of the same type, the existing one is returned with 200 and fixed_wallets_already_assigned. That reuse check ignores origin, so it can return an address created in the panel (origin: panel), which List Fixed Wallets does not show. The reuse is checked before the limits, so it never consumes allowance.

#### Headers

| Key | Value | Description |
| --- | --- | --- |
| `X-Bifrost-Invoke` | `{{api_key}}` | Private API key |
| `Content-Type` | `application/json` | - |

#### Body Parameters

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `type` | `string` | required | Chain family enabled for fixed addresses (e.g.: evm, tron, solana, bitcoin). See the type field in Supported Networks. Max 20 chars, case-insensitive. |
| `customer_info` | `string` | - | Customer identifier (no spaces, max 255). Enables idempotent reuse of the same address per customer. |

#### Request Body

```json
{
  "type": "evm",
  "customer_info": "customer123"
}
```

#### cURL Example

```bash
curl -X POST "https://bridge.bifrostcrypto.com/v1/fixed-wallets" \
  -H "X-Bifrost-Invoke: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "type": "evm",
  "customer_info": "customer123"
}'
```

#### Response Example

```json
{
  "message": "fixed_wallets_assign_success",
  "wallet": {
    "customer_info": "customer123",
    "wallet": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0",
    "type": "evm",
    "status": "STATUS_ASSIGNED",
    "origin": "external_api",
    "last_transaction_at": null,
    "created_at": "2026-01-16 10:00:00",
    "updated_at": "2026-01-16 10:00:00",
    "networks": [
      "Ethereum",
      "BinanceSmartChain"
    ],
    "accepted_tokens": [
      {
        "network": "Ethereum",
        "token": "USDT",
        "symbol": "USDT"
      }
    ],
    "pending_revocation": false,
    "revoke_at": null
  }
}
```

#### Receiving on a fixed address

Each transfer to a fixed address creates a new payin, already completed, and sends payment_detected / payment_confirmed to the fixed-address webhook URL. That URL is set in the panel (it can also be set with PUT /v1/fixed-wallets/config); without it no notice is sent. The payin has a generated reference_id (equal to internal_reference), is_wallet_fixed: true and the customer_info of the address, so match the deposit to your customer by customer_info or wallet. Payins from addresses created through the API are also returned by List Payins and Check Specific Payin. Transfers worth less than 0.01 in fiat are ignored.

```json
{
  "event": "payment_confirmed",
  "timestamp": "2026-03-01 12:03:00",
  "data": {
    "status": "success",
    "funds_available": true,
    "finality_status": "final",
    "wallet": "0x742d35cc6634c0532925a3b844bc9e7595f0beb0",
    "customer_info": "customer123",
    "network": "BinanceSmartChain",
    "crypto_currency": "USDT",
    "crypto_amount": "25.00000000",
    "received_amount": "25.00000000",
    "reference_id": "8d1f3a52-6c0e-4b7a-9f21-3e5d7c9a0b14",
    "internal_reference": "8d1f3a52-6c0e-4b7a-9f21-3e5d7c9a0b14",
    "is_wallet_fixed": true,
    "is_duplicated": false,
    "...": "..."
  }
}
```

#### Errors

Every error has the shape error.code, error.message and, when useful, error.details. Authentication, scope and rate-limit errors (401, 403, 429) apply to every endpoint and are described under Authentication.

| Status | Code | When | What to do |
| --- | --- | --- | --- |
| `400` | `invalid_parameters` | type missing or longer than 20, or customer_info with spaces or longer than 255; details.fields. | Fix the request before sending it again. |
| `403` | `feature_disabled` | Fixed addresses are not enabled for the account. | Contact support. |
| `403` | `limit_not_allowed` | Your limit for this type is zero, or the type is not in your per-type map. | Contact support. |
| `409` | `limit_reached` | You already hold the maximum number of fixed addresses. | Check allowance in List Fixed Wallets. |
| `409` | `no_wallet_available` | No address of that type is available right now. | Nothing was changed. Retry later. |
| `422` | `type_not_supported` | type is not listed in Supported Networks. | Fix the request before sending it again. |
| `500` | `assign_failed` | The address could not be assigned. | Nothing was changed. Retry later. |
| `502` | `external_processor_error` | The address could not be created right now. | Nothing was changed. Retry later. |

#### Other responses

Responses that are not the happy path and still need handling in your integration.

##### 200 · Existing address reused

The same customer_info and chain family returns the address already assigned. A new assignment uses the body above with HTTP 201.

```json
{
  "message": "fixed_wallets_already_assigned",
  "wallet": {
    "customer_info": "customer123",
    "wallet": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0",
    "type": "evm",
    "status": "STATUS_ASSIGNED",
    "origin": "external_api",
    "last_transaction_at": "2026-02-03 18:21:07",
    "created_at": "2026-01-16 10:00:00",
    "updated_at": "2026-02-03 18:21:07",
    "networks": [
      "Ethereum",
      "BinanceSmartChain"
    ],
    "accepted_tokens": [
      {
        "network": "Ethereum",
        "token": "USDT",
        "symbol": "USDT"
      }
    ],
    "pending_revocation": false,
    "revoke_at": null
  }
}
```

##### 409 · Limit reached

Other refusals: 400 invalid_parameters (type missing or longer than 20, customer_info with spaces or longer than 255; details.fields), 403 feature_disabled, 422 type_not_supported (type not listed in Supported Networks), 403 limit_not_allowed (limit 0, or type missing from the per-type map), 409 limit_reached, 409 no_wallet_available (no address of that type is available right now; retry later), 500 assign_failed, 502 external_processor_error (the address could not be created right now; retry later).

```json
{
  "error": {
    "code": "limit_reached",
    "message": "Fixed address limit reached"
  }
}
```

---

## Changelog

Changes to the API contract, newest first. **breaking** marks a change that can stop an integration that works today; review those before each release.

### 2026-10-10

- **breaking**: API keys created before scopes existed (no scope list) are now read-only: payout:read, payin:read, balances:read, statements:read and prices:read. Calls outside these scopes return 403 scope_denied.
- **breaking**: Webhooks no longer carry the X-IPN-Secret header. Verify X-Bifrost-Signature.
- **breaking**: Every /v1 error now has the same shape: error.code, error.message and, when useful, error.details. Branch on error.code.
- **breaking**: Codes that named the external processor are now returned as external_processor_error.
- **breaking**: GET /v1/statements: amount is now a string with 8 decimals instead of a JSON number. GET /v1/statements/summary sums total_amount without floating-point rounding.
- **breaking**: GET /v1/statements and /v1/statements/summary return only movements caused by API operations: DEPOSIT, WITHDRAWAL, RATE, FEE, WITHDRAWAL_REFUND and DEPOSIT_REVERSAL. EXCHANGE and ROYALTY are no longer returned or accepted in type.
- **breaking**: Fixed-wallet success messages are now snake_case: fixed_wallets_supported_success, fixed_wallets_list_success, fixed_wallets_config_success, fixed_wallets_config_update_success, fixed_wallets_assign_success, fixed_wallets_already_assigned.
- **breaking**: Webhook payment_reversed: data.status is now reversed instead of success.
- **added**: POST /v1/payout and POST /v1/payin accept network aliases (eth, erc20, bnb, bsc, bep20, arb, trx, trc20, matic, sol, btc and others; see Network aliases). Case, spaces, hyphens and underscores are ignored. Payouts and payins are stored and returned with the canonical network name; list filters still take the canonical name.
- **added**: GET /v1/summary/tokens and the accepted_tokens of GET /v1/summary: balance_key, the key of the token in GET /v1/balances and in the balance filter of GET /v1/statements.
- **added**: GET /v1/check-payin and GET /v1/list-payin: status reversed and the reversal field (reversed_at, partial, amounts) when the network undid a credited payment. list-payin accepts status=reversed and its summary has total_reversed; status=completed excludes reversed payins.
- **changed**: GET /v1/get-price: prices below 1 keep 8 significant digits instead of 2 decimals, so low-value tokens are no longer quoted as 0,00. A quote stored as zero now answers 404 price_not_found.
- **changed**: POST /v1/payin: when the account has no minimum_payment set, the payin requires 100% of crypto_amount to be completed.
- **removed**: invalid_expires_at is no longer returned: an expires_at above 86400 answers 400 invalid_parameters with details.fields.expires_at.

---

## Integration recipes (step by step for agents)

### Accept a payment (payin)

1. Read the API key, and the signing secret when the key requires signing, from environment variables, never from code.
2. Call `GET /v1/summary/tokens` to choose the token and network (`min_receipt`, `max_receipt`). The list is not filtered by your account: the create call confirms whether the token accepts deposits for you.
3. Call `POST /v1/payin` with your order id as `reference_id`, `crypto_currency`, `network`, `process_by` and the matching amount as a decimal string, `webhook_url` and `customer_info`. Store `data.internal_reference` and show the customer `data.wallet`, plus the memo when the token needs one.
4. In the webhook endpoint, read the raw body, verify `X-Bifrost-Signature`, answer 2xx at once and process afterwards. Discard notices already processed by `data.internal_reference` together with `event`.
5. On `payment_confirmed`, compare `data.received_amount` with `data.crypto_amount` and deliver. Delivering on `payment_detected` is your decision and carries the risk of `payment_reversed`. When `is_duplicated` is true, link the payment to the order by `customer_info`.
6. After a timeout on create, resend with the same `reference_id`; on `duplicate_reference_id`, fetch the payin with `GET /v1/list-payin?reference_id=…`. Branch every error on `error.code`.
7. Reconcile with `GET /v1/check-payin/{internal_reference}`.

### Send payouts

1. Read the API key, and the signing secret when the key requires signing, from environment variables, never from code.
2. Check `GET /v1/balances` (use `available` when it has the key of the token) and `min_withdrawal` and `max_withdrawal` in `GET /v1/summary/tokens`.
3. Call `POST /v1/payout` with a list of up to 50 payouts, each with its own stable `reference_id`, amounts as decimal strings and `feetakenfromamount` 0 or 1, and send an `Idempotency-Key` per request.
4. Read `results[]` by `index`: store the `internal_reference` of the created items; for the refused ones decide by `results[].error.code` and resend only those, with a new Idempotency-Key.
5. After a timeout, resend the same body with the same Idempotency-Key within 24 hours. On 409 `idempotency_key_in_progress`, wait and retry with the same key. On 500, resend only the items counted in `summary.not_processed`.
6. Handle the `payment_processed` and `payment_failed` webhooks, verifying the signature and deduplicating on `data.internal_reference` together with `event`. To pay again after a failure, create a new payout with a new `reference_id`.
7. Reconcile with `GET /v1/check-payout/{internal_reference}`.

### Fixed address per customer

1. Fixed addresses must be enabled for the account; otherwise creating an address or reading or changing the configuration answers 403 `feature_disabled`. `GET /v1/fixed-wallets/supported` lists the types and `GET /v1/fixed-wallets/config` the limits and the webhook URL.
2. Set the fixed-address webhook URL in the panel or with `PUT /v1/fixed-wallets/config`. Without it no notice is sent.
3. Call `POST /v1/fixed-wallets` with `type` and your customer id in `customer_info`: 201 for a new address, 200 `fixed_wallets_already_assigned` when that customer already holds one of the type. One address serves every network of its family.
4. Each transfer to the address creates a payin already completed and sends `payment_detected` / `payment_confirmed` to the fixed-address URL, with `is_wallet_fixed` true and a generated `reference_id`. Match it to the customer by `customer_info` or `wallet`.
5. Handle `fixed_address_pre_revoke` (120 days without a transaction) and `fixed_address_revoked` (150 days). Addresses of type `evm` are permanent.
6. Use `GET /v1/fixed-wallets` to list the addresses and read `allowance` before creating more.
