# API do Bifrost Crypto v1

> Versão em Markdown da documentação pública da API v1 da Bifrost Crypto, para agentes de IA e geradores de código. Mesmo conteúdo das páginas HTML, gerado da mesma fonte, sem navegação nem estilo.
>
> Versão HTML: https://bifrostcrypto.com/pt-BR/docs
> Arquivos relacionados: [ai.md](https://bifrostcrypto.com/ai/pt-BR.md) · [Postman](https://bifrostcrypto.com/postman-collection.json) · [OpenAPI](https://bifrostcrypto.com/openapi.json) · [llms.txt](https://bifrostcrypto.com/llms.txt)
> Outros idiomas: [English](https://bifrostcrypto.com/ai.md) · [Español](https://bifrostcrypto.com/ai/es.md) · [Français](https://bifrostcrypto.com/ai/fr.md)
> API v1 · contrato alterado pela última vez em 2026-10-10

## Resumo rápido para agentes

- URL base `https://bridge.bifrostcrypto.com`. Todo caminho começa com `/v1`. Corpos de requisição e resposta em JSON (`Content-Type: application/json`).
- Envie a chave da API em `X-Bifrost-Invoke` em toda requisição. Não há login nem token a renovar. Sem a chave: 401 `api_key_missing`; chave desconhecida, revogada ou expirada: 401 `api_key_invalid`.
- Uma chave pode ser criada com assinatura de requisição. As requisições dela levam também `X-Bifrost-Timestamp` (segundos Unix), `X-Bifrost-Nonce` (16 a 80 caracteres, aceito uma vez) e `X-Bifrost-Signature`: o HMAC-SHA256 em hexadecimal, com o segredo de assinatura, de METHOD, PATH (com `/v1`), a QUERY crua, TIMESTAMP, NONCE e o SHA-256 em hexadecimal dos bytes exatos do corpo, unidos por `\n`. Assine os bytes que você envia e nunca serialize o corpo de novo depois de assinar.
- Cada chave carrega escopos. Uma chamada fora deles devolve 403 `scope_denied` com `details.required_scopes`. Chave criada sem lista de escopos é somente leitura.
- Envie `crypto_amount` e `fiat_amount` como strings decimais simples, como `"100.50"`: dígitos e um ponto opcional, sem notação científica, sinal ou separador de milhar. Os valores nas respostas são strings com número de casas variável: converta com uma biblioteca decimal, não como float.
- Exceção: `price` em `GET /v1/get-price` usa vírgula como separador decimal e ponto como separador de milhar (`"65.432,10"`). Retire os pontos e troque a vírgula por ponto antes de converter.
- Campos de data e hora vêm como `YYYY-MM-DD HH:MM:SS` em UTC−03:00, sem offset no valor, e os filtros de data são lidos no mesmo offset. Exceções: o limite diário de payout de uma chave reinicia às 00:00 UTC, e os webhooks de endereço fixo enviam `timestamp` em ISO 8601 com o offset.
- `network` recebe os nomes canônicos listados em Supported Networks (`Ethereum`, `BinanceSmartChain`, `Tron`…). Os endpoints de criação também aceitam apelidos (`erc20`, `bsc`, `trc20`…); respostas, webhooks e filtros de listagem usam sempre o nome canônico, e os filtros diferenciam maiúsculas. Os tokens de cada rede vêm de `GET /v1/summary/tokens`: não fixe no código.
- `GET /v1/balances` tem uma chave por token (`balance_<nome>`), compartilhada por todas as redes; a chave de cada token é o `balance_key` de `GET /v1/summary/tokens`. Percorra as chaves em vez de fixá-las no código. Quando `available` tiver a chave, use-o: o resto está retido por depósitos ainda não finais.
- `POST /v1/payout` aceita um objeto ou uma lista de até 50 e sempre responde com `summary` e `results[]`, uma entrada por item na ordem enviada: 200 quando todos foram criados, 207 quando alguns foram recusados, 400 quando nenhum foi criado. Item recusado traz `results[].error.code`. Dê a cada payout o seu próprio `reference_id` (um repetido é recusado com `duplicate_payment_reference`, nunca pago duas vezes) e envie um `Idempotency-Key` por requisição (guardado por 24 horas).
- `POST /v1/payin` não tem Idempotency-Key: o `reference_id` único é o que impede um segundo payin. Depois de um timeout, reenvie com o mesmo `reference_id`; `duplicate_reference_id` significa que o primeiro foi criado, e `GET /v1/list-payin?reference_id=…` o devolve.
- Guarde o `internal_reference` devolvido em toda criação: os endpoints de consulta o recebem no caminho, e os webhooks o trazem.
- Um payin fica `completed` quando `received_amount` atinge `minimum_payment` por cento de `crypto_amount`, o que pode ser menos que o valor inteiro. Compare `received_amount` com `crypto_amount` antes de entregar.
- Todo erro tem `error.code`, `error.message` e, quando útil, `error.details`. Decida pelo `error.code`, nunca pela mensagem. 4xx: mude a requisição antes de reenviar. 5xx: a mesma requisição pode ser repetida. 429: espere os segundos de `Retry-After`.
- Webhooks são POSTs com corpo JSON para o seu `webhook_url`. Antes de confiar no corpo, valide `X-Bifrost-Signature`: `v1=` mais o HMAC-SHA256 em hexadecimal de `v1`, `X-Bifrost-Timestamp`, `X-Bifrost-Delivery` e o SHA-256 em hexadecimal do corpo cru, unidos por `\n`, com a chave de webhook da conta (no painel). Recuse timestamp com mais de 300 segundos de diferença, compare em tempo constante, responda 2xx em até 30 segundos e processe depois.
- Decida pelo campo `event` do corpo, não por `X-Bifrost-Webhook-Type`. Payouts: `payment_processed`, `payment_failed`. Payins: `payment_detected` (`funds_available` false, ainda não final), `payment_confirmed` (final, saldo disponível), `payment_reversed`. O mesmo aviso pode chegar mais de uma vez: deduplique por `data.internal_reference` junto com `event`.
- Os endpoints de listagem e consulta devolvem só payouts e payins criados pela API; os feitos no painel não são devolvidos.
- Cada chave tem seu próprio limite de requisições, 500 a cada 60 segundos por padrão, informado em `X-RateLimit-Limit`, `X-RateLimit-Remaining` e `X-RateLimit-Reset`.
- Não há sandbox público: toda chamada age sobre a conta real da chave. Teste com valores pequenos.
- Mudanças no contrato, inclusive as que quebram integração, estão no Changelog, da mais recente para a mais antiga.

### Endpoints

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

## Conteúdo

- API do Bifrost Crypto: https://bifrostcrypto.com/ai/pt-BR/introduction.md
- Autenticação e Controle de Acesso: https://bifrostcrypto.com/ai/pt-BR/authentication.md
- Receber um pagamento: https://bifrostcrypto.com/ai/pt-BR/guide-payin.md
- Enviar um payout: https://bifrostcrypto.com/ai/pt-BR/guide-payout.md
- Verificação de webhooks: https://bifrostcrypto.com/ai/pt-BR/webhooks.md
- Webhooks de saque: https://bifrostcrypto.com/ai/pt-BR/webhooks-payments.md
- Webhooks de depósito: https://bifrostcrypto.com/ai/pt-BR/webhooks-receipts.md
- Códigos de Erro: https://bifrostcrypto.com/ai/pt-BR/error-codes.md
- Referência da API · Payments Payout
- Referência da API · Deposits Payin
- Referência da API · Queries
- Referência da API · Prices
- Referência da API · Fixed Wallets
- Changelog: https://bifrostcrypto.com/ai/pt-BR/changelog.md
- Receitas de integração (passo a passo para agentes)

---

## API do Bifrost Crypto

Referência completa da API Bifrost Crypto V1. Integre pagamentos cripto multi-chain na sua plataforma em minutos.

### Autenticação

Todas as rotas requerem o header com sua chave API privada: `X-Bifrost-Invoke`

### URL Base

Todas as requisições são feitas para: `https://bridge.bifrostcrypto.com`

### Formato

Todos os corpos de requisição e resposta usam formato JSON: `Content-Type: application/json`

### Início Rápido

Cada requisição da API deve incluir sua chave privada no header:

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

### Especificação OpenAPI

A referência completa num arquivo OpenAPI 3.1. Importe no Postman ou no Insomnia para testar os endpoints, ou gere um cliente na sua linguagem. Ela é gerada da mesma fonte desta documentação.

https://bifrostcrypto.com/openapi.json

### Datas e horários

Campos de data e hora (created_at, updated_at, expires_at e os demais) vêm como YYYY-MM-DD HH:MM:SS em UTC−03:00, sem o offset no valor. Os filtros de data (date_from, date_to) são lidos no mesmo offset. Duas exceções: o limite diário de payout de uma chave de API reinicia às 00:00 UTC, e os webhooks de endereço fixo enviam timestamp em ISO 8601 com o offset incluído.

### Redes Suportadas

Valores aceitos no campo network, sem diferenciar maiúsculas na criação. Na criação também são aceitos apelidos comuns (erc20, bsc, trc20, matic…), convertidos para estes nomes; veja Network aliases nos endpoints de criação. Respostas, webhooks e filtros de listagem usam sempre os nomes abaixo. Quais tokens estão disponíveis em cada rede é informado por 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 Status HTTP

| Código | Significado |
| --- | --- |
| `200` | Sucesso |
| `201` | Criado - endereço fixo atribuído |
| `207` | Multi-Status - lote de payout com itens recusados |
| `400` | Requisição Inválida - parâmetros inválidos |
| `401` | Não Autorizado - chave de API ou assinatura ausente ou inválida |
| `403` | Proibido - sem permissão para este recurso |
| `404` | Não Encontrado - recurso ou rota inexistente |
| `409` | Conflito - pedido ainda em andamento ou limite atingido |
| `422` | Não Processável - type não suportado |
| `429` | Muitas Requisições - cota ou limite de falhas de autenticação |
| `500` | Erro do Servidor |
| `502` | Bad Gateway - um serviço do qual dependemos falhou. Tente novamente mais tarde. |
| `503` | Serviço Indisponível - tente mais tarde |

### Categorias da API

- **Pagamentos (Payout)** (3 endpoints): Crie e gerencie saques cripto. Até 50 por lote.
- **Depósitos (Payin)** (3 endpoints): Gere carteiras de depósito e rastreie pagamentos recebidos.
- **Consultas** (6 endpoints): Verifique saldos, extratos, tokens e informações da conta.
- **Preços** (2 endpoints): Obtenha taxas de conversão em tempo real e moedas disponíveis.
- **Endereços Fixos** (5 endpoints): Atribua e gerencie endereços de depósito permanentes por cliente.

## Autenticação e Controle de Acesso

Toda requisição /v1 é autenticada por chave de API. A chave pode ainda ser restringida por escopo, por IP de origem e por assinatura da requisição.

### Chave de API

Envie sua chave no cabeçalho X-Bifrost-Invoke em toda requisição. Não há etapa de login nem token a renovar: a chave é a credencial.

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

O acesso é reconferido a cada requisição. Revogar a chave, deixá-la expirar ou ter o acesso à API desativado na conta vale já na requisição seguinte, sem esperar cache nenhum vencer.

- Cabeçalho ausente: 401 api_key_missing. Chave desconhecida, revogada ou expirada: 401 api_key_invalid.
- Acesso à API desativado na conta: 403 api_access_denied.
- E-mail da conta não verificado: 403 email_not_verified. Verifique o e-mail no painel; a chave passa a funcionar na requisição seguinte.
- Conta suspensa: as chaves são mantidas, mas respondem 401 api_key_invalid enquanto durar a suspensão, e voltam a funcionar quando a conta é reativada.

### Escopos

Escopo é a permissão de alcançar um grupo de endpoints. Ao criar uma chave você escolhe quais escopos ela carrega, de modo que uma chave usada só para ler saldos não consiga criar um saque nem se vazar.

> Uma chave criada sem lista de escopos é somente leitura: payout:read, payin:read, balances:read, statements:read e prices:read. Ela não tem escopo de escrita nem de endereços fixos.

| Escopo | Dá acesso a |
| --- | --- |
| `payout:read` | Listar saques e consultar o status de um saque |
| `payout:write` | Criar saques |
| `payin:read` | Listar depósitos e consultar o status de um depósito |
| `payin:write` | Criar depósitos |
| `balances:read` | Ler os saldos da conta |
| `statements:read` | Ler extratos, resumos, tokens aceitos e moedas fiduciárias aceitas |
| `prices:read` | Ler cotações e o catálogo de moedas |
| `wallets:read` | Listar carteiras fixas e ler a configuração delas |
| `wallets:write` | Criar carteiras fixas e alterar a configuração delas |

Chamar um endpoint fora dos escopos da chave devolve 403 informando quais escopos serviriam, para que a correção fique visível sem adivinhação. Alguns endpoints aceitam mais de um, e ter qualquer um deles basta:

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

Nas chaves com escopo de escrita (payout:write, payin:write, wallets:write) vale somar uma segunda camada ao escopo: whitelist de IP, se a sua integração chama de endereço fixo, ou assinatura da requisição, se não chama. Nenhuma das duas é obrigatória, e as duas existem porque uma credencial que movimenta dinheiro merece mais que um segredo estático.

### Whitelist de IP

> A whitelist é opcional, em qualquer escopo. Sem ela, a chave é aceita a partir de qualquer origem, e nada do que você já roda precisa mudar. Ela é uma camada a mais para quem chama de endereço fixo; se a sua integração não tem IP fixo, use require_signature, que protege a chave vazada sem depender da origem.

A chave pode ser restringida a um conjunto de endereços de origem. São aceitos endereços individuais e faixas em notação CIDR, tanto IPv4 quanto IPv6:

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

Uma requisição chega por uma família de endereços de cada vez. Se sua infraestrutura nos alcança por IPv4 e por IPv6, declare as duas, porque uma entrada de uma família não autoriza a outra. Requisições de fora da lista são recusadas com 403 ip_not_authorized.

### Assinatura da Requisição (HMAC)

> A assinatura é opcional e se liga por chave, no momento da criação. Chaves que não a exigem continuam autenticando apenas com o X-Bifrost-Invoke, sem nenhum cabeçalho a mais, e nada do que você já roda é afetado.

Opcional por chave. A chave de API sozinha prova que quem chamou conhece um segredo estático, mas não prova que a requisição é recente, que o corpo está íntegro, nem que aquela chamada já não aconteceu. Com assinatura ligada, cada requisição carrega uma prova válida uma única vez e por uma janela curta.

| Cabeçalho | Descrição |
| --- | --- |
| `X-Bifrost-Timestamp` | Horário Unix em segundos inteiros, só dígitos, no momento em que a requisição foi montada. Recusado se diferir do horário do servidor em mais de 300 segundos. |
| `X-Bifrost-Nonce` | Valor único por requisição, de 16 a 80 caracteres entre A-Z, a-z, 0-9, sublinhado e hífen. Aceito uma vez por chave. |
| `X-Bifrost-Signature` | HMAC-SHA256 da string canônica, com o seu segredo de assinatura como chave, em hexadecimal. Envie em minúsculas; maiúsculas também são aceitas. |

#### A string canônica

Monte estes seis campos unidos por um único caractere de quebra de linha, exatamente nesta ordem:

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

METHOD em maiúsculas. PATH é o caminho exatamente como aparece na linha da requisição, com a barra inicial e o prefixo /v1 (por exemplo /v1/check-payout/f47ac10b-58cc-4372-a567-0e02b2c3d479): não é decodificado e a barra final é mantida. QUERY é a query string crua como enviada, sem o ponto de interrogação inicial, sem reordenar e sem recodificar, e fica vazia quando não há parâmetros. O último campo é o SHA-256 em hexadecimal dos bytes exatos do corpo que você envia. Em requisições sem corpo, use o hash da string vazia, que é e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.

#### Assinando uma requisição

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

#### Pontos de atenção

- O segredo de assinatura é exibido uma única vez, na criação da chave. Ele nunca mais é devolvido, então guarde antes de fechar o painel.
- O segredo é separado da chave de API. Os dois são necessários: a chave identifica você, a assinatura prova a requisição.
- Faça o hash dos mesmos bytes que você transmite. Serializar o corpo duas vezes, ou reserializar depois de assinar, muda o resumo e invalida a assinatura.
- Cada nonce é aceito uma vez por chave. Reenviar a requisição idêntica devolve signature_replayed mesmo com todo o resto correto.
- Relógio fora de hora é a causa mais comum de signature_timestamp_out_of_range. Mantenha seus servidores em NTP.

### Limites de Requisição

Cada chave tem cota própria em uma janela fixa alinhada ao relógio: por padrão 500 requisições a cada 60 segundos, definidas na criação da chave. As respostas trazem o estado atual, para você regular o consumo em vez de descobrir o limite ao esbarrar nele:

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

- X-RateLimit-Limit é a cota, X-RateLimit-Remaining o que resta na janela e X-RateLimit-Reset o horário Unix em que a janela recomeça.
- Os cabeçalhos vêm em toda resposta que passou pela autenticação e pela conferência de escopo, inclusive respostas 4xx e 5xx do endpoint.
- Eles não aparecem nas recusas de autenticação e de escopo, nem em chaves configuradas sem cota, que não têm limite de requisições.

Passando da cota, as requisições devolvem 429 rate_limit_exceeded com o cabeçalho Retry-After até a janela recomeçar. Não há punição 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"
    }
  }
}
```

#### Falhas repetidas de autenticação

Independentemente da cota por chave, um endereço IP que erra a autenticação repetidamente fica bloqueado por um tempo. Enquanto bloqueado, recebe 429 too_many_failed_authentications com o cabeçalho Retry-After: aguarde esses segundos antes de tentar de novo. Requisições bem-sucedidas nunca contam para esse bloqueio.

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

## Receber um pagamento

Da escolha do token à entrega do pedido, usando payins criados pela API.

### 1. Escolha o token e a rede

GET /v1/summary/tokens lista os tokens da plataforma por rede, com min_receipt e max_receipt. A lista não é filtrada pela sua conta: a criação do payin confirma se o token aceita depósito para você naquela rede.

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

### 2. Crie o payin

POST /v1/payin com um reference_id único no seu sistema, o valor (process_by crypto_amount ou fiat_amount), o webhook_url e, se quiser, expires_at em segundos (padrão de 4 horas). Guarde o internal_reference da resposta e mostre ao cliente a wallet, e o memo quando o token exigir.

```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. Receba o webhook

Verifique o X-Bifrost-Signature sobre o corpo bruto, responda 2xx em até 30 segundos e processe depois. O mesmo aviso pode chegar mais de uma vez: elimine duplicatas por data.internal_reference junto com event.

### 4. Confira o valor antes de entregar

O payin fica concluído quando received_amount atinge minimum_payment por cento de crypto_amount, o que pode ser menos que o valor total. Compare data.received_amount com data.crypto_amount e decida o que fazer com a diferença.

### 5. Escolha quando entregar

payment_detected significa que a transferência foi vista (funds_available false): entregar agora é decisão sua e traz o risco de reversão. payment_confirmed significa que é final (funds_available true).

### 6. Trate as exceções

payment_reversed: a rede desfez o pagamento e o crédito foi retirado; o check-payin passa a devolver status reversed. is_duplicated true: um pagamento extra ou em outra rede gerou um payin novo, com referências próprias; vincule ao pedido por customer_info.

### 7. Concilie

GET /v1/check-payin/{internal_reference} devolve o payin a qualquer momento. GET /v1/statements lista as movimentações do saldo: DEPOSIT para o crédito, RATE e FEE para as cobranças.

### O que fazer quando algo falha

POST /v1/payin não tem Idempotency-Key. O reference_id é o que impede um segundo payin.

| Situação | O que fazer |
| --- | --- |
| Timeout ou sem resposta no POST /v1/payin | Reenvie com o mesmo reference_id. Se a resposta for duplicate_reference_id, a primeira requisição funcionou: busque-a com GET /v1/list-payin?reference_id=… |
| 400 | Corrija a requisição. Reenviar sem mudanças dá a mesma resposta. |
| 500, 502 ou 503 | Nada foi criado. Tente mais tarde com o mesmo reference_id. |
| 429 | Aguarde os segundos indicados em Retry-After. |
| O webhook não chegou | As entregas são repetidas até 10 vezes, com cerca de 5 minutos entre elas. Enquanto isso, consulte o payin com GET /v1/check-payin/{internal_reference}. |

> Payins criados no painel não são devolvidos pela API.

## Enviar um payout

Da conferência do saldo à confirmação da transferência, para um payout ou um lote de até 50.

### 1. Confira o saldo

GET /v1/balances devolve um saldo por token; a chave de cada token é o balance_key devolvido por GET /v1/summary/tokens. Quando available tiver a chave, use esse valor: o restante está retido por depósitos ainda não finais. min_withdrawal e max_withdrawal por token vêm de GET /v1/summary/tokens.

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

### 2. Crie o payout

POST /v1/payout com um objeto ou uma lista de até 50. Dê a cada payout um reference_id próprio e envie um Idempotency-Key por requisição. feetakenfromamount 1 desconta as taxas do valor; 0 soma as taxas ao 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. Leia o resultado de cada item

200 significa que todos os itens foram criados, 207 que parte foi, 400 que nenhum foi. results tem uma entrada por item, na ordem enviada, com o index. Guarde o internal_reference de cada payout criado.

### 4. Receba o webhook

payment_processed (data.status success, com o hash da transação em data.transactions) quando a transferência é feita, ou payment_failed quando não pode ser concluída. Verifique a assinatura e elimine duplicatas por data.internal_reference junto com event.

### 5. Concilie

GET /v1/check-payout/{internal_reference} devolve o payout a qualquer momento. GET /v1/statements lista WITHDRAWAL para o débito e RATE e FEE para as cobranças.

### O que fazer quando algo falha

Duas coisas impedem que você pague duas vezes: o Idempotency-Key da requisição e o reference_id de cada payout.

| Situação | O que fazer |
| --- | --- |
| Timeout ou sem resposta | Reenvie o mesmo corpo com o mesmo Idempotency-Key em até 24 horas: você recebe a resposta original e nada é processado de novo. |
| 409 idempotency_key_in_progress | A primeira requisição ainda está em execução. Aguarde e tente com a mesma chave. |
| 207 | Corrija e reenvie só os itens recusados, com um novo Idempotency-Key. Os criados continuam criados. |
| 500 com summary.not_processed | Reenvie os itens que não foram processados. Um reference_id já usado é recusado com duplicate_payment_reference, nunca pago duas vezes. |
| duplicate_payment_reference | O payout já existe. Use o internal_reference e o original_status do resultado. |
| 429 | Aguarde os segundos indicados em Retry-After. |

> Payouts criados no painel não são devolvidos pela API.

## Verificação de webhooks

Confirme que o aviso foi enviado pela Bifrost antes de confiar no corpo.

### Headers Enviados nos Webhooks

Todos os webhooks são enviados com headers especiais para validação e identificação:

```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 e X-Bifrost-Delivery acompanham toda entrega. São eles que provam a origem da mensagem.

### Tipos de webhook

O campo event do corpo diz o que aconteceu. Estes são todos os valores que ele pode ter:

| event | Descrição |
| --- | --- |
| `payment_processed` | Payout concluído. data.status vem success. X-Bifrost-Webhook-Type: payment_processed. |
| `payment_failed` | Payout não pôde ser concluído. data traz só status (failed), reference_id e internal_reference. X-Bifrost-Webhook-Type: payment_processed. |
| `payment_detected` | Depósito visto na rede; funds_available vem false. X-Bifrost-Webhook-Type: payment_confirmed. |
| `payment_confirmed` | Depósito final; funds_available vem true. X-Bifrost-Webhook-Type: payment_confirmed. |
| `payment_reversed` | Uma reorganização desfez o depósito e o crédito foi retirado. data.status é reversed, e o check-payin passa a devolver status reversed. X-Bifrost-Webhook-Type: payment_confirmed. |
| `fixed_address_pre_revoke · fixed_address_revoked` | Ciclo de vida do endereço fixo, descrito no fim desta página. X-Bifrost-Webhook-Type: fixed_address_lifecycle. |

X-Bifrost-Webhook-Type só nomeia a família do aviso: todo evento de depósito, inclusive payment_detected e payment_reversed, chega com payment_confirmed nesse cabeçalho, e um payout que falhou chega com payment_processed. Decida sempre pelo campo event do corpo.

### Entrega e retentativas

Cada aviso é um POST com corpo JSON para a webhook_url que você cadastrou. O que conta como entregue, e o que acontece quando não é:

- Responda com qualquer status 2xx em até 30 segundos (a conexão em si precisa abrir em até 10 segundos). Qualquer outro status, inclusive 3xx, timeout ou erro de conexão, conta como falha.
- Redirecionamentos não são seguidos. O certificado TLS é verificado, HTTPS é obrigatório em produção, e a conexão é feita por IPv4 ao endereço validado para o seu host.
- Uma entrega que falhou é tentada de novo cerca de 5 minutos depois, até 10 tentativas no total. Depois da décima falha o aviso fica marcado como failed e pode ser reenviado pelo painel.
- Toda tentativa leva o mesmo corpo, um X-Bifrost-Delivery e um X-Bifrost-Timestamp novos, e uma assinatura calculada para eles.
- O mesmo aviso pode, portanto, chegar mais de uma vez. Deduplique por data.internal_reference junto com event (nos avisos de endereço fixo, wallet junto com event).
- Responda primeiro e processe depois: fazer trabalho lento antes de responder é o caminho mais comum para estourar o limite de 30 segundos.

### O Selo de Heimdall

Como confirmar que a mensagem veio mesmo da ponte

O endereço que recebe seus webhooks é público: qualquer um que descubra a URL pode enviar uma requisição para ela. Sem verificação, um POST forjado anunciando pagamento aprovado é indistinguível de um pagamento real, e costuma virar mercadoria liberada sem que o dinheiro tenha entrado.

#### A chave de assinatura

Cada conta tem uma chave dedicada, gerada automaticamente e nunca transmitida. Nós usamos a nossa cópia para assinar cada entrega; você busca a sua uma vez e guarda. Como ela não viaja junto com o webhook, um vazamento de logs no seu ambiente não entrega a chave, e é isso que faz a assinatura provar alguma coisa.

#### Obtendo a chave

A chave já existe na sua conta e fica no painel, em Configurações, na área de webhooks. Exibi-la pede o seu segundo fator. Copie o valor para o seu servidor e guarde-o como guardaria uma senha de banco de dados: em variável de ambiente ou cofre de segredos, nunca no código versionado.

> O painel também permite trocar a chave, e a troca invalida a anterior de imediato. Entre gerar a nova e atualizar a sua cópia, os webhooks saem assinados com uma chave que você ainda não tem, e a sua verificação vai falhar nesse intervalo.

#### Cabeçalhos da assinatura

| Campo | Descrição |
| --- | --- |
| `X-Bifrost-Signature` | v1= seguido do HMAC-SHA256 em hexadecimal. |
| `X-Bifrost-Timestamp` | Unix timestamp de quando o webhook foi emitido. |
| `X-Bifrost-Delivery` | 32 caracteres hexadecimais minúsculos, únicos para esta tentativa de entrega. Muda a cada retentativa. |

#### A string assinada

O HMAC não é calculado sobre o corpo cru, e sim sobre esta string, montada com quebras de linha entre os campos:

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

O timestamp e o identificador de entrega entram no cálculo de propósito. Se apenas o corpo fosse assinado, esses dois cabeçalhos poderiam ser reescritos por qualquer um sem invalidar a assinatura, e um webhook capturado hoje seria reenviado meses depois parecendo recém-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)
```

#### Cuidados

- Use o corpo cru. Desserializar o JSON e serializar de novo muda espaços em branco e ordem de chaves, e o hash deixa de bater. Capture os bytes exatamente como chegaram, antes de qualquer parsing.
- Compare em tempo constante. hash_equals, timingSafeEqual e compare_digest existem para isso: uma comparação comum de strings vaza, pelo tempo de resposta, quantos caracteres iniciais estão certos.
- Trate o reenvio. Uma entrega que não recebe 2xx é tentada de novo, com X-Bifrost-Delivery diferente a cada tentativa. O corpo não tem identificador de evento: deduplique por data.internal_reference junto com event, para não processar o mesmo pagamento duas vezes.
- HTTPS é obrigatório em produção. URLs http:// são recusadas tanto no cadastro quanto no envio.

#### Webhook sem assinatura

Se o cabeçalho X-Bifrost-Signature não vier, não processe a mensagem e fale com o suporte.

### Ciclo de vida do endereço fixo

Endereços fixos que ficam muito tempo sem receber nada são recolhidos. Antes e no momento em que isso acontece, um aviso é enviado para a URL de webhook de endereços fixos cadastrada no painel, não para uma URL de pagamento; sem essa URL nenhum aviso é enviado. Endereços do tipo evm são permanentes e nunca recebem esses eventos. O corpo é plano, sem o envelope data, e é assinado como todo webhook.

| event | Descrição |
| --- | --- |
| `fixed_address_pre_revoke` | Depois de 120 dias sem transação. revoke_at informa quando o endereço será revogado (cerca de 30 dias depois), a menos que receba uma transação antes. |
| `fixed_address_revoked` | Depois de 150 dias sem transação. O endereço deixa de pertencer à sua conta; status vem revoked e revoke_at vem 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 | Descrição |
| --- | --- |
| `event` | fixed_address_pre_revoke ou fixed_address_revoked |
| `wallet` | O endereço fixo |
| `network · token` | Rede e token do endereço; token pode ser null |
| `provider` | Reservado. Não dependa dele. |
| `customer_info` | O customer_info informado ao pedir o endereço, ou null |
| `status` | Status do endereço no momento do aviso (STATUS_ASSIGNED na pré-revogação), ou revoked |
| `last_transaction_at · assigned_at` | Última transação recebida e data de atribuição; a inatividade conta a partir de last_transaction_at, ou de assigned_at quando o endereço nunca recebeu transação |
| `revoke_at` | Data prevista da revogação em fixed_address_pre_revoke; null em fixed_address_revoked |
| `message` | Explicação legível, em inglês |
| `timestamp` | Quando o aviso foi gerado, em ISO 8601 |

## Webhooks de saque

Avisos de um payout criado pela API.

### `payment_processed` · Payout Confirmado

Enviado quando o saque é processado. status vem success. O corpo traz a carteira, os valores, a rede e 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 Falhou

Enviado quando o saque não pode ser concluído. O corpo traz só 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"
}
```

> Sua integração precisa tratar este aviso: localize o payout por reference_id ou internal_reference e decida o que informar ao seu cliente. Para pagar de novo, crie um novo payout com um novo reference_id.

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

Seu próprio identificador do cliente, como ID de usuário, e-mail ou número do pedido. Ele volta nos webhooks e nas listagens, então você sabe de quem é cada transação, inclusive os payins que a Bifrost cria por conta própria (pagamentos adicionais e depósitos em endereço fixo), cujo reference_id é gerado pela Bifrost.

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

## Webhooks de depósito

Notificações de um payin, do momento em que a transferência aparece na rede até o valor ficar disponível no seu saldo.

### Pagamentos parciais

Um depósito é marcado como completed, e payment_detected / payment_confirmed são enviados, quando received_amount atinge minimum_payment por cento de crypto_amount. O minimum_payment é definido por conta; abaixo de 100, o cliente pode ter pago menos que a fatura. Antes de entregar, compare data.received_amount com data.crypto_amount (ambos em data.crypto_currency) e decida o que fazer com a diferença.

### `payment_detected` · Pagamento detectado

A transação apareceu na rede e o payin está concluído, mas este não é o status final. funds_available é false e finality_status é pending_finality enquanto o bloco ainda pode ser reorganizado. Se precisar de agilidade, você pode liberar o produto neste aviso. O valor fica retido e não faz parte do saldo disponível até a rede atingir as confirmações exigidas.

> Liberar o produto nesta etapa é uma decisão sua. Um saque desse valor é recusado até o payment_confirmed. Até hoje nenhum pagamento detectado pela Bifrost foi revertido; veja 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` · Pagamento confirmado

O bloco é final. funds_available é true e o valor está disponível no seu saldo. Credite o cliente de forma definitiva neste 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` · Pagamento revertido

Pode ficar tranquilo: até hoje nenhum pagamento recebido pela Bifrost foi revertido.

Este aviso existe só por causa das redes L2, como Arbitrum, Optimism e Base. Nelas, o pagamento é detectado assim que entra num bloco da L2 (payment_detected), em segundos, e se torna definitivo quando esse bloco é finalizado na rede principal (payment_confirmed), alguns minutos depois. Em teoria, uma transação pode ser desfeita nesse intervalo. É raríssimo, mas como a possibilidade existe, preferimos documentar.

Se acontecer, o crédito daquele depósito é retirado do saldo, data.status vem como reversed e reversal_reason informa o motivo. A partir daí, check-payin e list-payin também devolvem status reversed.

Se você libera o produto só em payment_confirmed, este aviso não afeta você. Se libera em payment_detected, basta ligar este aviso ao pedido pelo 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"
}
```

### Quando a detecção já é final

Em algumas redes o pagamento é final assim que é detectado. O payment_confirmed é enviado logo depois do payment_detected; o payment_detected já traz finality_status final, e o payment_confirmed pode trazer finality_status null. Decida pelo event e pelo funds_available: nem todo payment_detected retém o valor.

### Campos do depósito

| Campo | Tipo | Descrição |
| --- | --- | --- |
| `funds_available` | `boolean` | false enquanto o valor está retido. true quando pode ser sacado. |
| `finality_status` | `string \| null` | pending_finality enquanto as confirmações correm, final quando o bloco está consolidado, reorged quando o depósito foi desfeito. Pode ser null num payment_confirmed de pagamento que já era final na detecção. |
| `confirmations` | `number \| null` | Reservado para a contagem de confirmações. Hoje vem sempre null; não dependa dele. |
| `estimated_release_seconds` | `number \| null` | Reservado para a estimativa de espera até a liberação. Hoje vem sempre null; não dependa dele. |
| `reversal_reason` | `string \| null` | Por que o crédito foi retirado. Null em payment_detected e payment_confirmed. |
| `received_amount` | `string` | Valor em cripto creditado no recibo. |
| `received_amount_fiat` | `string` | Valor fiat do montante recebido na cotação da invoice. |
| `received_amount_fiat_current` | `string` | Valor fiat na cotação do momento em que o webhook foi montado. Cai para a cotação da invoice se o preço ao vivo não estiver disponível. |
| `cross_token` | `boolean` | true quando o cliente pagou em um token diferente do token da invoice. |
| `paid_with` | `array` | Tokens de fato recebidos on-chain. Cada item tem currency e amount. |
| `is_wallet_fixed` | `boolean` | true quando o depósito caiu em um endereço fixo. |

**`is_duplicated`** (Apenas Payin)

true quando a própria Bifrost criou este payin, para um pagamento que não correspondia a um payin em aberto: um pagamento extra para um endereço cujo payin já estava concluído, ou um pagamento em rede diferente da fatura. É um payin novo, com internal_reference próprio e reference_id gerado pela Bifrost. O valor é creditado no seu saldo e o aviso vai para o webhook_url do payin original.

> Quando is_duplicated é true, o reference_id não é seu. Vincule o pagamento ao seu pedido por customer_info ou wallet. O payin também aparece em List Payins e Check Specific Payin.

**`customer_info`**

Seu próprio identificador do cliente, como ID de usuário, e-mail ou número do pedido. Ele volta nos webhooks e nas listagens, então você sabe de quem é cada transação, inclusive os payins que a Bifrost cria por conta própria (pagamentos adicionais e depósitos em endereço fixo), cujo reference_id é gerado pela Bifrost.

## Códigos de Erro

Todos os códigos de erro que a API devolve, agrupados por tipo, com o que fazer em cada um.

### Formato do erro

Todo erro da /v1 tem o mesmo formato, com o status HTTP das tabelas abaixo. Compare error.code, que é snake_case estável. error.message é texto livre em inglês para os seus logs. error.details só aparece quando há dado útil, como campos inválidos, limites ou escopos.

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

Num lote de POST /v1/payout, cada item recusado traz o mesmo objeto em results[].error, ao lado do seu index e reference_id. A resposta é 200 quando todos os itens deram certo, 207 quando parte deu certo e parte falhou, e 400 quando nenhum deu certo:

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

- As mensagens de validação ficam sempre em error.details.fields, indexadas pelo nome do campo, tanto no topo quanto dentro de um item do lote.
- Um item de payout duplicado conta como falha: traz error.code duplicate_payment_reference mais duplicate, original_status e original_created_at no próprio item. Um lote só de duplicados responde 400.
- Decida por error.code, nunca por error.message: as mensagens podem ser reescritas, os códigos não mudam.
- 4xx significa que o pedido precisa mudar antes de ser reenviado. 5xx significa que a falha foi do nosso lado e o mesmo pedido pode ser repetido; no POST /v1/payout, o reference_id continua protegendo contra saque duplicado.
- Um caminho /v1 desconhecido devolve 404 route_not_found em JSON. Um cliente que pede text/html no Accept recebe uma página HTML.

### Erros de Autenticação e Acesso

| Código HTTP | Erro | Descrição |
| --- | --- | --- |
| `401` | `api_key_missing` | O cabeçalho X-Bifrost-Invoke não foi enviado. |
| `401` | `api_key_invalid` | API Key desconhecida, revogada ou expirada. Também devolvido enquanto a conta está suspensa. |
| `403` | `api_access_denied` | O acesso à API está desativado nesta conta. Conferido a cada requisição. |
| `403` | `email_not_verified` | O e-mail da conta ainda não foi verificado. |
| `403` | `ip_not_authorized` | O IP de origem não está na whitelist da chave. |
| `400` | `invalid_ip` | Não foi possível determinar o IP do cliente. |
| `401` | `signature_headers_missing` | A chave exige assinatura e falta X-Bifrost-Signature, X-Bifrost-Timestamp ou X-Bifrost-Nonce. |
| `401` | `signature_timestamp_invalid` | X-Bifrost-Timestamp não é composto só de dígitos (segundos Unix). |
| `401` | `signature_timestamp_out_of_range` | O timestamp difere do horário do servidor em mais de 300 segundos. |
| `401` | `signature_nonce_length_invalid` | O nonce tem menos de 16 ou mais de 80 caracteres. |
| `401` | `signature_nonce_charset_invalid` | O nonce tem caracteres fora de A-Z, a-z, 0-9, sublinhado e hífen. |
| `401` | `signature_invalid` | A assinatura não confere com a string canônica. |
| `401` | `signature_replayed` | Este nonce já foi usado por esta chave. |
| `401` | `signature_secret_unavailable` | A chave exige assinatura mas não tem segredo utilizável. Fale com o suporte. |
| `403` | `scope_denied` | A chave não tem o escopo que este endpoint exige. details.required_scopes lista o que serve e details.granted_scopes o que a chave tem. |
| `429` | `rate_limit_exceeded` | A cota da chave na janela atual acabou. Retry-After e details.retry_after_seconds dizem quando tentar de novo. |
| `429` | `too_many_failed_authentications` | Este IP está bloqueado temporariamente após falhas repetidas de autenticação. Vem com Retry-After. |
| `401` | `user_not_authenticated` | Não foi possível associar a requisição à sua chave de API. Tente de novo; se repetir, fale com o suporte. |

### Erros do Pedido

| Código HTTP | Erro | Descrição |
| --- | --- | --- |
| `404` | `route_not_found` | Nenhuma rota /v1 corresponde ao caminho e ao método. Respondido antes da autenticação. |
| `400` | `json_parse_exception` | O corpo da requisição não é JSON válido. Qualquer rota /v1. |
| `400` | `json_parse_failed` | POST /v1/payout: o corpo foi decodificado como null ou false. |
| `400` | `invalid_parameters` | Parâmetros inválidos ou ausentes. details.fields traz cada campo com sua mensagem (POST /v1/payout com todos os itens inválidos, POST /v1/payin, endereços fixos, filtros de extrato). |
| `400` | `invalid_payment_list` | POST /v1/payout: o corpo está vazio ou não é um objeto de payout nem uma lista de payouts. |
| `400` | `max_payouts_exceeded` | POST /v1/payout: mais de 50 payouts em uma requisição. |
| `409` | `idempotency_key_in_progress` | Uma requisição com esta Idempotency-Key ainda está em andamento. Aguarde e repita com a mesma chave. |
| `400` | `crypto_amount_required` | POST /v1/payin: process_by é crypto_amount e falta crypto_amount. |
| `400` | `fiat_amount_required` | POST /v1/payin: process_by é fiat_amount e falta fiat_amount. |
| `400` | `fiat_currency_required` | POST /v1/payin: process_by é fiat_amount e 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 fora de 1 a 100. |
| `400` | `invalid_status_filter` | list-payout / list-payin: valor de status desconhecido. |
| `400` | `invalid_network_filter` | list-payout / list-payin: rede desconhecida. |
| `400` | `invalid_date_from` | list-payout / list-payin: date_from não é uma data. |
| `400` | `invalid_date_to` | list-payout / list-payin: date_to não é uma data. |
| `400` | `invalid_reference_id` | check-payout / check-payin: referência com menos de 10 ou mais de 756 caracteres. Extratos: reference_id com mais de 255. |
| `400` | `invalid_pagination` | Extratos: page ou per_page fora do intervalo. |
| `400` | `invalid_format` | Extratos: date_from ou date_to não é uma data (details.field). |
| `400` | `invalid_date_range` | Extratos: date_from é posterior a date_to. |
| `400` | `date_range_too_wide` | Extratos: o intervalo é maior que o permitido. details.max_days informa o teto. |
| `400` | `invalid_amount` | Extratos: amount_from ou amount_to inválido, ou amount_from maior que amount_to. |
| `400` | `missing_parameters` | get-price: falta currency1 ou currency2. |
| `400` | `invalid_currency` | get-price: currency1 ou currency2 não é um código de moeda válido. |
| `400` | `invalid_webhook_url` | PUT /v1/fixed-wallets/config: webhook_url aponta para um destino não permitido. |

### Erros de Negócio

| Código HTTP | Erro | Descrição |
| --- | --- | --- |
| `400` | `insufficient_total_balance` | POST /v1/payout: os itens de uma moeda somam mais que o saldo disponível. Nada é debitado. details traz crypto_currency, available e required. |
| `404` | `user_not_found` | POST /v1/payout: o cadastro da conta está incompleto. Fale com o suporte. |
| `404` | `record_not_found` | O payout ou payin consultado por check-payout / check-payin não existe nesta conta. Também devolvido quando falta um dado da conta de que a requisição precisa. |
| `400` | `duplicate_reference_id` | POST /v1/payin: já existe um payin com este reference_id na conta. |
| `400` | `invalid_network_or_deposits_disabled` | POST /v1/payin: rede desconhecida, ou depósitos desativados nela. |
| `400` | `unsupported_token_or_deposits_disabled` | POST /v1/payin: token não suportado, ou depósitos desativados para ele. |
| `400` | `token_network_mismatch` | POST /v1/payin: o token não está disponível nesta rede. |
| `400` | `memo_required` | POST /v1/payin: este token exige memo. |
| `400` | `amount_out_of_limits` | POST /v1/payin: o valor em cripto está fora dos limites do token. details traz min_allowed e max_allowed. |
| `400` | `value_below_minimum` | POST /v1/payin com fiat_amount: o valor convertido está abaixo do mínimo. |
| `400` | `value_above_maximum` | POST /v1/payin com fiat_amount: o valor convertido está acima do máximo. |
| `400` | `token_not_supported_or_inactive` | POST /v1/payin com fiat_amount: o token não é suportado ou está inativo para conversão. |
| `400` | `crypto_amount_does_not_correspond_to_fiat_amount_provided` | POST /v1/payin: crypto_amount e fiat_amount foram enviados juntos e não batem com a cotação atual. |
| `400` | `invalid_parameters_for_process_by` | POST /v1/payin: os valores enviados não combinam com o process_by escolhido. |
| `404` | `price_not_found` | get-price: não há cotação para este par de moedas. |
| `403` | `feature_disabled` | Endereços fixos não estão habilitados para a conta (config, atualização de config e criação). |
| `422` | `type_not_supported` | POST /v1/fixed-wallets: este type não está disponível para endereços fixos. |
| `403` | `limit_not_allowed` | POST /v1/fixed-wallets: o limite da conta para este type é zero. |
| `409` | `limit_reached` | POST /v1/fixed-wallets: a conta já tem o número máximo de endereços fixos. |
| `409` | `no_wallet_available` | POST /v1/fixed-wallets: nenhum endereço desse tipo disponível agora. Tente mais tarde. |

### Erros por Item do Lote de Payout (results[].error)

| Código HTTP | Erro | Descrição |
| --- | --- | --- |
| `207/400` | `invalid_parameters` | O item falhou na validação. error.details.fields traz cada campo com sua mensagem. |
| `207/400` | `missing_required_fields` | Falta um campo obrigatório do item. |
| `207/400` | `duplicate_reference_id_in_request` | O mesmo reference_id aparece antes no mesmo lote. |
| `207/400` | `duplicate_payment_reference` | O reference_id já foi usado nesta conta. O item traz duplicate, original_status, original_created_at e o internal_reference original; nenhuma segunda transferência é feita. |
| `207/400` | `invalid_crypto_amount` | process_by é crypto_amount e crypto_amount está ausente ou inválido. |
| `207/400` | `invalid_fiat_amount` | process_by é fiat_amount e fiat_amount está ausente ou inválido. |
| `207/400` | `fiat_currency_required` | process_by é fiat_amount e falta fiat_currency. |
| `207/400` | `fiat_currency_not_accepted` | A fiat_currency não é aceita. |
| `207/400` | `memo_required` | Este token exige memo. |
| `207/400` | `invalid_network_or_withdrawals_disabled` | Rede desconhecida, ou saques desativados nela. |
| `207/400` | `unsupported_token_or_withdrawals_disabled` | Token não suportado, ou saques desativados para ele. |
| `207/400` | `token_network_mismatch` | O token não está disponível nesta rede. |
| `207/400` | `token_not_active` | O token não está ativo para saque ou para conversão. |
| `207/400` | `invalid_format` | O endereço da wallet não é válido para a rede. |
| `207/400` | `limit_exceeded` | O valor está fora do mínimo e do máximo do token (também quando o valor fiat convertido cai fora deles). |
| `207/400` | `amount_does_not_cover_fees` | As taxas consomem todo o valor, e o destinatário não receberia nada. |
| `207/400` | `crypto_amount_does_not_correspond_to_fiat_amount_provided` | crypto_amount e fiat_amount não batem com a cotação atual. |
| `207/400` | `price_unavailable` | Sem cotação para converter o valor, ou a chave tem teto diário e o token não tem cotação em USD para contar o saque. |
| `207/400` | `insufficient_balance` | O saldo disponível não cobre este item. |
| `207/400` | `daily_payout_limit_exceeded` | O item ultrapassaria o teto diário configurado na chave. |
| `207/400` | `daily_payout_limit_unavailable` | A chave tem teto diário mas o consumo não pôde ser registrado. Reenvie o item. |
| `207/400` | `error_fetching_active_tokens · error_fetching_active_currencies` | Não foi possível carregar o catálogo de tokens ou moedas. Reenvie o item. |
| `207/400` | `user_not_found · record_not_found` | Falta um dado da conta de que o saque precisa. |
| `207/400` | `internal_error · database_error · transaction_failed · network_unavailable · duplicate_record` | A gravação do saque falhou do nosso lado e foi desfeita. Nada foi debitado por este item; confira com check-payout ou list-payout antes de reenviar. |

### Erros de Sistema

| Código HTTP | Erro | Descrição |
| --- | --- | --- |
| `500` | `internal_error` | Falha inesperada. No POST /v1/payout, uma falha no meio do lote também devolve summary (com not_processed) e results no topo, para você reenviar só o que faltou. |
| `500` | `database_error` | Extratos: a consulta falhou. |
| `500` | `error_fetching_active_currencies` | POST /v1/payout e POST /v1/payin: não foi possível carregar o catálogo de tokens ou fiats. Nada foi processado. |
| `500` | `token_type_not_found · receipt_creation_failed · database_transaction_failed` | POST /v1/payin: o payin não pôde ser criado do nosso lado. |
| `502` | `external_processor_error` | POST /v1/payin e POST /v1/fixed-wallets: o endereço de depósito não pôde ser criado agora. Nada foi criado; tente mais tarde. |
| `503` | `no_processor_configured · no_wallet_available` | POST /v1/payin: não há processador ou endereço de depósito disponível para este token e rede agora. Tente mais tarde. |
| `503` | `price_not_available_for_conversion · price_not_available_for_validation` | POST /v1/payin: não há cotação disponível para converter ou conferir o valor. Tente mais tarde. |
| `503` | `service_unavailable` | POST /v1/payin e endpoints de summary: uma dependência está temporariamente indisponível. |
| `500` | `config_update_failed` | PUT /v1/fixed-wallets/config: a configuração não pôde ser gravada. |
| `500` | `assign_failed` | POST /v1/fixed-wallets: o endereço não pôde ser atribuído. |

### Melhores Práticas

#### Segurança

**Faça**

- Armazene sua API Key em variáveis de ambiente
- Configure IP Whitelist
- Verifique o X-Bifrost-Signature de todo webhook
- Use HTTPS em todas as requisições
- Implemente timeouts nas requisições

**Não Faça**

- Compartilhar API Key publicamente
- Commitar API Keys no Git
- Usar a mesma API Key em múltiplos ambientes

#### Performance

- Os dados de extrato e de resumo podem ter até 5 minutos de atraso; não consulte com frequência maior que isso
- Implemente paginação adequada
- Evite requisições excessivas (respeite o rate limit)
- Processe webhooks de forma assíncrona

#### Idempotência

- Use reference_id único e significativo: ele é a trava contra saque duplicado, e reapresentá-lo devolve o pagamento original em vez de criar outro
- Envie Idempotency-Key nos lotes: se a conexão cair, repetir com a mesma chave devolve a resposta original em vez de reprocessar
- POST /v1/payin não tem Idempotency-Key: depois de um timeout, reenvie com o mesmo reference_id e, se a resposta for duplicate_reference_id, busque o payin com GET /v1/list-payin?reference_id=…
- Trate 207 e 500 lendo results: os dois trazem o que já foi criado, para você reenviar apenas o que faltou
- Trate as retentativas de webhook deduplicando por data.internal_reference junto com event, para não processar o mesmo aviso duas vezes

---

> A referência da API está em inglês. Os guias, a autenticação, os webhooks e os códigos de erro estão em português.

## Referência da 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`

Escopo exigido: `payout:write` (chave nova sem lista é somente leitura)

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

#### Outras respostas

Respostas que não são o caminho feliz e ainda assim precisam de tratamento na sua integração.

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

Escopo exigido: `payout:write` (chave nova sem lista é somente leitura)

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

#### Outras respostas

Respostas que não são o caminho feliz e ainda assim precisam de tratamento na sua integração.

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

Escopo exigido: `payout:read` (chave nova sem lista é somente leitura)

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

#### Outras respostas

Respostas que não são o caminho feliz e ainda assim precisam de tratamento na sua integração.

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

Escopo exigido: `payout:read` (chave nova sem lista é somente leitura)

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

#### Outras respostas

Respostas que não são o caminho feliz e ainda assim precisam de tratamento na sua integração.

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

## Referência da 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`

Escopo exigido: `payin:write` (chave nova sem lista é somente leitura)

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

#### Outras respostas

Respostas que não são o caminho feliz e ainda assim precisam de tratamento na sua integração.

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

Escopo exigido: `payin:read` (chave nova sem lista é somente leitura)

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

#### Outras respostas

Respostas que não são o caminho feliz e ainda assim precisam de tratamento na sua integração.

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

Escopo exigido: `payin:read` (chave nova sem lista é somente leitura)

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

#### Outras respostas

Respostas que não são o caminho feliz e ainda assim precisam de tratamento na sua integração.

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

## Referência da API · Queries

Endpoints for querying balances, statements, and account information.

### Get Balances

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

Escopo exigido: `balances:read` (chave nova sem lista é somente leitura)

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`

Escopo exigido: `statements:read` (chave nova sem lista é somente leitura)

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

#### Outras respostas

Respostas que não são o caminho feliz e ainda assim precisam de tratamento na sua integração.

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

Escopo exigido: `statements:read` (chave nova sem lista é somente leitura)

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

#### Outras respostas

Respostas que não são o caminho feliz e ainda assim precisam de tratamento na sua integração.

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

Escopo exigido: `statements:read` (chave nova sem lista é somente leitura)

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

#### Outras respostas

Respostas que não são o caminho feliz e ainda assim precisam de tratamento na sua integração.

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

Escopo exigido: `prices:read` ou `statements:read` (qualquer um deles basta)

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`

Escopo exigido: `prices:read` ou `statements:read` (qualquer um deles basta)

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

## Referência da 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`

Escopo exigido: `prices:read` (chave nova sem lista é somente leitura)

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

#### Outras respostas

Respostas que não são o caminho feliz e ainda assim precisam de tratamento na sua integração.

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

Escopo exigido: `prices:read` (chave nova sem lista é somente leitura)

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

## Referência da 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`

Escopo exigido: `wallets:read` (chave nova sem lista é somente leitura)

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`

Escopo exigido: `wallets:read` (chave nova sem lista é somente leitura)

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

#### Outras respostas

Respostas que não são o caminho feliz e ainda assim precisam de tratamento na sua integração.

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

Escopo exigido: `wallets:write` (chave nova sem lista é somente leitura)

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

#### Outras respostas

Respostas que não são o caminho feliz e ainda assim precisam de tratamento na sua integração.

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

Escopo exigido: `wallets:read` (chave nova sem lista é somente leitura)

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.

#### Outras respostas

Respostas que não são o caminho feliz e ainda assim precisam de tratamento na sua integração.

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

Escopo exigido: `wallets:write` (chave nova sem lista é somente leitura)

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

#### Outras respostas

Respostas que não são o caminho feliz e ainda assim precisam de tratamento na sua integração.

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

---

## Receitas de integração (passo a passo para agentes)

### Receber um pagamento (payin)

1. Leia a chave da API, e o segredo de assinatura quando a chave exigir, de variáveis de ambiente, nunca do código.
2. Chame `GET /v1/summary/tokens` para escolher o token e a rede (`min_receipt`, `max_receipt`). A lista não é filtrada pela sua conta: a criação confirma se o token aceita depósito para você.
3. Chame `POST /v1/payin` com o id do pedido como `reference_id`, `crypto_currency`, `network`, `process_by` e o valor correspondente como string decimal, `webhook_url` e `customer_info`. Guarde `data.internal_reference` e mostre ao cliente `data.wallet`, e o memo quando o token exigir.
4. No endpoint de webhook, leia o corpo cru, valide `X-Bifrost-Signature`, responda 2xx na hora e processe depois. Descarte avisos já processados por `data.internal_reference` junto com `event`.
5. Em `payment_confirmed`, compare `data.received_amount` com `data.crypto_amount` e entregue. Entregar em `payment_detected` é decisão sua e corre o risco de `payment_reversed`. Quando `is_duplicated` for true, ligue o pagamento ao pedido pelo `customer_info`.
6. Depois de um timeout na criação, reenvie com o mesmo `reference_id`; em `duplicate_reference_id`, busque o payin com `GET /v1/list-payin?reference_id=…`. Decida todo erro pelo `error.code`.
7. Concilie com `GET /v1/check-payin/{internal_reference}`.

### Enviar payouts

1. Leia a chave da API, e o segredo de assinatura quando a chave exigir, de variáveis de ambiente, nunca do código.
2. Consulte `GET /v1/balances` (use `available` quando ele tiver a chave do token) e `min_withdrawal` e `max_withdrawal` em `GET /v1/summary/tokens`.
3. Chame `POST /v1/payout` com uma lista de até 50 payouts, cada um com o seu `reference_id` estável, valores como string decimal e `feetakenfromamount` 0 ou 1, e envie um `Idempotency-Key` por requisição.
4. Leia `results[]` pelo `index`: guarde o `internal_reference` dos itens criados; nos recusados, decida pelo `results[].error.code` e reenvie só esses, com um Idempotency-Key novo.
5. Depois de um timeout, reenvie o mesmo corpo com o mesmo Idempotency-Key em até 24 horas. Em 409 `idempotency_key_in_progress`, espere e repita com a mesma chave. Em 500, reenvie só os itens contados em `summary.not_processed`.
6. Trate os webhooks `payment_processed` e `payment_failed`, validando a assinatura e deduplicando por `data.internal_reference` junto com `event`. Para pagar de novo depois de uma falha, crie um payout novo com um `reference_id` novo.
7. Concilie com `GET /v1/check-payout/{internal_reference}`.

### Endereço fixo por cliente

1. Endereços fixos precisam estar habilitados para a conta; caso contrário, criar um endereço ou ler ou alterar a configuração responde 403 `feature_disabled`. `GET /v1/fixed-wallets/supported` lista os tipos e `GET /v1/fixed-wallets/config` os limites e a URL de webhook.
2. Defina a URL de webhook de endereço fixo no painel ou com `PUT /v1/fixed-wallets/config`. Sem ela, nenhum aviso é enviado.
3. Chame `POST /v1/fixed-wallets` com `type` e o id do seu cliente em `customer_info`: 201 para um endereço novo, 200 `fixed_wallets_already_assigned` quando esse cliente já tem um do tipo. Um endereço atende todas as redes da sua família.
4. Cada transferência para o endereço cria um payin já concluído e envia `payment_detected` / `payment_confirmed` para a URL de endereço fixo, com `is_wallet_fixed` true e um `reference_id` gerado. Ligue ao cliente pelo `customer_info` ou pela `wallet`.
5. Trate `fixed_address_pre_revoke` (120 dias sem transação) e `fixed_address_revoked` (150 dias). Endereços do tipo `evm` são permanentes.
6. Use `GET /v1/fixed-wallets` para listar os endereços e ler `allowance` antes de criar mais.
