Aicscan Pay
Aicscan Pay encaisse des paiements en USDT sans jamais détenir les fonds : le payeur envoie le montant à une adresse de dépôt propre à la facture, et les fonds partent ensuite directement vers votre adresse de versement. Aucun point d’accès de l’API ne déplace de fonds.
L’API Pay suit les mêmes règles que le reste de l’API v1 (authentification, pagination, erreurs, versionnement), avec les différences ci-dessous.
Activer Pay et obtenir des clés#
- Dans l’espace client, onglet Pay, le propriétaire active Pay. La double authentification est obligatoire pour toute action Pay.
- Le mode test est disponible tout de suite : créez une clé
test. - Pour le mode live, lancez la vérification de votre entreprise (KYB). Les clés
livene sont émises qu’une fois le KYB approuvé. - Déclarez votre wallet signataire et votre adresse de versement : l’onglet renvoie, pour chaque chaîne ouverte, le message EIP-712
RegisterMerchantà signer avec ce wallet. Rien n’est envoyé on-chain à ce stade.
| Clé | Préfixe | Environnement |
|---|---|---|
| Test | aic_test_… |
testnets, sans KYB |
| Live | aic_live_… |
réseaux principaux, après KYB |
Portées : pay:read (lectures), pay:write (factures, webhooks), pay:refund (préparation de remboursements). Toute portée Pay comprend pay:read. Une clé Alerts reçoit 403 insufficient_scope sur les routes Pay, et une clé Pay sur les routes Alerts. Une clé Pay ne dépend pas de l’offre Alerts de l’espace (pas de plan_required, pas de quota quotidien) ; la limite de débit par seconde s’applique.
Chaque objet appartient à l’environnement de la clé qui l’a créé : une facture test est introuvable (404) avec une clé live.
Idempotency-Key est obligatoire sur tous les POST Pay (400 idempotency_key_required). Utilisez un UUID par opération et renvoyez la même valeur si vous réessayez : la facture n’est jamais créée deux fois. Voir Idempotence.
Créer une facture#
curl https://aicscan.io/v1/invoices \
-H "Authorization: Bearer $AICSCAN_PAY_KEY" \
-H "Idempotency-Key: 6f1c2d0e-8b7a-4c55-9e2f-1a3b5c7d9e0f" \
-H "Content-Type: application/json" \
-d '{"currency":"EUR","price_e6":"25000000","chain_ids":[56,728126428],"external_ref":"cmd-1042","ttl_seconds":3600}'| Champ | Sens |
|---|---|
currency |
USDT, ou une devise d’affichage ouverte (USD, EUR, XOF… voir GET /v1/rates). |
price_e6 |
Prix en millionièmes de la devise, en chaîne : "25000000" = 25,00. Pas plus de décimales que la devise (XOF : aucune). |
chain_ids |
Chaînes proposées au payeur : une adresse de dépôt par chaîne. |
variant |
A (défaut) : une adresse par facture. B : adresse partagée et montant légèrement unique, une seule chaîne. |
ttl_seconds |
Validité, 3 600 s par défaut, de 900 s à 7 jours. Prévoyez long si vos clients paient depuis un exchange. |
external_ref, description, metadata |
Votre référence (unique par environnement), un libellé, un objet JSON libre. |
Le cours d’une devise est figé à la création (price.fx_rate_e6, fx_source, fx_as_of) et le montant dû est arrondi au profit du payeur. Les montants de jeton sont toujours des chaînes en unités natives, avec leur forme lisible :
{ "amount": "27012345", "amount_formatted": "27.012345", "decimals": 6 }La réponse (201) donne pour chaque chaîne l’adresse de dépôt (deposit_address, base58 T… sur TRON), le montant dû et, sur les chaînes EVM, un lien EIP-681 (payment_uri) à mettre dans un QR code.
Refus fréquents : chain_not_available, amount_below_minimum, amount_above_cap, duplicate_external_ref (409), unique_amount_exhausted (409, variante B : toutes les valeurs uniques sont prises), currency_suspended (409 : la garde USDT/USD a suspendu les factures en devise ; les factures en USDT ne sont pas concernées), kyb_required (403, live).
Statuts d’une facture#
| Statut | Sens |
|---|---|
waiting |
Créée, rien reçu. |
confirming |
Paiement vu dans un bloc, confirmations en cours. |
paid |
Montant reçu dans la tolérance, confirmé. |
underpaid |
Reçu moins que le dû : un complément reste possible jusqu’à l’expiration. |
overpaid |
Reçu plus que le dû : remboursez l’excédent si besoin. |
expired |
Délai écoulé sans paiement complet. |
paid_late |
Payée après l’expiration (ou après annulation) : à vous d’honorer ou de rembourser. |
canceled |
Annulée par vous avant tout paiement (POST /v1/invoices/{id}/cancel). |
held |
Paiement depuis une adresse sanctionnée : décision manuelle. |
Plusieurs transferts vers la même adresse s’additionnent. Les montants minuscules (poussière) sont ignorés. Le détail d’une facture (GET /v1/invoices/{id}) liste ses paiements, ses balayages vers votre adresse de versement et ses remboursements.
Webhooks Pay#
Déclarez un point de réception avec POST /v1/webhook_endpoints (https:// uniquement ; adresses privées et locales refusées). Le secret whsec_… n’est renvoyé qu’une fois. Les envois suivent exactement le mécanisme des webhooks d’Alerts : POST JSON, en-têtes X-Event-Id, X-Timestamp et X-Signature: sha256=HMAC(secret, "{X-Timestamp}.{corps brut}"), rejet au-delà de 300 secondes, seule une réponse 2xx vaut succès, relances pendant 72 heures.
{
"id": "evt_7850b21f0ab248a5e16ddedf2c6626e9",
"type": "invoice.paid",
"api_version": "1",
"environment": "live",
"livemode": true,
"created_at": "2026-11-20T10:05:00Z",
"data": {
"object": { "id": "0193a1d0-1a2b-7c3d-8e4f-5a6b7c8d9e0f", "object": "invoice", "status": "paid" },
"payment": { "amount": "27012345", "amount_formatted": "27.012345", "decimals": 6, "chain_id": 56,
"tx_hash": "0x…", "log_index": 3, "block_number": 45123456, "confirmations": 15 }
}
}Types : invoice.created, invoice.confirming, invoice.paid, invoice.underpaid, invoice.overpaid, invoice.expired, invoice.paid_late, invoice.canceled, invoice.reverted, invoice.held, sweep.succeeded, sweep.failed, refund.succeeded, refund.pending, merchant.kyb_updated… L’id (X-Event-Id) est identique à chaque relance : servez-vous-en pour ignorer un doublon. Après une réorganisation de chaîne, invoice.reverted annule un paiement ; un nouvel invoice.paid suit s’il est réinclus.
POST /v1/webhook_endpoints/{id}/testenvoie un événementwebhook_endpoint.testà ce seul point.GET /v1/eventsetGET /v1/events/{id}donnent le journal et l’état des livraisons ;POST /v1/events/{id}/resendremet un événement en file.
Rembourser#
Un remboursement est un transfert depuis votre propre wallet. POST /v1/refunds (portée pay:refund) renvoie la transaction à signer (transaction.to = contrat du jeton, transaction.data = transfer(to, amount)) ; Aicscan ne l’envoie jamais.
L’adresse d’origine d’un paiement n’est jamais proposée par défaut : c’est souvent le portefeuille d’un exchange, et un remboursement y serait perdu. Saisissez dans to_address l’adresse que le payeur vous a donnée. L’avertissement exchange_address signale une adresse connue d’exchange, origin_address une adresse qui a envoyé un paiement de la facture.
La commission n’est pas remboursée. Le montant est plafonné au reçu moins les remboursements déjà préparés.
Mode test#
Les clés test fonctionnent sur les testnets, avec la même API et les mêmes webhooks ("environment": "test", "livemode": false). Rien de ce que vous créez en test n’apparaît en live, et inversement. Vérifiez qu’une adresse est bien l’une de vos adresses de dépôt avec GET /v1/deposit_addresses/{address}/verify.
Référence complète : Pay : factures, remboursements, webhooks.