# API Bifrost Crypto v1

> Version Markdown de la documentation publique de l'API v1 de Bifrost Crypto, pour les agents d'IA et les générateurs de code. Même contenu que les pages HTML, généré à partir de la même source, sans navigation ni mise en forme.
>
> Version HTML: https://bifrostcrypto.com/fr/docs
> Fichiers liés: [ai.md](https://bifrostcrypto.com/ai/fr.md) · [Postman](https://bifrostcrypto.com/postman-collection.json) · [OpenAPI](https://bifrostcrypto.com/openapi.json) · [llms.txt](https://bifrostcrypto.com/llms.txt)
> Autres langues: [Português](https://bifrostcrypto.com/ai/pt-BR.md) · [English](https://bifrostcrypto.com/ai.md) · [Español](https://bifrostcrypto.com/ai/es.md)
> API v1 · dernière modification du contrat le 2026-10-10

## Résumé rapide pour les agents

- URL de base `https://bridge.bifrostcrypto.com`. Chaque chemin commence par `/v1`. Les corps de requête et de réponse sont en JSON (`Content-Type: application/json`).
- Envoyez la clé d'API dans `X-Bifrost-Invoke` à chaque requête. Il n'y a ni connexion ni jeton à renouveler. Clé absente : 401 `api_key_missing` ; clé inconnue, révoquée ou expirée : 401 `api_key_invalid`.
- Une clé peut être créée avec la signature des requêtes. Ses requêtes portent alors aussi `X-Bifrost-Timestamp` (secondes Unix), `X-Bifrost-Nonce` (16 à 80 caractères, accepté une fois) et `X-Bifrost-Signature` : le HMAC-SHA256 en hexadécimal, avec le secret de signature, de METHOD, PATH (avec `/v1`), la QUERY brute, TIMESTAMP, NONCE et le SHA-256 en hexadécimal des octets exacts du corps, joints par `\n`. Signez les octets que vous envoyez et ne resérialisez jamais le corps après la signature.
- Chaque clé porte des scopes. Un appel en dehors renvoie 403 `scope_denied` avec `details.required_scopes`. Une clé créée sans liste de scopes est en lecture seule.
- Envoyez `crypto_amount` et `fiat_amount` sous forme de chaînes décimales simples comme `"100.50"` : des chiffres et un point facultatif, sans notation scientifique, signe ni séparateur de milliers. Les montants des réponses sont des chaînes dont le nombre de décimales varie : convertissez-les avec une bibliothèque décimale, pas en float.
- Exception : `price` dans `GET /v1/get-price` utilise la virgule comme séparateur décimal et le point comme séparateur de milliers (`"65.432,10"`). Retirez les points, puis remplacez la virgule par un point avant de convertir.
- Les champs de date et heure sont au format `YYYY-MM-DD HH:MM:SS` en UTC−03:00, sans décalage dans la valeur, et les filtres de date sont lus avec le même décalage. Exceptions : la limite quotidienne de payout d'une clé repart à 00:00 UTC, et les webhooks d'adresse fixe envoient `timestamp` en ISO 8601 avec le décalage.
- `network` prend les noms canoniques listés dans Supported Networks (`Ethereum`, `BinanceSmartChain`, `Tron`…). Les endpoints de création acceptent aussi des alias (`erc20`, `bsc`, `trc20`…) ; les réponses, les webhooks et les filtres de liste utilisent toujours le nom canonique, et les filtres sont sensibles à la casse. Les tokens de chaque réseau viennent de `GET /v1/summary/tokens` : ne les codez pas en dur.
- `GET /v1/balances` a une clé par token (`balance_<nom>`), partagée par tous les réseaux ; la clé de chaque token est le `balance_key` de `GET /v1/summary/tokens`. Parcourez les clés au lieu de les coder en dur. Quand `available` contient la clé, utilisez-le : le reste est retenu par des dépôts pas encore définitifs.
- `POST /v1/payout` accepte un objet ou une liste de 50 au plus et répond toujours avec `summary` et `results[]`, une entrée par élément dans l'ordre d'envoi : 200 quand tous ont été créés, 207 quand certains ont été refusés, 400 quand aucun n'a été créé. Un élément refusé porte `results[].error.code`. Donnez à chaque payout son propre `reference_id` (un identifiant réutilisé est refusé avec `duplicate_payment_reference`, jamais payé deux fois) et envoyez un `Idempotency-Key` par requête (conservé 24 heures).
- `POST /v1/payin` n'a pas d'Idempotency-Key : c'est le `reference_id` unique qui empêche un second payin. Après un timeout, renvoyez avec le même `reference_id` ; `duplicate_reference_id` signifie que le premier a été créé, et `GET /v1/list-payin?reference_id=…` le renvoie.
- Conservez l'`internal_reference` renvoyé par chaque création : les endpoints de consultation le prennent dans le chemin, et les webhooks le portent.
- Un payin passe à `completed` quand `received_amount` atteint `minimum_payment` pour cent de `crypto_amount`, ce qui peut être moins que le montant total. Comparez `received_amount` à `crypto_amount` avant de livrer.
- Chaque erreur contient `error.code`, `error.message` et, si utile, `error.details`. Décidez selon `error.code`, jamais selon le message. 4xx : modifiez la requête avant de la renvoyer. 5xx : la même requête peut être répétée. 429 : attendez les secondes de `Retry-After`.
- Les webhooks sont des POST avec un corps JSON vers votre `webhook_url`. Avant de faire confiance au corps, vérifiez `X-Bifrost-Signature` : `v1=` suivi du HMAC-SHA256 en hexadécimal de `v1`, `X-Bifrost-Timestamp`, `X-Bifrost-Delivery` et du SHA-256 en hexadécimal du corps brut, joints par `\n`, avec la clé de webhook du compte (dans le panneau). Refusez un timestamp décalé de plus de 300 secondes, comparez en temps constant, répondez 2xx en moins de 30 secondes et traitez ensuite.
- Décidez selon le champ `event` du corps, pas selon `X-Bifrost-Webhook-Type`. Payouts : `payment_processed`, `payment_failed`. Payins : `payment_detected` (`funds_available` false, pas encore définitif), `payment_confirmed` (définitif, fonds disponibles), `payment_reversed`. Le même avis peut arriver plusieurs fois : dédupliquez sur `data.internal_reference` avec `event`.
- Les endpoints de liste et de consultation ne renvoient que les payouts et payins créés via l'API ; ceux faits dans le panneau ne sont pas renvoyés.
- Chaque clé a sa propre limite de requêtes, 500 par 60 secondes par défaut, indiquée dans `X-RateLimit-Limit`, `X-RateLimit-Remaining` et `X-RateLimit-Reset`.
- Il n'y a pas de sandbox public : chaque appel agit sur le compte réel de la clé. Testez avec de petits montants.
- Les changements de contrat, y compris ceux qui cassent une intégration, sont listés dans le Changelog, du plus récent au plus ancien.

### Endpoints

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

## Sommaire

- API Bifrost Crypto: https://bifrostcrypto.com/ai/fr/introduction.md
- Authentification et Contrôle d'Accès: https://bifrostcrypto.com/ai/fr/authentication.md
- Recevoir un paiement: https://bifrostcrypto.com/ai/fr/guide-payin.md
- Envoyer un payout: https://bifrostcrypto.com/ai/fr/guide-payout.md
- Vérification des webhooks: https://bifrostcrypto.com/ai/fr/webhooks.md
- Webhooks de retrait: https://bifrostcrypto.com/ai/fr/webhooks-payments.md
- Webhooks de dépôt: https://bifrostcrypto.com/ai/fr/webhooks-receipts.md
- Codes d'Erreur: https://bifrostcrypto.com/ai/fr/error-codes.md
- Référence de l'API · Payments Payout
- Référence de l'API · Deposits Payin
- Référence de l'API · Queries
- Référence de l'API · Prices
- Référence de l'API · Fixed Wallets
- Changelog: https://bifrostcrypto.com/ai/fr/changelog.md
- Recettes d'intégration (pas à pas pour les agents)

---

## API Bifrost Crypto

Référence complète pour l'API Bifrost Crypto V1. Intégrez des paiements crypto multi-chaîne dans votre plateforme en minutes.

### Authentification

Toutes les routes nécessitent l'en-tête avec votre clé API privée : `X-Bifrost-Invoke`

### URL de Base

Toutes les requêtes sont effectuées vers : `https://bridge.bifrostcrypto.com`

### Format

Tous les corps de requête et de réponse utilisent le format JSON : `Content-Type: application/json`

### Démarrage Rapide

Chaque requête API doit inclure votre clé privée dans l'en-tête :

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

### Spécification OpenAPI

Toute la référence dans un fichier OpenAPI 3.1. Importez-le dans Postman ou Insomnia pour essayer les endpoints, ou générez un client dans votre langage. Il est construit à partir de la même source que cette documentation.

https://bifrostcrypto.com/openapi.json

### Dates et heures

Les champs de date et heure (created_at, updated_at, expires_at et les autres) sont au format YYYY-MM-DD HH:MM:SS en UTC−03:00, sans le décalage dans la valeur. Les filtres de date (date_from, date_to) sont lus avec le même décalage. Deux exceptions : la limite quotidienne de payout d'une clé d'API repart à 00:00 UTC, et les webhooks d'adresse fixe envoient timestamp en ISO 8601 avec le décalage inclus.

### Réseaux Pris en Charge

Valeurs acceptées dans le champ network, sans tenir compte de la casse à la création. À la création, des alias courants sont aussi acceptés (erc20, bsc, trc20, matic…) et convertis vers ces noms ; voir Network aliases sur les endpoints de création. Les réponses, webhooks et filtres de liste utilisent toujours les noms ci-dessous. Les tokens disponibles sur chaque réseau sont indiqués par 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`

### Codes de Statut HTTP

| Code | Signification |
| --- | --- |
| `200` | Succès |
| `201` | Créé - adresse fixe attribuée |
| `207` | Multi-Status - lot de payout avec des éléments refusés |
| `400` | Requête Invalide - paramètres invalides |
| `401` | Non Autorisé - clé API ou signature absente ou invalide |
| `403` | Interdit - aucune permission sur cette ressource |
| `404` | Non Trouvé - ressource ou route inexistante |
| `409` | Conflit - requête encore en cours ou limite atteinte |
| `422` | Non Traitable - type non pris en charge |
| `429` | Trop de Requêtes - quota ou limite d'échecs d'authentification |
| `500` | Erreur Serveur |
| `502` | Bad Gateway - un service dont nous dépendons a échoué. Réessayez plus tard. |
| `503` | Service Indisponible - réessayez plus tard |

### Catégories d'API

- **Paiements (Payout)** (3 endpoints): Créez et gérez les retraits crypto. Jusqu'à 50 par lot.
- **Dépôts (Payin)** (3 endpoints): Générez des portefeuilles de dépôt et suivez les paiements entrants.
- **Requêtes** (6 endpoints): Vérifiez les soldes, les relevés, les tokens et les informations de compte.
- **Prix** (2 endpoints): Obtenez les taux de conversion en temps réel et les devises disponibles.
- **Adresses Fixes** (5 endpoints): Attribuez et gérez des adresses de dépôt permanentes par client.

## Authentification et Contrôle d'Accès

Chaque requête /v1 est authentifiée par clé d'API. La clé peut en outre être restreinte par portée, par IP source et par signature de la requête.

### Clé d'API

Envoyez votre clé dans l'en-tête X-Bifrost-Invoke à chaque requête. Il n'y a ni étape de connexion ni jeton à renouveler : la clé est la crédence.

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

L'accès est revérifié à chaque requête. Révoquer la clé, la laisser expirer ou voir l'accès API désactivé sur le compte prend effet dès la requête suivante, sans attendre l'expiration d'un cache.

- En-tête absent : 401 api_key_missing. Clé inconnue, révoquée ou expirée : 401 api_key_invalid.
- Accès API désactivé pour le compte : 403 api_access_denied.
- E-mail du compte non vérifié : 403 email_not_verified. Vérifiez l'e-mail dans le panneau ; la clé fonctionne dès la requête suivante.
- Compte suspendu : les clés sont conservées mais répondent 401 api_key_invalid pendant la suspension, et fonctionnent de nouveau à la réactivation du compte.

### Portées

Une portée est l'autorisation d'atteindre un groupe de points de terminaison. À la création d'une clé, vous choisissez les portées qu'elle porte : une clé servant uniquement à lire les soldes ne peut pas créer un retrait, même si elle fuite.

> Une clé créée sans liste de scopes est en lecture seule : payout:read, payin:read, balances:read, statements:read et prices:read. Elle n'a aucun scope d'écriture ni d'adresses fixes.

| Portée | Donne accès à |
| --- | --- |
| `payout:read` | Lister les retraits et consulter le statut d'un retrait |
| `payout:write` | Créer des retraits |
| `payin:read` | Lister les dépôts et consulter le statut d'un dépôt |
| `payin:write` | Créer des dépôts |
| `balances:read` | Lire les soldes du compte |
| `statements:read` | Lire les relevés, les résumés, les jetons acceptés et les monnaies fiduciaires acceptées |
| `prices:read` | Lire les cotations et le catalogue des devises |
| `wallets:read` | Lister les portefeuilles fixes et lire leur configuration |
| `wallets:write` | Créer des portefeuilles fixes et modifier leur configuration |

Appeler un endpoint hors des portées de la clé renvoie 403 en indiquant quelles portées conviendraient, pour que la correction soit visible sans deviner. Certains endpoints en acceptent plusieurs, et en détenir une seule suffit :

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

Sur les clés portant une portée d'écriture (payout:write, payin:write, wallets:write), il vaut la peine d'ajouter une seconde couche à la portée : une liste blanche d'IP si votre intégration appelle depuis une adresse fixe, ou la signature de la requête sinon. Aucune des deux n'est obligatoire, mais elles existent parce qu'une crédence qui déplace de l'argent mérite mieux qu'un secret statique.

### Liste blanche d'IP

> La liste blanche est facultative, quelle que soit la portée. Sans elle, la clé est acceptée depuis n'importe quelle origine, et rien de ce que vous exécutez déjà n'a besoin de changer. C'est une couche supplémentaire pour ceux qui appellent depuis une adresse fixe ; si votre intégration n'a pas d'IP fixe, utilisez require_signature, qui protège une clé divulguée sans dépendre de l'origine.

La clé peut être restreinte à un ensemble d'adresses sources. Les adresses individuelles et les plages en notation CIDR sont acceptées, en IPv4 comme en IPv6 :

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

Une requête arrive par une seule famille d'adresses à la fois. Si votre infrastructure nous atteint en IPv4 et en IPv6, déclarez les deux, car une entrée d'une famille n'autorise pas l'autre. Les requêtes hors de la liste sont refusées avec 403 ip_not_authorized.

### Signature de la Requête (HMAC)

> La signature est facultative et s'active par clé, au moment de la création. Les clés qui ne l'exigent pas continuent de s'authentifier avec le seul X-Bifrost-Invoke, sans en-tête supplémentaire, et rien de ce que vous exécutez déjà n'est affecté.

Facultative par clé. La clé d'API seule prouve que l'appelant connaît un secret statique, mais elle ne prouve ni que la requête est récente, ni que le corps est intact, ni que cet appel n'a pas déjà eu lieu. Avec la signature activée, chaque requête porte une preuve valable une seule fois et sur une courte fenêtre.

| En-tête | Description |
| --- | --- |
| `X-Bifrost-Timestamp` | Heure Unix en secondes entières, chiffres uniquement, au moment où la requête a été construite. Refusée si elle s'écarte de l'heure du serveur de plus de 300 secondes. |
| `X-Bifrost-Nonce` | Valeur unique par requête, de 16 à 80 caractères parmi A-Z, a-z, 0-9, tiret bas et trait d'union. Acceptée une fois par clé. |
| `X-Bifrost-Signature` | HMAC-SHA256 de la chaîne canonique, avec votre secret de signature comme clé, en hexadécimal. Envoyez-le en minuscules ; les majuscules sont aussi acceptées. |

#### La chaîne canonique

Construisez ces six champs joints par un unique caractère de saut de ligne, exactement dans cet ordre :

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

METHOD en majuscules. PATH est le chemin exactement tel qu'il figure dans la ligne de requête, avec la barre initiale et le préfixe /v1 (par exemple /v1/check-payout/f47ac10b-58cc-4372-a567-0e02b2c3d479) : il n'est pas décodé et la barre finale est conservée. QUERY est la query string brute telle qu'envoyée, sans point d'interrogation initial, ni triée ni réencodée, et vide en l'absence de paramètres. Le dernier champ est le SHA-256 hexadécimal des octets exacts du corps envoyé. Pour les requêtes sans corps, hachez la chaîne vide, ce qui donne e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.

#### Signer une requête

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

#### Points de vigilance

- Le secret de signature est affiché une seule fois, à la création de la clé. Il n'est jamais renvoyé, conservez-le donc avant de fermer le panneau.
- Le secret est distinct de la clé d'API. Les deux sont nécessaires : la clé vous identifie, la signature prouve la requête.
- Hachez les octets que vous transmettez réellement. Sérialiser le corps deux fois, ou le re-sérialiser après signature, change le condensé et invalide la signature.
- Chaque nonce est accepté une fois par clé. Renvoyer la requête à l'identique renvoie signature_replayed même si tout le reste est correct.
- Le décalage d'horloge est la cause la plus fréquente de signature_timestamp_out_of_range. Gardez vos serveurs sur NTP.

### Limites de Requêtes

Chaque clé a son propre quota sur une fenêtre fixe alignée sur l'horloge : par défaut 500 requêtes par 60 secondes, défini à la création de la clé. Les réponses portent l'état courant, pour que vous régliez votre consommation au lieu de découvrir la limite en la heurtant :

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

- X-RateLimit-Limit est le quota, X-RateLimit-Remaining ce qui reste dans la fenêtre et X-RateLimit-Reset l'heure Unix de réinitialisation de la fenêtre.
- Les en-têtes accompagnent toute réponse ayant passé l'authentification et la vérification de scope, y compris les réponses 4xx et 5xx de l'endpoint.
- Ils sont absents des refus d'authentification et de scope, ainsi que pour les clés configurées sans quota, qui ne sont pas limitées.

Au-delà du quota, les requêtes renvoient 429 rate_limit_exceeded avec un en-tête Retry-After jusqu'à la réinitialisation de la fenêtre. Aucune pénalité supplémentaire :

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

#### Échecs répétés d'authentification

Indépendamment du quota par clé, une adresse IP qui échoue à s'authentifier de façon répétée est bloquée pendant un temps. Pendant le blocage, elle reçoit 429 too_many_failed_authentications avec l'en-tête Retry-After : attendez ce nombre de secondes avant de réessayer. Les requêtes réussies ne comptent jamais pour ce blocage.

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

## Recevoir un paiement

Du choix du token à la livraison de la commande, avec des payins créés par l'API.

### 1. Choisissez le token et le réseau

GET /v1/summary/tokens liste les tokens de la plateforme par réseau, avec min_receipt et max_receipt. La liste n'est pas filtrée par votre compte : la création du payin confirme si le token accepte les dépôts pour vous sur ce réseau.

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

### 2. Créez le payin

POST /v1/payin avec un reference_id unique dans votre système, le montant (process_by crypto_amount ou fiat_amount), webhook_url et, si vous le souhaitez, expires_at en secondes (4 heures par défaut). Conservez l'internal_reference de la réponse et montrez au client la wallet, et le memo quand le token l'exige.

```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. Recevez le webhook

Vérifiez X-Bifrost-Signature sur le corps brut, répondez 2xx en moins de 30 secondes et traitez ensuite. Le même avis peut arriver plusieurs fois : dédoublonnez sur data.internal_reference avec event.

### 4. Vérifiez le montant avant de livrer

Le payin est terminé dès que received_amount atteint minimum_payment pour cent de crypto_amount, ce qui peut être moins que le total. Comparez data.received_amount à data.crypto_amount et décidez quoi faire de la différence.

### 5. Choisissez quand livrer

payment_detected signifie que le transfert a été vu (funds_available false) : livrer maintenant est votre décision et comporte un risque d'annulation. payment_confirmed signifie qu'il est définitif (funds_available true).

### 6. Traitez les exceptions

payment_reversed : le réseau a annulé le paiement et le crédit a été retiré ; check-payin renvoie désormais le statut reversed. is_duplicated true : un paiement supplémentaire ou sur un autre réseau a créé un nouveau payin avec ses propres références ; rattachez-le à la commande par customer_info.

### 7. Rapprochez

GET /v1/check-payin/{internal_reference} renvoie le payin à tout moment. GET /v1/statements liste les mouvements du solde : DEPOSIT pour le crédit, RATE et FEE pour les frais.

### Que faire en cas d'échec

POST /v1/payin n'a pas d'Idempotency-Key. C'est le reference_id qui empêche un second payin.

| Situation | Que faire |
| --- | --- |
| Timeout ou pas de réponse sur POST /v1/payin | Renvoyez avec le même reference_id. Si la réponse est duplicate_reference_id, la première requête a abouti : récupérez-la avec GET /v1/list-payin?reference_id=… |
| 400 | Corrigez la requête. La renvoyer telle quelle donne la même réponse. |
| 500, 502 ou 503 | Rien n'a été créé. Réessayez plus tard avec le même reference_id. |
| 429 | Attendez le nombre de secondes indiqué par Retry-After. |
| Le webhook n'est pas arrivé | Les envois sont répétés jusqu'à 10 fois, à environ 5 minutes d'intervalle. En attendant, lisez le payin avec GET /v1/check-payin/{internal_reference}. |

> Les payins créés dans le panneau ne sont pas renvoyés par l'API.

## Envoyer un payout

De la vérification du solde à la confirmation du transfert, pour un payout ou un lot de jusqu'à 50.

### 1. Vérifiez le solde

GET /v1/balances renvoie un solde par token ; la clé de chaque token est le balance_key renvoyé par GET /v1/summary/tokens. Quand available contient la clé, utilisez cette valeur : le reste est bloqué par des dépôts pas encore définitifs. min_withdrawal et max_withdrawal par token viennent de GET /v1/summary/tokens.

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

### 2. Créez le payout

POST /v1/payout avec un objet ou une liste de jusqu'à 50. Donnez à chaque payout son propre reference_id et envoyez un Idempotency-Key par requête. feetakenfromamount 1 retire les frais du montant ; 0 les ajoute au débit.

```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. Lisez le résultat de chaque élément

200 signifie que tous les éléments ont été créés, 207 une partie, 400 aucun. results contient une entrée par élément, dans l'ordre d'envoi, avec son index. Conservez l'internal_reference de chaque payout créé.

### 4. Recevez le webhook

payment_processed (data.status success, avec le hash de la transaction dans data.transactions) quand le transfert est fait, ou payment_failed quand il ne peut pas aboutir. Vérifiez la signature et dédoublonnez sur data.internal_reference avec event.

### 5. Rapprochez

GET /v1/check-payout/{internal_reference} renvoie le payout à tout moment. GET /v1/statements liste WITHDRAWAL pour le débit, et RATE et FEE pour les frais.

### Que faire en cas d'échec

Deux choses vous empêchent de payer deux fois : l'Idempotency-Key de la requête et le reference_id de chaque payout.

| Situation | Que faire |
| --- | --- |
| Timeout ou pas de réponse | Renvoyez le même corps avec le même Idempotency-Key dans les 24 heures : vous recevez la réponse d'origine et rien n'est retraité. |
| 409 idempotency_key_in_progress | La première requête est encore en cours. Attendez et réessayez avec la même clé. |
| 207 | Corrigez et renvoyez seulement les éléments refusés, avec un nouvel Idempotency-Key. Ceux qui ont été créés le restent. |
| 500 avec summary.not_processed | Renvoyez les éléments non traités. Un reference_id déjà utilisé est refusé avec duplicate_payment_reference, jamais payé deux fois. |
| duplicate_payment_reference | Le payout existe déjà. Utilisez l'internal_reference et l'original_status du résultat. |
| 429 | Attendez le nombre de secondes indiqué par Retry-After. |

> Les payouts créés dans le panneau ne sont pas renvoyés par l'API.

## Vérification des webhooks

Confirmez que l'avis a été envoyé par Bifrost avant de faire confiance au corps.

### Headers Envoyés dans les Webhooks

Tous les webhooks sont envoyés avec des headers spéciaux pour validation et identification:

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

X-Bifrost-Signature, X-Bifrost-Timestamp et X-Bifrost-Delivery accompagnent chaque livraison. Ce sont eux qui prouvent l'origine du message.

### Types de webhooks

Le champ event du corps indique ce qui s'est passé. Voici toutes les valeurs qu'il peut prendre :

| event | Description |
| --- | --- |
| `payment_processed` | Payout terminé. data.status vaut success. X-Bifrost-Webhook-Type : payment_processed. |
| `payment_failed` | Le payout n'a pas pu aboutir. data ne contient que status (failed), reference_id et internal_reference. X-Bifrost-Webhook-Type : payment_processed. |
| `payment_detected` | Dépôt vu sur le réseau ; funds_available vaut false. X-Bifrost-Webhook-Type : payment_confirmed. |
| `payment_confirmed` | Dépôt final ; funds_available vaut true. X-Bifrost-Webhook-Type : payment_confirmed. |
| `payment_reversed` | Une réorganisation a annulé le dépôt et le crédit a été retiré. data.status vaut reversed, et check-payin renvoie désormais le statut reversed. X-Bifrost-Webhook-Type : payment_confirmed. |
| `fixed_address_pre_revoke · fixed_address_revoked` | Cycle de vie de l'adresse fixe, décrit à la fin de cette page. X-Bifrost-Webhook-Type : fixed_address_lifecycle. |

X-Bifrost-Webhook-Type ne nomme que la famille de l'avis : tout événement de dépôt, y compris payment_detected et payment_reversed, arrive avec payment_confirmed dans cet en-tête, et un payout échoué avec payment_processed. Décidez toujours selon le champ event du corps.

### Livraison et renvois

Chaque avis est un POST avec un corps JSON vers la webhook_url que vous avez enregistrée. Ce qui compte comme livré, et ce qui se passe sinon :

- Répondez avec n'importe quel statut 2xx en moins de 30 secondes (la connexion elle-même doit s'ouvrir en moins de 10 secondes). Tout autre statut, y compris 3xx, un timeout ou une erreur de connexion, compte comme un échec.
- Les redirections ne sont pas suivies. Le certificat TLS est vérifié, HTTPS est obligatoire en production, et la connexion se fait en IPv4 vers l'adresse validée pour votre hôte.
- Une livraison échouée est retentée environ 5 minutes plus tard, jusqu'à 10 tentatives au total. Après le dixième échec, l'avis est marqué failed et peut être renvoyé depuis le panneau.
- Chaque tentative porte le même corps, un X-Bifrost-Delivery et un X-Bifrost-Timestamp nouveaux, et une signature calculée pour eux.
- Le même avis peut donc vous parvenir plusieurs fois. Dédupliquez sur data.internal_reference avec event (pour les avis d'adresse fixe, wallet avec event).
- Répondez d'abord et traitez ensuite : effectuer un travail lent avant de répondre est la façon habituelle de dépasser la limite de 30 secondes.

### Le Sceau de Heimdall

Comment confirmer que le message vient bien du pont

L'adresse qui reçoit vos webhooks est publique : quiconque découvre l'URL peut lui envoyer une requête. Sans vérification, un POST falsifié annonçant un paiement approuvé est indiscernable d'un vrai, et il se traduit souvent par une marchandise livrée sans que l'argent soit entré.

#### La clé de signature

Chaque compte dispose d'une clé dédiée, générée automatiquement et jamais transmise. Nous utilisons notre copie pour signer chaque livraison ; vous récupérez la vôtre une fois et la conservez. Comme elle ne voyage pas avec le webhook, une fuite de journaux dans votre environnement ne livre pas la clé, et c'est ce qui donne à la signature sa valeur de preuve.

#### Obtenir la clé

La clé existe déjà sur votre compte et se trouve dans le panneau, dans Paramètres, à la section des webhooks. L'afficher requiert votre second facteur. Copiez la valeur sur votre serveur et conservez-la comme un mot de passe de base de données : dans une variable d'environnement ou un coffre à secrets, jamais dans le code versionné.

> Le panneau permet aussi de remplacer la clé, et ce remplacement invalide immédiatement la précédente. Entre la génération de la nouvelle et la mise à jour de votre copie, les webhooks sont signés avec une clé que vous n'avez pas encore, et votre vérification échouera durant cet intervalle.

#### En-têtes de la signature

| Champ | Description |
| --- | --- |
| `X-Bifrost-Signature` | v1= suivi du HMAC-SHA256 en hexadécimal. |
| `X-Bifrost-Timestamp` | Horodatage Unix du moment où le webhook a été émis. |
| `X-Bifrost-Delivery` | 32 caractères hexadécimaux minuscules, uniques pour cette tentative de livraison. Change à chaque renvoi. |

#### La chaîne signée

Le HMAC n'est pas calculé sur le corps brut, mais sur cette chaîne, assemblée avec des sauts de ligne entre les champs :

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

L'horodatage et l'identifiant de livraison entrent volontairement dans le calcul. Si seul le corps était signé, ces deux en-têtes pourraient être réécrits par n'importe qui sans invalider la signature, et un webhook capturé aujourd'hui serait rejoué des mois plus tard en paraissant fraîchement émis.

#### Vérification

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

#### Précautions

- Utilisez le corps brut. Désérialiser le JSON puis le resérialiser modifie les espaces et l'ordre des clés, et l'empreinte ne correspond plus. Capturez les octets exactement tels qu'ils sont arrivés, avant toute analyse.
- Comparez en temps constant. hash_equals, timingSafeEqual et compare_digest existent pour cela : une comparaison de chaînes ordinaire révèle, par le temps de réponse, combien de caractères initiaux sont corrects.
- Gérez les renvois. Une livraison qui ne reçoit pas de 2xx est retentée, avec un X-Bifrost-Delivery différent à chaque fois. Le corps n'a pas d'identifiant d'événement : dédupliquez sur data.internal_reference avec event, pour ne pas traiter deux fois le même paiement.
- HTTPS est obligatoire en production. Les URL http:// sont refusées aussi bien à l'enregistrement qu'à l'envoi.

#### Webhook sans signature

Si l'en-tête X-Bifrost-Signature est absent, ne traitez pas le message et contactez le support.

### Cycle de vie de l'adresse fixe

Les adresses fixes qui ne reçoivent rien pendant longtemps sont reprises. Avant et au moment où cela se produit, un avis est envoyé à l'URL de webhook des adresses fixes configurée dans le panneau, pas à une URL par paiement ; sans cette URL, aucun avis n'est envoyé. Les adresses de type evm sont permanentes et ne reçoivent jamais ces événements. Le corps est plat, sans enveloppe data, et signé comme tout autre webhook.

| event | Description |
| --- | --- |
| `fixed_address_pre_revoke` | Après 120 jours sans transaction. revoke_at indique quand l'adresse sera révoquée (environ 30 jours plus tard) sauf si elle reçoit une transaction avant. |
| `fixed_address_revoked` | Après 150 jours sans transaction. L'adresse n'appartient plus à votre compte ; status vaut revoked et revoke_at vaut 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"
}
```

| Champ | Description |
| --- | --- |
| `event` | fixed_address_pre_revoke ou fixed_address_revoked |
| `wallet` | L'adresse fixe |
| `network · token` | Réseau et token de l'adresse ; token peut être null |
| `provider` | Réservé. Ne vous y fiez pas. |
| `customer_info` | Le customer_info fourni lors de la demande d'adresse, ou null |
| `status` | Statut de l'adresse au moment de l'avis (STATUS_ASSIGNED en pré-révocation), ou revoked |
| `last_transaction_at · assigned_at` | Dernière transaction reçue et date d'attribution ; l'inactivité est comptée depuis last_transaction_at, ou depuis assigned_at si l'adresse n'a jamais reçu de transaction |
| `revoke_at` | Date de révocation prévue sur fixed_address_pre_revoke ; null sur fixed_address_revoked |
| `message` | Explication lisible, en anglais |
| `timestamp` | Moment où l'avis a été généré, en ISO 8601 |

## Webhooks de retrait

Avis d'un payout créé par l'API.

### `payment_processed` · Payout Confirmé

Envoyé lorsque le retrait est traité. status vaut success. Le corps porte le portefeuille, les montants, le réseau et 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 Échoué

Envoyé lorsque le retrait ne peut pas aboutir. Le corps ne porte que status, reference_id et 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"
}
```

> Votre intégration doit traiter cet avis : retrouvez le payout par reference_id ou internal_reference et décidez quoi indiquer à votre client. Pour payer à nouveau, créez un nouveau payout avec un nouveau reference_id.

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

Votre propre identifiant du client, comme un ID utilisateur, un e-mail ou un numéro de commande. Il est renvoyé dans les webhooks et les listes, pour savoir à qui appartient chaque transaction, y compris les payins que Bifrost crée lui-même (paiements supplémentaires et dépôts sur adresse fixe), dont le reference_id est généré par Bifrost.

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

## Webhooks de dépôt

Notifications d'un payin, du moment où le transfert apparaît sur la chaîne jusqu'à ce que le montant soit disponible sur votre solde.

### Paiements partiels

Un dépôt est marqué completed, et payment_detected / payment_confirmed sont envoyés, dès que received_amount atteint minimum_payment pour cent de crypto_amount. minimum_payment est défini par compte ; en dessous de 100, le client peut avoir payé moins que la facture. Avant de livrer, comparez data.received_amount à data.crypto_amount (tous deux en data.crypto_currency) et décidez quoi faire de la différence.

### `payment_detected` · Paiement détecté

La transaction est apparue sur le réseau et le payin est terminé, mais ce n'est pas le statut final. funds_available vaut false et finality_status vaut pending_finality tant que le bloc peut encore être réorganisé. Si vous devez aller vite, vous pouvez livrer le produit sur cet avis. Le montant reste bloqué et ne fait pas partie du solde disponible tant que le réseau n'a pas atteint les confirmations requises.

> Livrer le produit à cette étape est votre décision. Un retrait de ce montant est refusé jusqu'au payment_confirmed. À ce jour, aucun paiement détecté par Bifrost n'a été annulé ; voir 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` · Paiement confirmé

Le bloc est définitif. funds_available vaut true et le montant est disponible sur votre solde. Créditez le client définitivement sur cet événement.

```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` · Paiement annulé

Soyez rassuré : à ce jour, aucun paiement reçu par Bifrost n'a jamais été annulé.

Cet avis existe uniquement à cause des réseaux L2, comme Arbitrum, Optimism et Base. Sur ces réseaux, le paiement est détecté dès qu'il entre dans un bloc de la L2 (payment_detected), en quelques secondes, et devient définitif quand ce bloc est finalisé sur le réseau principal (payment_confirmed), quelques minutes plus tard. En théorie, une transaction peut être défaite dans cet intervalle. C'est extrêmement rare, mais comme la possibilité existe, nous la documentons.

Si cela arrive, le crédit de ce dépôt est retiré du solde, data.status vaut reversed et reversal_reason en indique la raison. À partir de là, check-payin et list-payin renvoient aussi le statut reversed.

Si vous ne livrez le produit qu'à payment_confirmed, cet avis ne vous concerne pas. Si vous livrez à payment_detected, rattachez simplement cet avis à la commande par 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"
}
```

### Quand la détection est déjà définitive

Sur certains réseaux, le paiement est définitif dès sa détection. payment_confirmed est alors envoyé juste après payment_detected ; payment_detected porte déjà finality_status final, et payment_confirmed peut porter finality_status null. Décidez d'après event et funds_available : tous les payment_detected ne bloquent pas le montant.

### Champs du dépôt

| Champ | Type | Description |
| --- | --- | --- |
| `funds_available` | `boolean` | false tant que le montant est bloqué. true quand il peut être retiré. |
| `finality_status` | `string \| null` | pending_finality pendant les confirmations, final quand le bloc est consolidé, reorged quand le dépôt a été annulé. Peut être null sur un payment_confirmed d'un paiement déjà définitif à la détection. |
| `confirmations` | `number \| null` | Réservé au nombre de confirmations. Actuellement toujours null ; ne vous y fiez pas. |
| `estimated_release_seconds` | `number \| null` | Réservé à l'estimation de l'attente avant libération. Actuellement toujours null ; ne vous y fiez pas. |
| `reversal_reason` | `string \| null` | Pourquoi le crédit a été retiré. Null sur payment_detected et payment_confirmed. |
| `received_amount` | `string` | Montant crypto crédité sur le reçu. |
| `received_amount_fiat` | `string` | Valeur fiat du montant reçu au cours de la facture. |
| `received_amount_fiat_current` | `string` | Valeur fiat au cours du moment où le webhook a été construit. Repli sur le cours de la facture si le prix en direct est indisponible. |
| `cross_token` | `boolean` | true lorsque le client a payé avec un jeton différent de celui de la facture. |
| `paid_with` | `array` | Jetons réellement reçus on-chain. Chaque élément a currency et amount. |
| `is_wallet_fixed` | `boolean` | true lorsque le dépôt est arrivé sur une adresse fixe. |

**`is_duplicated`** (Payin Seulement)

true quand Bifrost a créé ce payin lui-même, pour un paiement qui ne correspondait à aucun payin ouvert : un paiement supplémentaire vers une adresse dont le payin était déjà terminé, ou un paiement sur un autre réseau que celui de la facture. C'est un nouveau payin, avec son propre internal_reference et un reference_id généré par Bifrost. Le montant est crédité sur votre solde et l'avis est envoyé au webhook_url du payin d'origine.

> Quand is_duplicated vaut true, le reference_id n'est pas le vôtre. Rattachez le paiement à votre commande par customer_info ou wallet. Le payin est aussi renvoyé par List Payins et Check Specific Payin.

**`customer_info`**

Votre propre identifiant du client, comme un ID utilisateur, un e-mail ou un numéro de commande. Il est renvoyé dans les webhooks et les listes, pour savoir à qui appartient chaque transaction, y compris les payins que Bifrost crée lui-même (paiements supplémentaires et dépôts sur adresse fixe), dont le reference_id est généré par Bifrost.

## Codes d'Erreur

Tous les codes d'erreur renvoyés par l'API, regroupés par type, avec la conduite à tenir pour chacun.

### Format de l'erreur

Toute erreur /v1 a la même forme, avec le statut HTTP des tableaux ci-dessous. Comparez error.code, qui est un snake_case stable. error.message est un texte libre en anglais pour vos logs. error.details n'apparaît que s'il y a des données utiles, comme des champs invalides, des limites ou des scopes.

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

Dans un lot POST /v1/payout, chaque élément refusé porte le même objet dans results[].error, à côté de son index et de son reference_id. La réponse est 200 quand tous les éléments ont réussi, 207 quand certains ont réussi et d'autres échoué, et 400 quand aucun n'a réussi :

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

- Les messages de validation se trouvent toujours dans error.details.fields, indexés par nom de champ, au premier niveau comme dans un élément du lot.
- Un élément de payout en double compte comme un échec : il porte error.code duplicate_payment_reference ainsi que duplicate, original_status et original_created_at dans l'élément. Un lot composé uniquement de doublons répond 400.
- Décidez selon error.code, jamais selon error.message : les messages peuvent être reformulés, les codes ne changent pas.
- 4xx signifie que la requête doit être modifiée avant d'être renvoyée. 5xx signifie que l'échec vient de notre côté et que la même requête peut être réessayée ; sur POST /v1/payout, le reference_id vous protège toujours d'un double retrait.
- Un chemin /v1 inconnu renvoie 404 route_not_found en JSON. Un client qui demande text/html dans Accept reçoit une page HTML.

### Erreurs d'Authentification et d'Accès

| Code HTTP | Erreur | Description |
| --- | --- | --- |
| `401` | `api_key_missing` | L'en-tête X-Bifrost-Invoke n'a pas été envoyé. |
| `401` | `api_key_invalid` | Clé API inconnue, révoquée ou expirée. Également renvoyé tant que le compte est suspendu. |
| `403` | `api_access_denied` | L'accès à l'API est désactivé pour ce compte. Vérifié à chaque requête. |
| `403` | `email_not_verified` | L'adresse e-mail du compte n'a pas encore été vérifiée. |
| `403` | `ip_not_authorized` | L'IP source n'est pas dans la whitelist de la clé. |
| `400` | `invalid_ip` | Impossible de déterminer l'adresse IP du client. |
| `401` | `signature_headers_missing` | La clé exige une signature et X-Bifrost-Signature, X-Bifrost-Timestamp ou X-Bifrost-Nonce est absent. |
| `401` | `signature_timestamp_invalid` | X-Bifrost-Timestamp ne contient pas uniquement des chiffres (secondes Unix). |
| `401` | `signature_timestamp_out_of_range` | Le timestamp s'écarte de l'heure du serveur de plus de 300 secondes. |
| `401` | `signature_nonce_length_invalid` | Le nonce fait moins de 16 ou plus de 80 caractères. |
| `401` | `signature_nonce_charset_invalid` | Le nonce contient des caractères hors de A-Z, a-z, 0-9, tiret bas et tiret. |
| `401` | `signature_invalid` | La signature ne correspond pas à la chaîne canonique. |
| `401` | `signature_replayed` | Ce nonce a déjà été utilisé par cette clé. |
| `401` | `signature_secret_unavailable` | La clé exige une signature mais n'a pas de secret utilisable. Contactez le support. |
| `403` | `scope_denied` | La clé n'a pas le scope exigé par cet endpoint. details.required_scopes liste ceux qui conviennent et details.granted_scopes ceux de la clé. |
| `429` | `rate_limit_exceeded` | Le quota de la clé pour la fenêtre en cours est épuisé. Retry-After et details.retry_after_seconds indiquent quand réessayer. |
| `429` | `too_many_failed_authentications` | Cette IP est temporairement bloquée après des échecs d'authentification répétés. Envoyé avec Retry-After. |
| `401` | `user_not_authenticated` | La requête n'a pas pu être rattachée à votre clé d'API. Réessayez ; si cela se reproduit, contactez le support. |

### Erreurs de Requête

| Code HTTP | Erreur | Description |
| --- | --- | --- |
| `404` | `route_not_found` | Aucune route /v1 ne correspond au chemin et à la méthode. Répondu avant l'authentification. |
| `400` | `json_parse_exception` | Le corps de la requête n'est pas un JSON valide. Toute route /v1. |
| `400` | `json_parse_failed` | POST /v1/payout : le corps a été décodé en null ou false. |
| `400` | `invalid_parameters` | Paramètres invalides ou manquants. details.fields associe chaque champ à son message (POST /v1/payout avec tous les éléments invalides, POST /v1/payin, adresses fixes, filtres de relevés). |
| `400` | `invalid_payment_list` | POST /v1/payout : le corps est vide ou n'est ni un objet payout ni une liste de payouts. |
| `400` | `max_payouts_exceeded` | POST /v1/payout : plus de 50 payouts dans une requête. |
| `409` | `idempotency_key_in_progress` | Une requête avec cette Idempotency-Key est encore en cours. Attendez et réessayez avec la même clé. |
| `400` | `crypto_amount_required` | POST /v1/payin : process_by vaut crypto_amount et crypto_amount est absent. |
| `400` | `fiat_amount_required` | POST /v1/payin : process_by vaut fiat_amount et fiat_amount est absent. |
| `400` | `fiat_currency_required` | POST /v1/payin : process_by vaut fiat_amount et fiat_currency est absent. |
| `400` | `invalid_page_number` | list-payout / list-payin : page inférieur à 1. |
| `400` | `invalid_per_page_limit` | list-payout / list-payin : per_page hors de 1 à 100. |
| `400` | `invalid_status_filter` | list-payout / list-payin : valeur de status inconnue. |
| `400` | `invalid_network_filter` | list-payout / list-payin : réseau inconnu. |
| `400` | `invalid_date_from` | list-payout / list-payin : date_from n'est pas une date. |
| `400` | `invalid_date_to` | list-payout / list-payin : date_to n'est pas une date. |
| `400` | `invalid_reference_id` | check-payout / check-payin : référence de moins de 10 ou plus de 756 caractères. Relevés : reference_id de plus de 255. |
| `400` | `invalid_pagination` | Relevés : page ou per_page hors limites. |
| `400` | `invalid_format` | Relevés : date_from ou date_to n'est pas une date (details.field). |
| `400` | `invalid_date_range` | Relevés : date_from est postérieur à date_to. |
| `400` | `date_range_too_wide` | Relevés : la plage dépasse le maximum autorisé. details.max_days indique le plafond. |
| `400` | `invalid_amount` | Relevés : amount_from ou amount_to invalide, ou amount_from supérieur à amount_to. |
| `400` | `missing_parameters` | get-price : currency1 ou currency2 est absent. |
| `400` | `invalid_currency` | get-price : currency1 ou currency2 n'est pas un code de devise valide. |
| `400` | `invalid_webhook_url` | PUT /v1/fixed-wallets/config : webhook_url pointe vers une destination non autorisée. |

### Erreurs Métier

| Code HTTP | Erreur | Description |
| --- | --- | --- |
| `400` | `insufficient_total_balance` | POST /v1/payout : les éléments d'une devise dépassent le solde disponible. Rien n'est débité. details contient crypto_currency, available et required. |
| `404` | `user_not_found` | POST /v1/payout : la configuration du compte est incomplète. Contactez le support. |
| `404` | `record_not_found` | Le payout ou payin recherché par check-payout / check-payin n'existe pas sur ce compte. Aussi renvoyé quand une donnée du compte nécessaire à la requête manque. |
| `400` | `duplicate_reference_id` | POST /v1/payin : un payin avec ce reference_id existe déjà sur le compte. |
| `400` | `invalid_network_or_deposits_disabled` | POST /v1/payin : réseau inconnu, ou dépôts désactivés sur celui-ci. |
| `400` | `unsupported_token_or_deposits_disabled` | POST /v1/payin : token non pris en charge, ou dépôts désactivés pour celui-ci. |
| `400` | `token_network_mismatch` | POST /v1/payin : le token n'est pas disponible sur ce réseau. |
| `400` | `memo_required` | POST /v1/payin : ce token exige un memo. |
| `400` | `amount_out_of_limits` | POST /v1/payin : le montant crypto est hors des limites du token. details contient min_allowed et max_allowed. |
| `400` | `value_below_minimum` | POST /v1/payin avec fiat_amount : le montant converti est inférieur au minimum. |
| `400` | `value_above_maximum` | POST /v1/payin avec fiat_amount : le montant converti est supérieur au maximum. |
| `400` | `token_not_supported_or_inactive` | POST /v1/payin avec fiat_amount : le token n'est pas pris en charge ou est inactif pour la conversion. |
| `400` | `crypto_amount_does_not_correspond_to_fiat_amount_provided` | POST /v1/payin : crypto_amount et fiat_amount ont été envoyés ensemble et ne correspondent pas au cours actuel. |
| `400` | `invalid_parameters_for_process_by` | POST /v1/payin : les montants envoyés ne correspondent pas au process_by choisi. |
| `404` | `price_not_found` | get-price : aucun cours pour cette paire de devises. |
| `403` | `feature_disabled` | Les adresses fixes ne sont pas activées pour le compte (config, mise à jour de la config et création). |
| `422` | `type_not_supported` | POST /v1/fixed-wallets : ce type n'est pas disponible pour les adresses fixes. |
| `403` | `limit_not_allowed` | POST /v1/fixed-wallets : la limite du compte pour ce type est zéro. |
| `409` | `limit_reached` | POST /v1/fixed-wallets : le compte détient déjà le nombre maximal d'adresses fixes. |
| `409` | `no_wallet_available` | POST /v1/fixed-wallets : aucune adresse de ce type n'est disponible pour le moment. Réessayez plus tard. |

### Erreurs par Élément du Lot de Payout (results[].error)

| Code HTTP | Erreur | Description |
| --- | --- | --- |
| `207/400` | `invalid_parameters` | L'élément a échoué à la validation. error.details.fields associe chaque champ à son message. |
| `207/400` | `missing_required_fields` | Un champ obligatoire de l'élément est absent. |
| `207/400` | `duplicate_reference_id_in_request` | Le même reference_id apparaît plus haut dans le même lot. |
| `207/400` | `duplicate_payment_reference` | Le reference_id a déjà été utilisé sur ce compte. L'élément contient duplicate, original_status, original_created_at et l'internal_reference d'origine ; aucun second transfert n'est effectué. |
| `207/400` | `invalid_crypto_amount` | process_by vaut crypto_amount et crypto_amount est absent ou invalide. |
| `207/400` | `invalid_fiat_amount` | process_by vaut fiat_amount et fiat_amount est absent ou invalide. |
| `207/400` | `fiat_currency_required` | process_by vaut fiat_amount et fiat_currency est absent. |
| `207/400` | `fiat_currency_not_accepted` | La fiat_currency n'est pas acceptée. |
| `207/400` | `memo_required` | Ce token exige un memo. |
| `207/400` | `invalid_network_or_withdrawals_disabled` | Réseau inconnu, ou retraits désactivés sur celui-ci. |
| `207/400` | `unsupported_token_or_withdrawals_disabled` | Token non pris en charge, ou retraits désactivés pour celui-ci. |
| `207/400` | `token_network_mismatch` | Le token n'est pas disponible sur ce réseau. |
| `207/400` | `token_not_active` | Le token n'est pas actif pour les retraits ou pour la conversion. |
| `207/400` | `invalid_format` | L'adresse du wallet n'est pas valide pour le réseau. |
| `207/400` | `limit_exceeded` | Le montant est hors du minimum et du maximum du token (aussi quand le montant fiat converti en sort). |
| `207/400` | `amount_does_not_cover_fees` | Les frais absorbent tout le montant, le destinataire ne recevrait rien. |
| `207/400` | `crypto_amount_does_not_correspond_to_fiat_amount_provided` | crypto_amount et fiat_amount ne correspondent pas au cours actuel. |
| `207/400` | `price_unavailable` | Pas de cours pour convertir le montant, ou la clé a un plafond journalier et le token n'a pas de cours en USD pour compter le retrait. |
| `207/400` | `insufficient_balance` | Le solde disponible ne couvre pas cet élément. |
| `207/400` | `daily_payout_limit_exceeded` | L'élément dépasserait le plafond journalier configuré sur la clé. |
| `207/400` | `daily_payout_limit_unavailable` | La clé a un plafond journalier mais la dépense n'a pas pu être enregistrée. Renvoyez l'élément. |
| `207/400` | `error_fetching_active_tokens · error_fetching_active_currencies` | Le catalogue de tokens ou de devises n'a pas pu être chargé. Renvoyez l'élément. |
| `207/400` | `user_not_found · record_not_found` | Une donnée du compte nécessaire au retrait manque. |
| `207/400` | `internal_error · database_error · transaction_failed · network_unavailable · duplicate_record` | L'enregistrement du retrait a échoué de notre côté et a été annulé. Rien n'a été débité pour cet élément ; vérifiez avec check-payout ou list-payout avant de renvoyer. |

### Erreurs Système

| Code HTTP | Erreur | Description |
| --- | --- | --- |
| `500` | `internal_error` | Échec inattendu. Sur POST /v1/payout, un échec en cours de lot renvoie aussi summary (avec not_processed) et results au premier niveau, pour ne renvoyer que ce qui manque. |
| `500` | `database_error` | Relevés : la requête a échoué. |
| `500` | `error_fetching_active_currencies` | POST /v1/payout et POST /v1/payin : le catalogue de tokens ou de fiats n'a pas pu être chargé. Rien n'a été traité. |
| `500` | `token_type_not_found · receipt_creation_failed · database_transaction_failed` | POST /v1/payin : le payin n'a pas pu être créé de notre côté. |
| `502` | `external_processor_error` | POST /v1/payin et POST /v1/fixed-wallets : l'adresse de dépôt n'a pas pu être créée pour le moment. Rien n'a été créé ; réessayez plus tard. |
| `503` | `no_processor_configured · no_wallet_available` | POST /v1/payin : aucun processeur ni adresse de dépôt disponible pour ce token et ce réseau pour le moment. Réessayez plus tard. |
| `503` | `price_not_available_for_conversion · price_not_available_for_validation` | POST /v1/payin : aucun cours disponible pour convertir ou vérifier le montant. Réessayez plus tard. |
| `503` | `service_unavailable` | POST /v1/payin et endpoints summary : une dépendance est temporairement indisponible. |
| `500` | `config_update_failed` | PUT /v1/fixed-wallets/config : la configuration n'a pas pu être enregistrée. |
| `500` | `assign_failed` | POST /v1/fixed-wallets : l'adresse n'a pas pu être attribuée. |

### Meilleures Pratiques

#### Sécurité

**À Faire**

- Stockez votre clé API dans des variables d'environnement
- Configurez la whitelist IP
- Vérifiez le X-Bifrost-Signature de chaque webhook
- Utilisez HTTPS dans toutes les requêtes
- Implémentez des timeouts pour les requêtes

**À Ne Pas Faire**

- Partager la clé API publiquement
- Commiter les clés API dans Git
- Utiliser la même clé API dans plusieurs environnements

#### Performance

- Les données de relevé et de résumé peuvent avoir jusqu'à 5 minutes de retard ; ne les interrogez pas plus souvent
- Implémentez une pagination appropriée
- Évitez les requêtes excessives (respectez la limite de taux)
- Traitez les webhooks de manière asynchrone

#### Idempotence

- Utilisez un reference_id unique et significatif : c'est le verrou contre le double paiement, et le représenter renvoie le paiement d'origine au lieu d'en créer un autre
- Envoyez une Idempotency-Key sur les lots : si la connexion tombe, réessayer avec la même clé renvoie la réponse d'origine au lieu de retraiter
- POST /v1/payin n'a pas d'Idempotency-Key : après un timeout, renvoyez avec le même reference_id et, si la réponse est duplicate_reference_id, récupérez le payin avec GET /v1/list-payin?reference_id=…
- Traitez les 207 et 500 en lisant results : les deux contiennent ce qui a déjà été créé, pour ne renvoyer que ce qui manque
- Gérez les renvois de webhooks en dédupliquant sur data.internal_reference avec event, pour ne pas traiter deux fois le même avis

---

> La référence de l'API est en anglais. Les guides, l'authentification, les webhooks et les codes d'erreur sont en français.

## Référence de l'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`

Portée requise : `payout:write` (une clé nouvelle sans liste est en lecture seule)

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

#### Autres réponses

Réponses qui ne sont pas le chemin nominal et qui doivent malgré tout être traitées dans votre intégration.

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

Portée requise : `payout:write` (une clé nouvelle sans liste est en lecture seule)

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

#### Autres réponses

Réponses qui ne sont pas le chemin nominal et qui doivent malgré tout être traitées dans votre intégration.

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

Portée requise : `payout:read` (une clé nouvelle sans liste est en lecture seule)

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

#### Autres réponses

Réponses qui ne sont pas le chemin nominal et qui doivent malgré tout être traitées dans votre intégration.

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

Portée requise : `payout:read` (une clé nouvelle sans liste est en lecture seule)

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

#### Autres réponses

Réponses qui ne sont pas le chemin nominal et qui doivent malgré tout être traitées dans votre intégration.

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

## Référence de l'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`

Portée requise : `payin:write` (une clé nouvelle sans liste est en lecture seule)

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

#### Autres réponses

Réponses qui ne sont pas le chemin nominal et qui doivent malgré tout être traitées dans votre intégration.

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

Portée requise : `payin:read` (une clé nouvelle sans liste est en lecture seule)

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

#### Autres réponses

Réponses qui ne sont pas le chemin nominal et qui doivent malgré tout être traitées dans votre intégration.

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

Portée requise : `payin:read` (une clé nouvelle sans liste est en lecture seule)

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

#### Autres réponses

Réponses qui ne sont pas le chemin nominal et qui doivent malgré tout être traitées dans votre intégration.

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

## Référence de l'API · Queries

Endpoints for querying balances, statements, and account information.

### Get Balances

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

Portée requise : `balances:read` (une clé nouvelle sans liste est en lecture seule)

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`

Portée requise : `statements:read` (une clé nouvelle sans liste est en lecture seule)

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

#### Autres réponses

Réponses qui ne sont pas le chemin nominal et qui doivent malgré tout être traitées dans votre intégration.

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

Portée requise : `statements:read` (une clé nouvelle sans liste est en lecture seule)

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

#### Autres réponses

Réponses qui ne sont pas le chemin nominal et qui doivent malgré tout être traitées dans votre intégration.

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

Portée requise : `statements:read` (une clé nouvelle sans liste est en lecture seule)

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

#### Autres réponses

Réponses qui ne sont pas le chemin nominal et qui doivent malgré tout être traitées dans votre intégration.

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

Portée requise : `prices:read` ou `statements:read` (l'une d'elles suffit)

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`

Portée requise : `prices:read` ou `statements:read` (l'une d'elles suffit)

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

## Référence de l'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`

Portée requise : `prices:read` (une clé nouvelle sans liste est en lecture seule)

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

#### Autres réponses

Réponses qui ne sont pas le chemin nominal et qui doivent malgré tout être traitées dans votre intégration.

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

Portée requise : `prices:read` (une clé nouvelle sans liste est en lecture seule)

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

## Référence de l'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`

Portée requise : `wallets:read` (une clé nouvelle sans liste est en lecture seule)

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`

Portée requise : `wallets:read` (une clé nouvelle sans liste est en lecture seule)

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

#### Autres réponses

Réponses qui ne sont pas le chemin nominal et qui doivent malgré tout être traitées dans votre intégration.

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

Portée requise : `wallets:write` (une clé nouvelle sans liste est en lecture seule)

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

#### Autres réponses

Réponses qui ne sont pas le chemin nominal et qui doivent malgré tout être traitées dans votre intégration.

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

Portée requise : `wallets:read` (une clé nouvelle sans liste est en lecture seule)

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.

#### Autres réponses

Réponses qui ne sont pas le chemin nominal et qui doivent malgré tout être traitées dans votre intégration.

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

Portée requise : `wallets:write` (une clé nouvelle sans liste est en lecture seule)

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

#### Autres réponses

Réponses qui ne sont pas le chemin nominal et qui doivent malgré tout être traitées dans votre intégration.

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

---

## Recettes d'intégration (pas à pas pour les agents)

### Recevoir un paiement (payin)

1. Lisez la clé d'API, et le secret de signature quand la clé l'exige, dans des variables d'environnement, jamais dans le code.
2. Appelez `GET /v1/summary/tokens` pour choisir le token et le réseau (`min_receipt`, `max_receipt`). La liste n'est pas filtrée par votre compte : la création confirme si le token accepte les dépôts pour vous.
3. Appelez `POST /v1/payin` avec l'id de la commande comme `reference_id`, `crypto_currency`, `network`, `process_by` et le montant correspondant en chaîne décimale, `webhook_url` et `customer_info`. Conservez `data.internal_reference` et montrez au client `data.wallet`, ainsi que le memo quand le token l'exige.
4. Dans l'endpoint de webhook, lisez le corps brut, vérifiez `X-Bifrost-Signature`, répondez 2xx tout de suite et traitez ensuite. Ignorez les avis déjà traités selon `data.internal_reference` avec `event`.
5. Sur `payment_confirmed`, comparez `data.received_amount` à `data.crypto_amount` et livrez. Livrer sur `payment_detected` est votre décision et expose à `payment_reversed`. Quand `is_duplicated` vaut true, rattachez le paiement à la commande par `customer_info`.
6. Après un timeout à la création, renvoyez avec le même `reference_id` ; sur `duplicate_reference_id`, récupérez le payin avec `GET /v1/list-payin?reference_id=…`. Décidez chaque erreur selon `error.code`.
7. Rapprochez avec `GET /v1/check-payin/{internal_reference}`.

### Envoyer des payouts

1. Lisez la clé d'API, et le secret de signature quand la clé l'exige, dans des variables d'environnement, jamais dans le code.
2. Consultez `GET /v1/balances` (utilisez `available` quand il contient la clé du token) ainsi que `min_withdrawal` et `max_withdrawal` dans `GET /v1/summary/tokens`.
3. Appelez `POST /v1/payout` avec une liste de 50 payouts au plus, chacun avec son propre `reference_id` stable, des montants en chaîne décimale et `feetakenfromamount` à 0 ou 1, et envoyez un `Idempotency-Key` par requête.
4. Lisez `results[]` par `index` : conservez l'`internal_reference` des éléments créés ; pour les éléments refusés, décidez selon `results[].error.code` et ne renvoyez que ceux-là, avec un nouvel Idempotency-Key.
5. Après un timeout, renvoyez le même corps avec le même Idempotency-Key dans les 24 heures. Sur 409 `idempotency_key_in_progress`, attendez et réessayez avec la même clé. Sur 500, ne renvoyez que les éléments comptés dans `summary.not_processed`.
6. Traitez les webhooks `payment_processed` et `payment_failed`, en vérifiant la signature et en dédupliquant sur `data.internal_reference` avec `event`. Pour payer de nouveau après un échec, créez un nouveau payout avec un nouveau `reference_id`.
7. Rapprochez avec `GET /v1/check-payout/{internal_reference}`.

### Adresse fixe par client

1. Les adresses fixes doivent être activées pour le compte ; sinon, créer une adresse ou lire ou modifier la configuration répond 403 `feature_disabled`. `GET /v1/fixed-wallets/supported` liste les types et `GET /v1/fixed-wallets/config` les limites et l'URL de webhook.
2. Définissez l'URL de webhook des adresses fixes dans le panneau ou avec `PUT /v1/fixed-wallets/config`. Sans elle, aucun avis n'est envoyé.
3. Appelez `POST /v1/fixed-wallets` avec `type` et l'id de votre client dans `customer_info` : 201 pour une nouvelle adresse, 200 `fixed_wallets_already_assigned` quand ce client en a déjà une de ce type. Une adresse sert tous les réseaux de sa famille.
4. Chaque transfert vers l'adresse crée un payin déjà terminé et envoie `payment_detected` / `payment_confirmed` à l'URL des adresses fixes, avec `is_wallet_fixed` true et un `reference_id` généré. Rattachez-le au client par `customer_info` ou par `wallet`.
5. Traitez `fixed_address_pre_revoke` (120 jours sans transaction) et `fixed_address_revoked` (150 jours). Les adresses de type `evm` sont permanentes.
6. Utilisez `GET /v1/fixed-wallets` pour lister les adresses et lire `allowance` avant d'en créer d'autres.
