# API de Bifrost Crypto v1

> Versión en Markdown de la documentación pública de la API v1 de Bifrost Crypto, para agentes de IA y generadores de código. El mismo contenido de las páginas HTML, generado de la misma fuente, sin navegación ni estilos.
>
> Versión HTML: https://bifrostcrypto.com/es/docs
> Archivos relacionados: [ai.md](https://bifrostcrypto.com/ai/es.md) · [Postman](https://bifrostcrypto.com/postman-collection.json) · [OpenAPI](https://bifrostcrypto.com/openapi.json) · [llms.txt](https://bifrostcrypto.com/llms.txt)
> Otros idiomas: [Português](https://bifrostcrypto.com/ai/pt-BR.md) · [English](https://bifrostcrypto.com/ai.md) · [Français](https://bifrostcrypto.com/ai/fr.md)
> API v1 · último cambio de contrato el 2026-10-10

## Resumen rápido para agentes

- URL base `https://bridge.bifrostcrypto.com`. Toda ruta empieza con `/v1`. Los cuerpos de solicitud y respuesta son JSON (`Content-Type: application/json`).
- Envíe la clave de la API en `X-Bifrost-Invoke` en cada solicitud. No hay inicio de sesión ni token que renovar. Sin clave: 401 `api_key_missing`; clave desconocida, revocada o vencida: 401 `api_key_invalid`.
- Una clave puede crearse con firma de solicitudes. Sus solicitudes llevan además `X-Bifrost-Timestamp` (segundos Unix), `X-Bifrost-Nonce` (16 a 80 caracteres, aceptado una vez) y `X-Bifrost-Signature`: el HMAC-SHA256 en hexadecimal, con el secreto de firma, de METHOD, PATH (con `/v1`), la QUERY sin procesar, TIMESTAMP, NONCE y el SHA-256 en hexadecimal de los bytes exactos del cuerpo, unidos por `\n`. Firme los bytes que envía y nunca vuelva a serializar el cuerpo después de firmar.
- Cada clave tiene scopes. Una llamada fuera de ellos devuelve 403 `scope_denied` con `details.required_scopes`. Una clave creada sin lista de scopes es de solo lectura.
- Envíe `crypto_amount` y `fiat_amount` como cadenas decimales simples, como `"100.50"`: dígitos y un punto opcional, sin notación científica, signos ni separadores de miles. Los montos de las respuestas son cadenas con un número de decimales variable: conviértalos con una biblioteca decimal, no como float.
- Excepción: `price` en `GET /v1/get-price` usa coma como separador decimal y punto como separador de miles (`"65.432,10"`). Quite los puntos y cambie la coma por un punto antes de convertir.
- Los campos de fecha y hora son `YYYY-MM-DD HH:MM:SS` en UTC−03:00, sin offset en el valor, y los filtros de fecha se leen en el mismo offset. Excepciones: el límite diario de payout de una clave se reinicia a las 00:00 UTC, y los webhooks de dirección fija envían `timestamp` en ISO 8601 con el offset.
- `network` recibe los nombres canónicos listados en Supported Networks (`Ethereum`, `BinanceSmartChain`, `Tron`…). Los endpoints de creación también aceptan alias (`erc20`, `bsc`, `trc20`…); las respuestas, los webhooks y los filtros de listado usan siempre el nombre canónico, y los filtros distinguen mayúsculas. Los tokens de cada red vienen de `GET /v1/summary/tokens`: no los fije en el código.
- `GET /v1/balances` tiene una clave por token (`balance_<nombre>`), compartida por todas las redes; la clave de cada token es el `balance_key` de `GET /v1/summary/tokens`. Recorra las claves en lugar de fijarlas en el código. Cuando `available` tenga la clave, úselo: el resto está retenido por depósitos aún no finales.
- `POST /v1/payout` acepta un objeto o una lista de hasta 50 y siempre responde con `summary` y `results[]`, una entrada por ítem en el orden enviado: 200 cuando todos se crearon, 207 cuando algunos se rechazaron, 400 cuando ninguno se creó. Un ítem rechazado trae `results[].error.code`. Dé a cada payout su propio `reference_id` (uno repetido se rechaza con `duplicate_payment_reference`, nunca se paga dos veces) y envíe un `Idempotency-Key` por solicitud (se guarda 24 horas).
- `POST /v1/payin` no tiene Idempotency-Key: el `reference_id` único es lo que impide un segundo payin. Tras un timeout, reenvíe con el mismo `reference_id`; `duplicate_reference_id` significa que el primero se creó, y `GET /v1/list-payin?reference_id=…` lo devuelve.
- Guarde el `internal_reference` devuelto en cada creación: los endpoints de consulta lo reciben en la ruta y los webhooks lo traen.
- Un payin queda `completed` cuando `received_amount` alcanza `minimum_payment` por ciento de `crypto_amount`, lo que puede ser menos que el monto completo. Compare `received_amount` con `crypto_amount` antes de entregar.
- Todo error tiene `error.code`, `error.message` y, cuando es útil, `error.details`. Decida por `error.code`, nunca por el mensaje. 4xx: cambie la solicitud antes de reenviarla. 5xx: la misma solicitud puede repetirse. 429: espere los segundos de `Retry-After`.
- Los webhooks son POST con cuerpo JSON a su `webhook_url`. Antes de confiar en el cuerpo, verifique `X-Bifrost-Signature`: `v1=` más el HMAC-SHA256 en hexadecimal de `v1`, `X-Bifrost-Timestamp`, `X-Bifrost-Delivery` y el SHA-256 en hexadecimal del cuerpo sin procesar, unidos por `\n`, con la clave de webhook de la cuenta (en el panel). Rechace un timestamp con más de 300 segundos de diferencia, compare en tiempo constante, responda 2xx en menos de 30 segundos y procese después.
- Decida por el campo `event` del cuerpo, no por `X-Bifrost-Webhook-Type`. Payouts: `payment_processed`, `payment_failed`. Payins: `payment_detected` (`funds_available` false, aún no final), `payment_confirmed` (final, fondos disponibles), `payment_reversed`. El mismo aviso puede llegar más de una vez: deduplique por `data.internal_reference` junto con `event`.
- Los endpoints de listado y consulta devuelven solo payouts y payins creados por la API; los hechos en el panel no se devuelven.
- Cada clave tiene su propio límite de solicitudes, 500 cada 60 segundos por defecto, informado en `X-RateLimit-Limit`, `X-RateLimit-Remaining` y `X-RateLimit-Reset`.
- No hay sandbox público: cada llamada actúa sobre la cuenta real de la clave. Pruebe con montos pequeños.
- Los cambios de contrato, incluidos los que rompen integraciones, están en el Changelog, del más reciente al más antiguo.

### Endpoints

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

## Contenido

- API de Bifrost Crypto: https://bifrostcrypto.com/ai/es/introduction.md
- Autenticación y Control de Acceso: https://bifrostcrypto.com/ai/es/authentication.md
- Recibir un pago: https://bifrostcrypto.com/ai/es/guide-payin.md
- Enviar un payout: https://bifrostcrypto.com/ai/es/guide-payout.md
- Verificación de webhooks: https://bifrostcrypto.com/ai/es/webhooks.md
- Webhooks de retiro: https://bifrostcrypto.com/ai/es/webhooks-payments.md
- Webhooks de depósito: https://bifrostcrypto.com/ai/es/webhooks-receipts.md
- Códigos de Error: https://bifrostcrypto.com/ai/es/error-codes.md
- Referencia de la API · Payments Payout
- Referencia de la API · Deposits Payin
- Referencia de la API · Queries
- Referencia de la API · Prices
- Referencia de la API · Fixed Wallets
- Changelog: https://bifrostcrypto.com/ai/es/changelog.md
- Recetas de integración (paso a paso para agentes)

---

## API de Bifrost Crypto

Referencia completa de la API Bifrost Crypto V1. Integra pagos cripto multi-cadena en tu plataforma en minutos.

### Autenticación

Todas las rutas requieren el header con tu clave API privada: `X-Bifrost-Invoke`

### URL Base

Todas las solicitudes se realizan a: `https://bridge.bifrostcrypto.com`

### Formato

Todos los cuerpos de solicitud y respuesta usan formato JSON: `Content-Type: application/json`

### Inicio Rápido

Cada solicitud de la API debe incluir tu clave privada en el header:

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

### Especificación OpenAPI

La referencia completa en un archivo OpenAPI 3.1. Impórtela en Postman o Insomnia para probar los endpoints, o genere un cliente en su lenguaje. Se genera desde la misma fuente que esta documentación.

https://bifrostcrypto.com/openapi.json

### Fechas y horas

Los campos de fecha y hora (created_at, updated_at, expires_at y los demás) son YYYY-MM-DD HH:MM:SS en UTC−03:00, sin el offset en el valor. Los filtros de fecha (date_from, date_to) se leen en el mismo offset. Dos excepciones: el límite diario de payout de una clave de API se reinicia a las 00:00 UTC, y los webhooks de dirección fija envían timestamp en ISO 8601 con el offset incluido.

### Redes Soportadas

Valores aceptados en el campo network, sin distinguir mayúsculas al crear. Al crear también se aceptan alias comunes (erc20, bsc, trc20, matic…), que se convierten a estos nombres; vea Network aliases en los endpoints de creación. Las respuestas, los webhooks y los filtros de listado usan siempre los nombres de abajo. Qué tokens están disponibles en cada red lo indica 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`

### Códigos de Estado HTTP

| Código | Significado |
| --- | --- |
| `200` | Éxito |
| `201` | Creado - dirección fija asignada |
| `207` | Multi-Status - lote de payout con ítems rechazados |
| `400` | Solicitud Inválida - parámetros inválidos |
| `401` | No Autorizado - clave de API o firma ausente o inválida |
| `403` | Prohibido - sin permiso para este recurso |
| `404` | No Encontrado - recurso o ruta inexistente |
| `409` | Conflicto - solicitud aún en curso o límite alcanzado |
| `422` | No Procesable - type no soportado |
| `429` | Demasiadas Solicitudes - cuota o límite de fallos de autenticación |
| `500` | Error del Servidor |
| `502` | Bad Gateway - falló un servicio del que dependemos. Reintente más tarde. |
| `503` | Servicio No Disponible - reintente más tarde |

### Categorías de API

- **Pagos (Payout)** (3 endpoints): Crea y gestiona retiros cripto. Hasta 50 por lote.
- **Depósitos (Payin)** (3 endpoints): Genera billeteras de depósito y rastrea pagos entrantes.
- **Consultas** (6 endpoints): Verifica saldos, extractos, tokens e información de cuenta.
- **Precios** (2 endpoints): Obtén tasas de conversión en tiempo real y monedas disponibles.
- **Direcciones Fijas** (5 endpoints): Asigna y gestiona direcciones de depósito permanentes por cliente.

## Autenticación y Control de Acceso

Toda solicitud /v1 se autentica mediante clave de API. La clave puede además restringirse por alcance, por IP de origen y por firma de la solicitud.

### Clave de API

Envía tu clave en la cabecera X-Bifrost-Invoke en cada solicitud. No hay paso de inicio de sesión ni token que renovar: la clave es la credencial.

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

El acceso se vuelve a verificar en cada solicitud. Revocar la clave, dejarla expirar o tener el acceso a la API desactivado en la cuenta surte efecto en la siguiente solicitud, sin esperar a que venza ninguna caché.

- Encabezado ausente: 401 api_key_missing. Clave desconocida, revocada o expirada: 401 api_key_invalid.
- Acceso a la API desactivado en la cuenta: 403 api_access_denied.
- E-mail de la cuenta no verificado: 403 email_not_verified. Verifique el e-mail en el panel; la clave empieza a funcionar en la siguiente solicitud.
- Cuenta suspendida: las claves se conservan, pero responden 401 api_key_invalid mientras dure la suspensión y vuelven a funcionar cuando se reactiva la cuenta.

### Alcances

Un alcance es el permiso de llegar a un grupo de endpoints. Al crear una clave eliges qué alcances lleva, de modo que una clave usada solo para leer saldos no pueda crear un retiro aunque se filtre.

> Una clave creada sin lista de alcances es de solo lectura: payout:read, payin:read, balances:read, statements:read y prices:read. No tiene alcance de escritura ni de direcciones fijas.

| Alcance | Da acceso a |
| --- | --- |
| `payout:read` | Listar retiros y consultar el estado de un retiro |
| `payout:write` | Crear retiros |
| `payin:read` | Listar depósitos y consultar el estado de un depósito |
| `payin:write` | Crear depósitos |
| `balances:read` | Leer los saldos de la cuenta |
| `statements:read` | Leer extractos, resúmenes, tokens aceptados y monedas fiduciarias aceptadas |
| `prices:read` | Leer cotizaciones y el catálogo de monedas |
| `wallets:read` | Listar carteras fijas y leer su configuración |
| `wallets:write` | Crear carteras fijas y modificar su configuración |

Llamar a un endpoint fuera de los ámbitos de la clave devuelve 403 indicando qué ámbitos servirían, para que la corrección sea visible sin adivinar. Algunos endpoints aceptan más de uno, y basta con tener cualquiera:

```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"]
    }
  }
}
```

En las claves con alcance de escritura (payout:write, payin:write, wallets:write) conviene sumar una segunda capa al alcance: lista blanca de IP, si su integración llama desde una dirección fija, o firma de la solicitud, si no lo hace. Ninguna de las dos es obligatoria, pero ambas existen porque una credencial que mueve dinero merece más que un secreto estático.

### Lista blanca de IP

> La lista blanca es opcional, en cualquier alcance. Sin ella la clave se acepta desde cualquier origen, y nada de lo que ya ejecuta necesita cambiar. Es una capa adicional para quien llama desde una dirección fija; si su integración no tiene IP fija, use require_signature, que protege una clave filtrada sin depender del origen.

La clave puede restringirse a un conjunto de direcciones de origen. Se aceptan direcciones individuales y rangos en notación CIDR, tanto IPv4 como IPv6:

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

Una solicitud llega por una familia de direcciones a la vez. Si su infraestructura nos alcanza por IPv4 y por IPv6, declare ambas, porque una entrada de una familia no autoriza la otra. Las solicitudes de fuera de la lista se rechazan con 403 ip_not_authorized.

### Firma de la Solicitud (HMAC)

> La firma es opcional y se activa por clave, al momento de crearla. Las claves que no la exigen siguen autenticando solo con X-Bifrost-Invoke, sin ningún encabezado adicional, y nada de lo que ya ejecuta se ve afectado.

Opcional por clave. La clave de API por sí sola prueba que quien llamó conoce un secreto estático, pero no prueba que la solicitud sea reciente, que el cuerpo esté íntegro, ni que esa llamada no haya ocurrido ya. Con la firma activada, cada solicitud lleva una prueba válida una sola vez y por una ventana corta.

| Cabecera | Descripción |
| --- | --- |
| `X-Bifrost-Timestamp` | Hora Unix en segundos enteros, solo dígitos, en el momento en que se construyó la solicitud. Se rechaza si difiere de la hora del servidor en más de 300 segundos. |
| `X-Bifrost-Nonce` | Valor único por solicitud, de 16 a 80 caracteres entre A-Z, a-z, 0-9, guion bajo y guion. Se acepta una vez por clave. |
| `X-Bifrost-Signature` | HMAC-SHA256 de la cadena canónica, con su secreto de firma como clave, en hexadecimal. Envíelo en minúsculas; las mayúsculas también se aceptan. |

#### La cadena canónica

Construye estos seis campos unidos por un único carácter de salto de línea, exactamente en este orden:

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

METHOD en mayúsculas. PATH es la ruta exactamente como aparece en la línea de la solicitud, con la barra inicial y el prefijo /v1 (por ejemplo /v1/check-payout/f47ac10b-58cc-4372-a567-0e02b2c3d479): no se decodifica y la barra final se conserva. QUERY es la query string cruda tal como se envía, sin el signo de interrogación inicial, sin reordenar ni recodificar, y queda vacía cuando no hay parámetros. El último campo es el SHA-256 en hexadecimal de los bytes exactos del cuerpo que envía. En solicitudes sin cuerpo, use el hash de la cadena vacía, que es e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.

#### Firmando una solicitud

```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,
});
```

#### Puntos a tener en cuenta

- El secreto de firma se muestra una sola vez, al crear la clave. Nunca se devuelve de nuevo, así que guárdalo antes de cerrar el panel.
- El secreto es distinto de la clave de API. Ambos son necesarios: la clave te identifica, la firma prueba la solicitud.
- Calcula el hash de los mismos bytes que transmites. Serializar el cuerpo dos veces, o volver a serializarlo tras firmar, cambia el resumen e invalida la firma.
- Cada nonce se acepta una vez por clave. Reenviar la solicitud idéntica devuelve signature_replayed aunque todo lo demás sea correcto.
- El desfase de reloj es la causa más común de signature_timestamp_out_of_range. Mantén tus servidores con NTP.

### Límites de Solicitudes

Cada clave tiene su propia cuota en una ventana fija alineada al reloj: por defecto 500 solicitudes cada 60 segundos, definidas al crear la clave. Las respuestas traen el estado actual, para que regule su consumo en vez de descubrir el límite al chocar con él:

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

- X-RateLimit-Limit es la cuota, X-RateLimit-Remaining lo que queda en la ventana y X-RateLimit-Reset la hora Unix en que la ventana se reinicia.
- Los encabezados llegan en toda respuesta que pasó la autenticación y la verificación de scope, incluidas respuestas 4xx y 5xx del endpoint.
- No aparecen en los rechazos de autenticación y de scope, ni en claves configuradas sin cuota, que no tienen límite de solicitudes.

Superada la cuota, las solicitudes devuelven 429 rate_limit_exceeded con el encabezado Retry-After hasta que la ventana se reinicie. No hay penalización adicional:

```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"
    }
  }
}
```

#### Fallos repetidos de autenticación

Independientemente de la cuota por clave, una dirección IP que falla la autenticación repetidamente queda bloqueada durante un tiempo. Mientras está bloqueada recibe 429 too_many_failed_authentications con el encabezado Retry-After: espere esos segundos antes de reintentar. Las solicitudes exitosas nunca cuentan para este bloqueo.

```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"
    }
  }
}
```

## Recibir un pago

Desde elegir el token hasta entregar el pedido, con payins creados por la API.

### 1. Elija el token y la red

GET /v1/summary/tokens lista los tokens de la plataforma por red, con min_receipt y max_receipt. La lista no está filtrada por su cuenta: la creación del payin confirma si el token acepta depósitos para usted en esa red.

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

### 2. Cree el payin

POST /v1/payin con un reference_id único en su sistema, el monto (process_by crypto_amount o fiat_amount), webhook_url y, si quiere, expires_at en segundos (4 horas por defecto). Guarde el internal_reference de la respuesta y muestre al cliente la wallet, y el memo cuando el token lo exija.

```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. Reciba el webhook

Verifique X-Bifrost-Signature sobre el cuerpo crudo, responda 2xx en menos de 30 segundos y procese después. El mismo aviso puede llegar más de una vez: elimine duplicados por data.internal_reference junto con event.

### 4. Compruebe el monto antes de entregar

El payin queda completado cuando received_amount alcanza minimum_payment por ciento de crypto_amount, que puede ser menos que el total. Compare data.received_amount con data.crypto_amount y decida qué hacer con la diferencia.

### 5. Elija cuándo entregar

payment_detected significa que la transferencia se vio (funds_available false): entregar ahora es su decisión y conlleva el riesgo de una reversión. payment_confirmed significa que es final (funds_available true).

### 6. Trate las excepciones

payment_reversed: la red deshizo el pago y se retiró el crédito; check-payin pasa a devolver status reversed. is_duplicated true: un pago extra o en otra red generó un payin nuevo con referencias propias; vincúlelo al pedido por customer_info.

### 7. Concilie

GET /v1/check-payin/{internal_reference} devuelve el payin en cualquier momento. GET /v1/statements lista los movimientos del saldo: DEPOSIT para el crédito, RATE y FEE para los cargos.

### Qué hacer cuando algo falla

POST /v1/payin no tiene Idempotency-Key. El reference_id es lo que impide un segundo payin.

| Situación | Qué hacer |
| --- | --- |
| Timeout o sin respuesta en POST /v1/payin | Reenvíe con el mismo reference_id. Si la respuesta es duplicate_reference_id, la primera solicitud funcionó: obténgala con GET /v1/list-payin?reference_id=… |
| 400 | Corrija la solicitud. Reenviarla sin cambios da la misma respuesta. |
| 500, 502 o 503 | No se creó nada. Reintente más tarde con el mismo reference_id. |
| 429 | Espere los segundos indicados en Retry-After. |
| El webhook no llegó | Las entregas se repiten hasta 10 veces, con unos 5 minutos entre ellas. Mientras tanto, consulte el payin con GET /v1/check-payin/{internal_reference}. |

> Los payins creados en el panel no los devuelve la API.

## Enviar un payout

Desde comprobar el saldo hasta confirmar la transferencia, para un payout o un lote de hasta 50.

### 1. Compruebe el saldo

GET /v1/balances devuelve un saldo por token; la clave de cada token es el balance_key que devuelve GET /v1/summary/tokens. Cuando available tenga la clave, use ese valor: el resto está retenido por depósitos aún no finales. min_withdrawal y max_withdrawal por token vienen de GET /v1/summary/tokens.

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

### 2. Cree el payout

POST /v1/payout con un objeto o una lista de hasta 50. Dé a cada payout su propio reference_id y envíe un Idempotency-Key por solicitud. feetakenfromamount 1 descuenta las comisiones del monto; 0 las suma al débito.

```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. Lea el resultado de cada ítem

200 significa que se crearon todos los ítems, 207 que algunos, 400 que ninguno. results tiene una entrada por ítem, en el orden enviado, con su index. Guarde el internal_reference de cada payout creado.

### 4. Reciba el webhook

payment_processed (data.status success, con el hash de la transacción en data.transactions) cuando la transferencia se realiza, o payment_failed cuando no puede completarse. Verifique la firma y elimine duplicados por data.internal_reference junto con event.

### 5. Concilie

GET /v1/check-payout/{internal_reference} devuelve el payout en cualquier momento. GET /v1/statements lista WITHDRAWAL para el débito y RATE y FEE para los cargos.

### Qué hacer cuando algo falla

Dos cosas impiden que pague dos veces: el Idempotency-Key de la solicitud y el reference_id de cada payout.

| Situación | Qué hacer |
| --- | --- |
| Timeout o sin respuesta | Reenvíe el mismo cuerpo con el mismo Idempotency-Key dentro de 24 horas: recibe la respuesta original y no se procesa nada de nuevo. |
| 409 idempotency_key_in_progress | La primera solicitud sigue en curso. Espere y reintente con la misma clave. |
| 207 | Corrija y reenvíe solo los ítems rechazados, con un Idempotency-Key nuevo. Los creados siguen creados. |
| 500 con summary.not_processed | Reenvíe los ítems que no se procesaron. Un reference_id ya usado se rechaza con duplicate_payment_reference, nunca se paga dos veces. |
| duplicate_payment_reference | El payout ya existe. Use el internal_reference y el original_status del resultado. |
| 429 | Espere los segundos indicados en Retry-After. |

> Los payouts creados en el panel no los devuelve la API.

## Verificación de webhooks

Confirme que el aviso lo envió Bifrost antes de confiar en el cuerpo.

### Headers Enviados en Webhooks

Todos los webhooks se envían con headers especiales para validación e identificación:

```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 y X-Bifrost-Delivery acompañan cada entrega. Son ellos los que prueban el origen del mensaje.

### Tipos de webhook

El campo event del cuerpo indica qué ocurrió. Estos son todos los valores que puede tomar:

| event | Descripción |
| --- | --- |
| `payment_processed` | Payout completado. data.status es success. X-Bifrost-Webhook-Type: payment_processed. |
| `payment_failed` | El payout no pudo completarse. data trae solo status (failed), reference_id e internal_reference. X-Bifrost-Webhook-Type: payment_processed. |
| `payment_detected` | Depósito visto en la red; funds_available es false. X-Bifrost-Webhook-Type: payment_confirmed. |
| `payment_confirmed` | Depósito final; funds_available es true. X-Bifrost-Webhook-Type: payment_confirmed. |
| `payment_reversed` | Una reorganización deshizo el depósito y se retiró el crédito. data.status es reversed, y check-payin pasa a devolver status reversed. X-Bifrost-Webhook-Type: payment_confirmed. |
| `fixed_address_pre_revoke · fixed_address_revoked` | Ciclo de vida de la dirección fija, descrito al final de esta página. X-Bifrost-Webhook-Type: fixed_address_lifecycle. |

X-Bifrost-Webhook-Type solo nombra la familia del aviso: todo evento de depósito, incluidos payment_detected y payment_reversed, llega con payment_confirmed en ese encabezado, y un payout fallido con payment_processed. Decida siempre por el campo event del cuerpo.

### Entrega y reintentos

Cada aviso es un POST con cuerpo JSON a la webhook_url que registró. Qué cuenta como entregado, y qué ocurre cuando no lo es:

- Responda con cualquier estado 2xx en menos de 30 segundos (la conexión en sí debe abrirse en menos de 10 segundos). Cualquier otro estado, incluidos 3xx, un timeout o un error de conexión, cuenta como fallo.
- Las redirecciones no se siguen. El certificado TLS se verifica, HTTPS es obligatorio en producción, y la conexión se hace por IPv4 a la dirección validada para su host.
- Una entrega fallida se reintenta unos 5 minutos después, hasta 10 intentos en total. Tras el décimo fallo el aviso queda marcado como failed y puede reenviarse desde el panel.
- Cada intento lleva el mismo cuerpo, un X-Bifrost-Delivery y un X-Bifrost-Timestamp nuevos, y una firma calculada para ellos.
- Por lo tanto, el mismo aviso puede llegarle más de una vez. Deduplique por data.internal_reference junto con event (en los avisos de dirección fija, wallet junto con event).
- Responda primero y procese después: hacer trabajo lento antes de responder es la forma habitual de superar el límite de 30 segundos.

### El Sello de Heimdall

Cómo confirmar que el mensaje vino realmente del puente

La dirección que recibe sus webhooks es pública: cualquiera que descubra la URL puede enviarle una petición. Sin verificación, un POST falsificado anunciando un pago aprobado es indistinguible de uno real, y suele convertirse en mercancía entregada sin que el dinero haya entrado.

#### La clave de firma

Cada cuenta tiene una clave dedicada, generada automáticamente y nunca transmitida. Nosotros usamos nuestra copia para firmar cada entrega; usted obtiene la suya una vez y la guarda. Como no viaja junto con el webhook, una fuga de registros en su entorno no entrega la clave, y eso es lo que hace que la firma pruebe algo.

#### Obtener la clave

La clave ya existe en su cuenta y está en el panel, en Configuración, en el área de webhooks. Mostrarla exige su segundo factor. Copie el valor a su servidor y guárdelo como guardaría una contraseña de base de datos: en una variable de entorno o en un gestor de secretos, nunca en el código versionado.

> El panel también permite cambiar la clave, y el cambio invalida la anterior de inmediato. Entre generar la nueva y actualizar su copia, los webhooks saldrán firmados con una clave que usted aún no tiene, y su verificación fallará en ese intervalo.

#### Cabeceras de la firma

| Campo | Descripción |
| --- | --- |
| `X-Bifrost-Signature` | v1= seguido del HMAC-SHA256 en hexadecimal. |
| `X-Bifrost-Timestamp` | Marca de tiempo Unix de cuándo se emitió el webhook. |
| `X-Bifrost-Delivery` | 32 caracteres hexadecimales en minúscula, únicos para este intento de entrega. Cambia en cada reintento. |

#### La cadena firmada

El HMAC no se calcula sobre el cuerpo crudo, sino sobre esta cadena, unida con saltos de línea entre los campos:

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

La marca de tiempo y el identificador de entrega entran en el cálculo a propósito. Si solo se firmara el cuerpo, esas dos cabeceras podrían ser reescritas por cualquiera sin invalidar la firma, y un webhook capturado hoy se reenviaría meses después pareciendo recién emitido.

#### Verificando

**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)
```

#### Precauciones

- Use el cuerpo crudo. Deserializar el JSON y volver a serializarlo cambia los espacios en blanco y el orden de las claves, y el hash deja de coincidir. Capture los bytes exactamente como llegaron, antes de cualquier parseo.
- Compare en tiempo constante. hash_equals, timingSafeEqual y compare_digest existen para esto: una comparación común de cadenas filtra, por el tiempo de respuesta, cuántos caracteres iniciales son correctos.
- Gestione los reintentos. Una entrega que no recibe 2xx se intenta de nuevo, con un X-Bifrost-Delivery distinto cada vez. El cuerpo no tiene identificador de evento: deduplique por data.internal_reference junto con event, para no procesar dos veces el mismo pago.
- HTTPS es obligatorio en producción. Las URLs http:// se rechazan tanto al registrarlas como al enviarlas.

#### Webhook sin firma

Si no llega el encabezado X-Bifrost-Signature, no procese el mensaje y contacte al soporte.

### Ciclo de vida de la dirección fija

Las direcciones fijas que pasan mucho tiempo sin recibir nada se recuperan. Antes y en el momento en que ocurre, se envía un aviso a la URL de webhook de direcciones fijas configurada en el panel, no a una URL por pago; sin esa URL no se envía ningún aviso. Las direcciones de tipo evm son permanentes y nunca reciben estos eventos. El cuerpo es plano, sin el envoltorio data, y está firmado como cualquier otro webhook.

| event | Descripción |
| --- | --- |
| `fixed_address_pre_revoke` | Tras 120 días sin transacción. revoke_at indica cuándo se revocará la dirección (unos 30 días después), salvo que reciba una transacción antes. |
| `fixed_address_revoked` | Tras 150 días sin transacción. La dirección deja de pertenecer a su cuenta; status es revoked y revoke_at es 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"
}
```

| Campo | Descripción |
| --- | --- |
| `event` | fixed_address_pre_revoke o fixed_address_revoked |
| `wallet` | La dirección fija |
| `network · token` | Red y token de la dirección; token puede ser null |
| `provider` | Reservado. No dependa de él. |
| `customer_info` | El customer_info indicado al pedir la dirección, o null |
| `status` | Estado de la dirección en el momento del aviso (STATUS_ASSIGNED en la prerrevocación), o revoked |
| `last_transaction_at · assigned_at` | Última transacción recibida y fecha de asignación; la inactividad se cuenta desde last_transaction_at, o desde assigned_at cuando la dirección nunca recibió una transacción |
| `revoke_at` | Fecha prevista de revocación en fixed_address_pre_revoke; null en fixed_address_revoked |
| `message` | Explicación legible, en inglés |
| `timestamp` | Cuándo se generó el aviso, en ISO 8601 |

## Webhooks de retiro

Avisos de un payout creado por la API.

### `payment_processed` · Payout Confirmado

Se envía cuando el retiro se procesa. status es success. El cuerpo trae la billetera, los montos, la red y 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 Fallido

Se envía cuando el retiro no puede completarse. El cuerpo trae solo status, reference_id e 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"
}
```

> Su integración debe tratar este aviso: localice el payout por reference_id o internal_reference y decida qué informar a su cliente. Para pagar de nuevo, cree un payout nuevo con un reference_id nuevo.

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

Su propio identificador del cliente, como ID de usuario, e-mail o número de pedido. Se devuelve en los webhooks y listados, así sabe de quién es cada transacción, incluidos los payins que Bifrost crea por su cuenta (pagos adicionales y depósitos en direcciones fijas), cuyo reference_id genera Bifrost.

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

## Webhooks de depósito

Notificaciones de un payin, desde que la transferencia aparece en la red hasta que el monto está disponible en su saldo.

### Pagos parciales

Un depósito se marca como completed, y se envían payment_detected / payment_confirmed, cuando received_amount alcanza minimum_payment por ciento de crypto_amount. minimum_payment se define por cuenta; por debajo de 100, el cliente puede haber pagado menos que la factura. Antes de entregar, compare data.received_amount con data.crypto_amount (ambos en data.crypto_currency) y decida qué hacer con la diferencia.

### `payment_detected` · Pago detectado

La transacción apareció en la red y el payin está completado, pero este no es el estado final. funds_available es false y finality_status es pending_finality mientras el bloque aún pueda reorganizarse. Si necesita agilidad, puede liberar el producto con este aviso. El monto queda retenido y no forma parte del saldo disponible hasta que la red alcance las confirmaciones requeridas.

> Liberar el producto en esta etapa es una decisión suya. Un retiro de ese monto se rechaza hasta el payment_confirmed. Hasta hoy ningún pago detectado por Bifrost ha sido revertido; vea 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` · Pago confirmado

El bloque es final. funds_available es true y el monto está disponible en su saldo. Acredite al cliente de forma definitiva con este evento.

```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` · Pago revertido

Puede estar tranquilo: hasta hoy ningún pago recibido por Bifrost ha sido revertido.

Este aviso existe solo por las redes L2, como Arbitrum, Optimism y Base. En ellas, el pago se detecta en cuanto entra en un bloque de la L2 (payment_detected), en segundos, y se vuelve definitivo cuando ese bloque se finaliza en la red principal (payment_confirmed), unos minutos después. En teoría, una transacción puede deshacerse en ese intervalo. Es rarísimo, pero como la posibilidad existe, preferimos documentarlo.

Si ocurre, el crédito de ese depósito se retira del saldo, data.status viene como reversed y reversal_reason indica el motivo. A partir de ahí, check-payin y list-payin también devuelven status reversed.

Si usted libera el producto solo en payment_confirmed, este aviso no le afecta. Si lo libera en payment_detected, basta con vincular este aviso al pedido por 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"
}
```

### Cuando la detección ya es final

En algunas redes el pago es final en cuanto se detecta. payment_confirmed se envía justo después de payment_detected; payment_detected ya trae finality_status final, y payment_confirmed puede traer finality_status null. Decida por event y funds_available: no todo payment_detected retiene el monto.

### Campos del depósito

| Campo | Tipo | Descripción |
| --- | --- | --- |
| `funds_available` | `boolean` | false mientras el monto está retenido. true cuando puede retirarse. |
| `finality_status` | `string \| null` | pending_finality mientras corren las confirmaciones, final cuando el bloque está consolidado, reorged cuando el depósito se deshizo. Puede ser null en un payment_confirmed de un pago que ya era final al detectarse. |
| `confirmations` | `number \| null` | Reservado para el recuento de confirmaciones. Hoy siempre es null; no dependa de él. |
| `estimated_release_seconds` | `number \| null` | Reservado para la estimación de espera hasta la liberación. Hoy siempre es null; no dependa de él. |
| `reversal_reason` | `string \| null` | Por qué se retiró el crédito. Null en payment_detected y payment_confirmed. |
| `received_amount` | `string` | Monto en cripto acreditado en el recibo. |
| `received_amount_fiat` | `string` | Valor fiat del monto recibido a la cotización de la invoice. |
| `received_amount_fiat_current` | `string` | Valor fiat a la cotización del momento en que se armó el webhook. Cae a la cotización de la invoice si el precio en vivo no está disponible. |
| `cross_token` | `boolean` | true cuando el cliente pagó con un token distinto del token de la invoice. |
| `paid_with` | `array` | Tokens realmente recibidos on-chain. Cada ítem tiene currency y amount. |
| `is_wallet_fixed` | `boolean` | true cuando el depósito cayó en una dirección fija. |

**`is_duplicated`** (Solo Payin)

true cuando Bifrost creó este payin por su cuenta, para un pago que no correspondía a un payin abierto: un pago extra a una dirección cuyo payin ya estaba completado, o un pago en una red distinta de la factura. Es un payin nuevo, con internal_reference propio y un reference_id generado por Bifrost. El monto se acredita en su saldo y el aviso va al webhook_url del payin original.

> Cuando is_duplicated es true, el reference_id no es suyo. Vincule el pago a su pedido por customer_info o wallet. El payin también aparece en List Payins y Check Specific Payin.

**`customer_info`**

Su propio identificador del cliente, como ID de usuario, e-mail o número de pedido. Se devuelve en los webhooks y listados, así sabe de quién es cada transacción, incluidos los payins que Bifrost crea por su cuenta (pagos adicionales y depósitos en direcciones fijas), cuyo reference_id genera Bifrost.

## Códigos de Error

Todos los códigos de error que devuelve la API, agrupados por tipo, con qué hacer en cada caso.

### Formato del error

Todo error de /v1 tiene la misma forma, con el estado HTTP de las tablas siguientes. Compare error.code, que es snake_case estable. error.message es texto libre en inglés para sus logs. error.details solo aparece cuando hay datos útiles, como campos inválidos, límites o scopes.

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

En un lote de POST /v1/payout, cada ítem rechazado trae el mismo objeto en results[].error, junto a su index y reference_id. La respuesta es 200 cuando todos los ítems tuvieron éxito, 207 cuando algunos tuvieron éxito y otros fallaron, y 400 cuando ninguno tuvo éxito:

```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"
      }
    }
  ]
}
```

- Los mensajes de validación están siempre en error.details.fields, indexados por nombre de campo, tanto en el nivel superior como dentro de un ítem del lote.
- Un ítem de payout duplicado cuenta como fallo: trae error.code duplicate_payment_reference más duplicate, original_status y original_created_at en el propio ítem. Un lote compuesto solo de duplicados responde 400.
- Decida por error.code, nunca por error.message: los mensajes pueden reescribirse, los códigos no cambian.
- 4xx significa que la solicitud debe cambiar antes de reenviarse. 5xx significa que el fallo fue de nuestro lado y la misma solicitud puede repetirse; en POST /v1/payout, el reference_id sigue protegiéndole contra un retiro duplicado.
- Una ruta /v1 desconocida devuelve 404 route_not_found en JSON. Un cliente que pide text/html en Accept recibe una página HTML.

### Errores de Autenticación y Acceso

| Código HTTP | Error | Descripción |
| --- | --- | --- |
| `401` | `api_key_missing` | No se envió el encabezado X-Bifrost-Invoke. |
| `401` | `api_key_invalid` | API Key desconocida, revocada o expirada. También se devuelve mientras la cuenta está suspendida. |
| `403` | `api_access_denied` | El acceso a la API está desactivado en esta cuenta. Se verifica en cada solicitud. |
| `403` | `email_not_verified` | El e-mail de la cuenta aún no ha sido verificado. |
| `403` | `ip_not_authorized` | La IP de origen no está en la whitelist de la clave. |
| `400` | `invalid_ip` | No se pudo determinar la IP del cliente. |
| `401` | `signature_headers_missing` | La clave exige firma y falta X-Bifrost-Signature, X-Bifrost-Timestamp o X-Bifrost-Nonce. |
| `401` | `signature_timestamp_invalid` | X-Bifrost-Timestamp no está compuesto solo de dígitos (segundos Unix). |
| `401` | `signature_timestamp_out_of_range` | El timestamp difiere de la hora del servidor en más de 300 segundos. |
| `401` | `signature_nonce_length_invalid` | El nonce tiene menos de 16 o más de 80 caracteres. |
| `401` | `signature_nonce_charset_invalid` | El nonce tiene caracteres fuera de A-Z, a-z, 0-9, guion bajo y guion. |
| `401` | `signature_invalid` | La firma no coincide con la cadena canónica. |
| `401` | `signature_replayed` | Este nonce ya fue usado por esta clave. |
| `401` | `signature_secret_unavailable` | La clave exige firma pero no tiene un secreto utilizable. Contacte al soporte. |
| `403` | `scope_denied` | La clave no tiene el alcance que exige este endpoint. details.required_scopes lista los que sirven y details.granted_scopes los que tiene la clave. |
| `429` | `rate_limit_exceeded` | La cuota de la clave en la ventana actual se agotó. Retry-After y details.retry_after_seconds indican cuándo reintentar. |
| `429` | `too_many_failed_authentications` | Esta IP está bloqueada temporalmente tras fallos de autenticación repetidos. Llega con Retry-After. |
| `401` | `user_not_authenticated` | No se pudo asociar la solicitud a su clave de API. Reintente; si se repite, contacte al soporte. |

### Errores de la Solicitud

| Código HTTP | Error | Descripción |
| --- | --- | --- |
| `404` | `route_not_found` | Ninguna ruta /v1 corresponde a la ruta y al método. Se responde antes de la autenticación. |
| `400` | `json_parse_exception` | El cuerpo de la solicitud no es JSON válido. Cualquier ruta /v1. |
| `400` | `json_parse_failed` | POST /v1/payout: el cuerpo se decodificó como null o false. |
| `400` | `invalid_parameters` | Parámetros inválidos o ausentes. details.fields asocia cada campo a su mensaje (POST /v1/payout con todos los ítems inválidos, POST /v1/payin, direcciones fijas, filtros de extractos). |
| `400` | `invalid_payment_list` | POST /v1/payout: el cuerpo está vacío o no es un objeto de payout ni una lista de payouts. |
| `400` | `max_payouts_exceeded` | POST /v1/payout: más de 50 payouts en una solicitud. |
| `409` | `idempotency_key_in_progress` | Una solicitud con esta Idempotency-Key aún está en curso. Espere y reintente con la misma clave. |
| `400` | `crypto_amount_required` | POST /v1/payin: process_by es crypto_amount y falta crypto_amount. |
| `400` | `fiat_amount_required` | POST /v1/payin: process_by es fiat_amount y falta fiat_amount. |
| `400` | `fiat_currency_required` | POST /v1/payin: process_by es fiat_amount y falta fiat_currency. |
| `400` | `invalid_page_number` | list-payout / list-payin: page menor que 1. |
| `400` | `invalid_per_page_limit` | list-payout / list-payin: per_page fuera de 1 a 100. |
| `400` | `invalid_status_filter` | list-payout / list-payin: valor de status desconocido. |
| `400` | `invalid_network_filter` | list-payout / list-payin: red desconocida. |
| `400` | `invalid_date_from` | list-payout / list-payin: date_from no es una fecha. |
| `400` | `invalid_date_to` | list-payout / list-payin: date_to no es una fecha. |
| `400` | `invalid_reference_id` | check-payout / check-payin: referencia de menos de 10 o más de 756 caracteres. Extractos: reference_id de más de 255. |
| `400` | `invalid_pagination` | Extractos: page o per_page fuera de rango. |
| `400` | `invalid_format` | Extractos: date_from o date_to no es una fecha (details.field). |
| `400` | `invalid_date_range` | Extractos: date_from es posterior a date_to. |
| `400` | `date_range_too_wide` | Extractos: el rango es más amplio de lo permitido. details.max_days indica el tope. |
| `400` | `invalid_amount` | Extractos: amount_from o amount_to inválido, o amount_from mayor que amount_to. |
| `400` | `missing_parameters` | get-price: falta currency1 o currency2. |
| `400` | `invalid_currency` | get-price: currency1 o currency2 no es un código de moneda válido. |
| `400` | `invalid_webhook_url` | PUT /v1/fixed-wallets/config: webhook_url apunta a un destino no permitido. |

### Errores de Negocio

| Código HTTP | Error | Descripción |
| --- | --- | --- |
| `400` | `insufficient_total_balance` | POST /v1/payout: los ítems de una moneda suman más que el saldo disponible. No se debita nada. details trae crypto_currency, available y required. |
| `404` | `user_not_found` | POST /v1/payout: la configuración de la cuenta está incompleta. Contacte al soporte. |
| `404` | `record_not_found` | El payout o payin consultado por check-payout / check-payin no existe en esta cuenta. También se devuelve cuando falta un dato de la cuenta que la solicitud necesita. |
| `400` | `duplicate_reference_id` | POST /v1/payin: ya existe un payin con este reference_id en la cuenta. |
| `400` | `invalid_network_or_deposits_disabled` | POST /v1/payin: red desconocida, o los depósitos están desactivados en ella. |
| `400` | `unsupported_token_or_deposits_disabled` | POST /v1/payin: token no soportado, o los depósitos están desactivados para él. |
| `400` | `token_network_mismatch` | POST /v1/payin: el token no está disponible en esta red. |
| `400` | `memo_required` | POST /v1/payin: este token exige memo. |
| `400` | `amount_out_of_limits` | POST /v1/payin: el monto en cripto está fuera de los límites del token. details trae min_allowed y max_allowed. |
| `400` | `value_below_minimum` | POST /v1/payin con fiat_amount: el monto convertido está por debajo del mínimo. |
| `400` | `value_above_maximum` | POST /v1/payin con fiat_amount: el monto convertido está por encima del máximo. |
| `400` | `token_not_supported_or_inactive` | POST /v1/payin con fiat_amount: el token no es soportado o está inactivo para conversión. |
| `400` | `crypto_amount_does_not_correspond_to_fiat_amount_provided` | POST /v1/payin: se enviaron crypto_amount y fiat_amount y no coinciden con la cotización actual. |
| `400` | `invalid_parameters_for_process_by` | POST /v1/payin: los montos enviados no encajan con el process_by elegido. |
| `404` | `price_not_found` | get-price: no hay cotización para este par de monedas. |
| `403` | `feature_disabled` | Las direcciones fijas no están habilitadas para la cuenta (config, actualización de config y creación). |
| `422` | `type_not_supported` | POST /v1/fixed-wallets: este type no está disponible para direcciones fijas. |
| `403` | `limit_not_allowed` | POST /v1/fixed-wallets: el límite de la cuenta para este type es cero. |
| `409` | `limit_reached` | POST /v1/fixed-wallets: la cuenta ya tiene el número máximo de direcciones fijas. |
| `409` | `no_wallet_available` | POST /v1/fixed-wallets: no hay ninguna dirección de ese tipo disponible ahora. Reintente más tarde. |

### Errores por Ítem del Lote de Payout (results[].error)

| Código HTTP | Error | Descripción |
| --- | --- | --- |
| `207/400` | `invalid_parameters` | El ítem no pasó la validación. error.details.fields asocia cada campo a su mensaje. |
| `207/400` | `missing_required_fields` | Falta un campo obligatorio del ítem. |
| `207/400` | `duplicate_reference_id_in_request` | El mismo reference_id aparece antes en el mismo lote. |
| `207/400` | `duplicate_payment_reference` | El reference_id ya se usó en esta cuenta. El ítem trae duplicate, original_status, original_created_at y el internal_reference original; no se hace una segunda transferencia. |
| `207/400` | `invalid_crypto_amount` | process_by es crypto_amount y crypto_amount falta o es inválido. |
| `207/400` | `invalid_fiat_amount` | process_by es fiat_amount y fiat_amount falta o es inválido. |
| `207/400` | `fiat_currency_required` | process_by es fiat_amount y falta fiat_currency. |
| `207/400` | `fiat_currency_not_accepted` | La fiat_currency no es aceptada. |
| `207/400` | `memo_required` | Este token exige memo. |
| `207/400` | `invalid_network_or_withdrawals_disabled` | Red desconocida, o los retiros están desactivados en ella. |
| `207/400` | `unsupported_token_or_withdrawals_disabled` | Token no soportado, o los retiros están desactivados para él. |
| `207/400` | `token_network_mismatch` | El token no está disponible en esta red. |
| `207/400` | `token_not_active` | El token no está activo para retiros o para conversión. |
| `207/400` | `invalid_format` | La dirección de la wallet no es válida para la red. |
| `207/400` | `limit_exceeded` | El monto está fuera del mínimo y del máximo del token (también cuando el monto fiat convertido queda fuera). |
| `207/400` | `amount_does_not_cover_fees` | Las comisiones consumen todo el monto, y el destinatario no recibiría nada. |
| `207/400` | `crypto_amount_does_not_correspond_to_fiat_amount_provided` | crypto_amount y fiat_amount no coinciden con la cotización actual. |
| `207/400` | `price_unavailable` | Sin cotización para convertir el monto, o la clave tiene tope diario y el token no tiene cotización en USD para contabilizar el retiro. |
| `207/400` | `insufficient_balance` | El saldo disponible no cubre este ítem. |
| `207/400` | `daily_payout_limit_exceeded` | El ítem superaría el tope diario configurado en la clave. |
| `207/400` | `daily_payout_limit_unavailable` | La clave tiene tope diario pero no se pudo registrar el consumo. Reenvíe el ítem. |
| `207/400` | `error_fetching_active_tokens · error_fetching_active_currencies` | No se pudo cargar el catálogo de tokens o monedas. Reenvíe el ítem. |
| `207/400` | `user_not_found · record_not_found` | Falta un dato de la cuenta que el retiro necesita. |
| `207/400` | `internal_error · database_error · transaction_failed · network_unavailable · duplicate_record` | El registro del retiro falló de nuestro lado y se revirtió. No se debitó nada por este ítem; verifíquelo con check-payout o list-payout antes de reenviar. |

### Errores de Sistema

| Código HTTP | Error | Descripción |
| --- | --- | --- |
| `500` | `internal_error` | Fallo inesperado. En POST /v1/payout, un fallo a mitad del lote también devuelve summary (con not_processed) y results en el nivel superior, para que reenvíe solo lo que falta. |
| `500` | `database_error` | Extractos: la consulta falló. |
| `500` | `error_fetching_active_currencies` | POST /v1/payout y POST /v1/payin: no se pudo cargar el catálogo de tokens o fiats. No se procesó nada. |
| `500` | `token_type_not_found · receipt_creation_failed · database_transaction_failed` | POST /v1/payin: el payin no pudo crearse de nuestro lado. |
| `502` | `external_processor_error` | POST /v1/payin y POST /v1/fixed-wallets: la dirección de depósito no pudo crearse ahora. No se creó nada; reintente más tarde. |
| `503` | `no_processor_configured · no_wallet_available` | POST /v1/payin: no hay procesador ni dirección de depósito disponible para este token y red ahora. Reintente más tarde. |
| `503` | `price_not_available_for_conversion · price_not_available_for_validation` | POST /v1/payin: no hay cotización disponible para convertir o verificar el monto. Reintente más tarde. |
| `503` | `service_unavailable` | POST /v1/payin y endpoints de summary: una dependencia no está disponible temporalmente. |
| `500` | `config_update_failed` | PUT /v1/fixed-wallets/config: la configuración no pudo guardarse. |
| `500` | `assign_failed` | POST /v1/fixed-wallets: la dirección no pudo asignarse. |

### Mejores Prácticas

#### Seguridad

**Hacer**

- Almacena tu API Key en variables de entorno
- Configura IP Whitelist
- Verifique el X-Bifrost-Signature de cada webhook
- Usa HTTPS en todas las solicitudes
- Implementa timeouts en las solicitudes

**No Hacer**

- Compartir API Key públicamente
- Hacer commit de API Keys en Git
- Usar la misma API Key en múltiples entornos

#### Rendimiento

- Los datos de extracto y de resumen pueden tener hasta 5 minutos de retraso; no los consulte con más frecuencia
- Implementa paginación adecuada
- Evita solicitudes excesivas (respeta el rate limit)
- Procesa webhooks de forma asíncrona

#### Idempotencia

- Use un reference_id único y significativo: es el cerrojo contra el pago duplicado, y volver a presentarlo devuelve el pago original en vez de crear otro
- Envíe Idempotency-Key en los lotes: si se cae la conexión, repetir con la misma clave devuelve la respuesta original en vez de reprocesar
- POST /v1/payin no tiene Idempotency-Key: tras un timeout reenvíe con el mismo reference_id y, si la respuesta es duplicate_reference_id, obtenga el payin con GET /v1/list-payin?reference_id=…
- Trate 207 y 500 leyendo results: ambos traen lo que ya se creó, para que reenvíe solo lo que faltó
- Gestione los reintentos de webhook deduplicando por data.internal_reference junto con event, para no procesar dos veces el mismo aviso

---

> La referencia de la API está en inglés. Las guías, la autenticación, los webhooks y los códigos de error están en español.

## Referencia de la API · 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`

Alcance requerido: `payout:write` (una clave nueva sin lista es de solo lectura)

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. |

#### Otras respuestas

Respuestas que no son el camino feliz y aun así necesitan tratamiento en su integración.

##### 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`

Alcance requerido: `payout:write` (una clave nueva sin lista es de solo lectura)

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. |

#### Otras respuestas

Respuestas que no son el camino feliz y aun así necesitan tratamiento en su integración.

##### 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`

Alcance requerido: `payout:read` (una clave nueva sin lista es de solo lectura)

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. |

#### Otras respuestas

Respuestas que no son el camino feliz y aun así necesitan tratamiento en su integración.

##### 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}`

Alcance requerido: `payout:read` (una clave nueva sin lista es de solo lectura)

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. |

#### Otras respuestas

Respuestas que no son el camino feliz y aun así necesitan tratamiento en su integración.

##### 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"
  }
}
```

## Referencia de la API · 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`

Alcance requerido: `payin:write` (una clave nueva sin lista es de solo lectura)

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. |

#### Otras respuestas

Respuestas que no son el camino feliz y aun así necesitan tratamiento en su integración.

##### 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`

Alcance requerido: `payin:read` (una clave nueva sin lista es de solo lectura)

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. |

#### Otras respuestas

Respuestas que no son el camino feliz y aun así necesitan tratamiento en su integración.

##### 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}`

Alcance requerido: `payin:read` (una clave nueva sin lista es de solo lectura)

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. |

#### Otras respuestas

Respuestas que no son el camino feliz y aun así necesitan tratamiento en su integración.

##### 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"
    }
  }
}
```

## Referencia de la API · Queries

Endpoints for querying balances, statements, and account information.

### Get Balances

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

Alcance requerido: `balances:read` (una clave nueva sin lista es de solo lectura)

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`

Alcance requerido: `statements:read` (una clave nueva sin lista es de solo lectura)

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. |

#### Otras respuestas

Respuestas que no son el camino feliz y aun así necesitan tratamiento en su integración.

##### 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`

Alcance requerido: `statements:read` (una clave nueva sin lista es de solo lectura)

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. |

#### Otras respuestas

Respuestas que no son el camino feliz y aun así necesitan tratamiento en su integración.

##### 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`

Alcance requerido: `statements:read` (una clave nueva sin lista es de solo lectura)

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. |

#### Otras respuestas

Respuestas que no son el camino feliz y aun así necesitan tratamiento en su integración.

##### 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`

Alcance requerido: `prices:read` o `statements:read` (basta con cualquiera de ellos)

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`

Alcance requerido: `prices:read` o `statements:read` (basta con cualquiera de ellos)

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. |

## Referencia de la API · 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`

Alcance requerido: `prices:read` (una clave nueva sin lista es de solo lectura)

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. |

#### Otras respuestas

Respuestas que no son el camino feliz y aun así necesitan tratamiento en su integración.

##### 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`

Alcance requerido: `prices:read` (una clave nueva sin lista es de solo lectura)

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. |

## Referencia de la API · 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`

Alcance requerido: `wallets:read` (una clave nueva sin lista es de solo lectura)

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`

Alcance requerido: `wallets:read` (una clave nueva sin lista es de solo lectura)

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. |

#### Otras respuestas

Respuestas que no son el camino feliz y aun así necesitan tratamiento en su integración.

##### 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`

Alcance requerido: `wallets:write` (una clave nueva sin lista es de solo lectura)

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. |

#### Otras respuestas

Respuestas que no son el camino feliz y aun así necesitan tratamiento en su integración.

##### 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`

Alcance requerido: `wallets:read` (una clave nueva sin lista es de solo lectura)

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.

#### Otras respuestas

Respuestas que no son el camino feliz y aun así necesitan tratamiento en su integración.

##### 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`

Alcance requerido: `wallets:write` (una clave nueva sin lista es de solo lectura)

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. |

#### Otras respuestas

Respuestas que no son el camino feliz y aun así necesitan tratamiento en su integración.

##### 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.

---

## Recetas de integración (paso a paso para agentes)

### Recibir un pago (payin)

1. Lea la clave de la API, y el secreto de firma cuando la clave lo exija, de variables de entorno, nunca del código.
2. Llame a `GET /v1/summary/tokens` para elegir el token y la red (`min_receipt`, `max_receipt`). La lista no está filtrada por su cuenta: la creación confirma si el token acepta depósitos para usted.
3. Llame a `POST /v1/payin` con el id del pedido como `reference_id`, `crypto_currency`, `network`, `process_by` y el monto correspondiente como cadena decimal, `webhook_url` y `customer_info`. Guarde `data.internal_reference` y muestre al cliente `data.wallet`, y el memo cuando el token lo exija.
4. En el endpoint de webhook, lea el cuerpo sin procesar, verifique `X-Bifrost-Signature`, responda 2xx de inmediato y procese después. Descarte los avisos ya procesados por `data.internal_reference` junto con `event`.
5. En `payment_confirmed`, compare `data.received_amount` con `data.crypto_amount` y entregue. Entregar en `payment_detected` es su decisión y corre el riesgo de `payment_reversed`. Cuando `is_duplicated` sea true, vincule el pago al pedido por `customer_info`.
6. Tras un timeout en la creación, reenvíe con el mismo `reference_id`; ante `duplicate_reference_id`, busque el payin con `GET /v1/list-payin?reference_id=…`. Decida cada error por `error.code`.
7. Concilie con `GET /v1/check-payin/{internal_reference}`.

### Enviar payouts

1. Lea la clave de la API, y el secreto de firma cuando la clave lo exija, de variables de entorno, nunca del código.
2. Consulte `GET /v1/balances` (use `available` cuando tenga la clave del token) y `min_withdrawal` y `max_withdrawal` en `GET /v1/summary/tokens`.
3. Llame a `POST /v1/payout` con una lista de hasta 50 payouts, cada uno con su propio `reference_id` estable, montos como cadena decimal y `feetakenfromamount` 0 o 1, y envíe un `Idempotency-Key` por solicitud.
4. Lea `results[]` por `index`: guarde el `internal_reference` de los ítems creados; en los rechazados, decida por `results[].error.code` y reenvíe solo esos, con un Idempotency-Key nuevo.
5. Tras un timeout, reenvíe el mismo cuerpo con el mismo Idempotency-Key dentro de las 24 horas. Ante 409 `idempotency_key_in_progress`, espere y repita con la misma clave. Ante 500, reenvíe solo los ítems contados en `summary.not_processed`.
6. Atienda los webhooks `payment_processed` y `payment_failed`, verificando la firma y deduplicando por `data.internal_reference` junto con `event`. Para pagar de nuevo tras una falla, cree un payout nuevo con un `reference_id` nuevo.
7. Concilie con `GET /v1/check-payout/{internal_reference}`.

### Dirección fija por cliente

1. Las direcciones fijas deben estar habilitadas para la cuenta; si no, crear una dirección o leer o cambiar la configuración responde 403 `feature_disabled`. `GET /v1/fixed-wallets/supported` lista los tipos y `GET /v1/fixed-wallets/config` los límites y la URL de webhook.
2. Defina la URL de webhook de dirección fija en el panel o con `PUT /v1/fixed-wallets/config`. Sin ella no se envía ningún aviso.
3. Llame a `POST /v1/fixed-wallets` con `type` y el id de su cliente en `customer_info`: 201 para una dirección nueva, 200 `fixed_wallets_already_assigned` cuando ese cliente ya tiene una del tipo. Una dirección sirve para todas las redes de su familia.
4. Cada transferencia a la dirección crea un payin ya completado y envía `payment_detected` / `payment_confirmed` a la URL de dirección fija, con `is_wallet_fixed` true y un `reference_id` generado. Vincúlelo al cliente por `customer_info` o por `wallet`.
5. Atienda `fixed_address_pre_revoke` (120 días sin transacciones) y `fixed_address_revoked` (150 días). Las direcciones de tipo `evm` son permanentes.
6. Use `GET /v1/fixed-wallets` para listar las direcciones y leer `allowance` antes de crear más.
