Transactions
Base URL : /api/v1/transactions
Consultation et application des mouvements financiers sur les cartes. Chaque opération mutante accepte l'en-tête Idempotency-Key et, pour les recharges Mobile Money, nécessite le contexte tenant.
Types de transaction
Type (type) | Description |
|---|---|
credit_cash | Recharge espèces (agent) |
credit_mobile_money | Recharge Mobile Money |
debit_purchase | Achat de produits |
debit_reservation | Débit lié à une réservation |
correction | Correction manuelle (crédit) |
debit_correction | Correction manuelle (débit) |
debit_lost_card | Débit pour carte perdue |
refund | Remboursement |
GET /transactions
Liste paginée des transactions de l'organisation.
Permissions : transaction.read ou card.manage
Paramètres de query
| Param | Type | Défaut | Description |
|---|---|---|---|
search | string | — | Recherche (code carte, porteur…) |
type | string | — | Filtrer par type (ex: credit_cash, debit_purchase) |
date | date | — | Jour précis (YYYY-MM-DD) |
date_from | date | — | Début de plage |
date_to | date | — | Fin de plage |
limit | integer | 20 | Éléments par page |
offset | integer | 0 | Décalage |
cursor | string | — | Curseur {timestamp},{id} |
Réponse (200)
Pagination par curseur : next_cursor vaut null sur la dernière page.
{
"items": [
{
"id": "uuid",
"card_id": "uuid",
"card_code": "CL-EL-0008",
"card_number": "6287940000123456",
"cardholder_name": "Fatou Diallo",
"is_voided": false,
"type": "debit_purchase",
"amount_cents": 1500,
"balance_before_cents": 5000,
"balance_after_cents": 3500,
"performed_by_user_id": "uuid",
"performed_by_name": "Jean Mbeki",
"site_id": "uuid",
"point_of_sale_id": "uuid",
"created_at": "2025-06-01T10:00:00Z",
"amount": 1500,
"amount_formatted": "1 500 FCFA",
"balance_before": 5000,
"balance_before_formatted": "5 000 FCFA",
"balance_after": 3500,
"balance_after_formatted": "3 500 FCFA",
"currency": "XOF",
"purchase_items": [
{ "product_id": "uuid", "name": "Café", "quantity": 1, "unit_price_cents": 1500 }
]
}
],
"total": 142,
"next_cursor": "2025-06-01T10:00:00+00:00,550e8400-e29b-41d4-a716-446655440000",
"currency": "XOF"
}
purchase_itemsprésent uniquement sitype == "debit_purchase".reservation_itemsprésent uniquement sitype == "debit_reservation".is_voidedest calculé en détectant les lignes de reversal associées.
POST /transactions/recharge
Recharge une carte. Deux méthodes : cash (synchrone) et mobile_money (asynchrone via un fournisseur de paiement).
Permissions : card.recharge.cash ou card.recharge.mobile_money ou card.manage.
Pour mobile_money, la permission card.recharge.mobile_money est vérifiée explicitement.
En-tête : Idempotency-Key (recommandé)
Corps de la requête
| Champ | Type | Requis | Description |
|---|---|---|---|
card_id | uuid | Oui | Carte à recharger |
amount_cents | integer | Oui | Montant (1 à 1 000 000 FCFA) |
method | string | Oui | "cash" ou "mobile_money" |
point_of_sale_id | uuid | Non | Point de vente |
site_id | uuid | Non | Site |
register_id | uuid | Non | Session de caisse |
metadata | object | Non | Métadonnées libres |
customer_phone | string | Cond. | Requis si method=mobile_money |
payment_provider | string | Non | "fedapay" (défaut) ou "paygate" |
POST /api/v1/transactions/recharge
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json
{
"card_id": "uuid",
"amount_cents": 2000,
"method": "cash",
"point_of_sale_id": "uuid"
}
Réponse cash (200)
{
"transaction": {
"id": "uuid",
"card_id": "uuid",
"type": "credit_cash",
"amount_cents": 2000,
"balance_before_cents": 5000,
"balance_after_cents": 7000,
"performed_by_user_id": "uuid",
"performed_by_name": "Jean Mbeki",
"created_at": "2025-06-01T10:00:00Z",
"amount": 2000,
"amount_formatted": "2 000 FCFA",
"balance_before": 5000,
"balance_before_formatted": "5 000 FCFA",
"balance_after": 7000,
"balance_after_formatted": "7 000 FCFA",
"currency": "XOF"
},
"new_balance_cents": 7000,
"new_balance": 7000,
"new_balance_formatted": "7 000 FCFA",
"currency": "XOF"
}
Réponse mobile_money (200)
Crée une session de paiement asynchrone. Le crédit sera appliqué via le callback du fournisseur.
{
"payment_url": "https://pay.fedapay.com/...",
"session_id": "uuid",
"provider_tx_id": null
}
Le fournisseur de paiement doit être configuré (state.payment_client), sinon l'erreur PaymentNotConfigured est renvoyée.
POST /transactions/purchase
Effectue un achat multi-articles. Le solde est vérifié au moment du débit.
Permissions : product.sell ou product.manage
En-tête : Idempotency-Key
Corps de la requête
| Champ | Type | Requis | Description |
|---|---|---|---|
card_id | uuid | Oui | Carte débitée |
items | array | Oui | Articles achetés |
items[].product_id | uuid | Oui | Produit |
items[].quantity | integer | Oui | Quantité |
items[].unit_price_cents | integer | Oui | Prix unitaire |
point_of_sale_id | uuid | Non | Point de vente |
site_id | uuid | Non | Site |
register_id | uuid | Non | Session de caisse |
POST /api/v1/transactions/purchase
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440001
Content-Type: application/json
{
"card_id": "uuid",
"items": [
{ "product_id": "uuid", "quantity": 2, "unit_price_cents": 1000 }
]
}
Réponse (200)
{
"transaction": {
"id": "uuid",
"card_id": "uuid",
"type": "debit_purchase",
"amount_cents": 2000,
"balance_before_cents": 7000,
"balance_after_cents": 5000,
"created_at": "2025-06-01T10:05:00Z",
"amount": 2000,
"amount_formatted": "2 000 FCFA",
"balance_before": 7000,
"balance_before_formatted": "7 000 FCFA",
"balance_after": 5000,
"balance_after_formatted": "5 000 FCFA",
"currency": "XOF"
},
"new_balance_cents": 5000,
"new_balance": 5000,
"new_balance_formatted": "5 000 FCFA",
"currency": "XOF"
}
POST /transactions/correction
Applique une correction manuelle (crédit ou débit).
Permissions : card.correction ou card.manage.
Si la correction rend le solde négatif, card.balance.manual_adjust est requis et le porteur doit être de niveau préscolaire (metadata.education_level ∈ preschool/maternelle).
En-tête : Idempotency-Key
Corps de la requête
| Champ | Type | Requis | Description |
|---|---|---|---|
card_id | uuid | Oui | Carte concernée |
amount_cents | integer | Oui | Montant absolu (> 0) |
direction | string | Non | "credit" ou "debit" |
reason | string | Oui | Motif de la correction |
original_transaction_id | uuid | Non | Transaction d'origine |
site_id | uuid | Non | Site |
point_of_sale_id | uuid | Non | Point de vente |
register_id | uuid | Non | Session de caisse |
POST /api/v1/transactions/correction
Content-Type: application/json
{
"card_id": "uuid",
"amount_cents": 500,
"direction": "credit",
"reason": "Erreur de caisse"
}
direction obsolèteSi direction est omis, le signe de amount_cents est utilisé : négatif = débit, positif = crédit. Ce mode est déprécié — utilisez direction.
Réponse (200)
Même enveloppe que purchase, avec "type": "correction".
POST /transactions/refund
Rembourse tout ou partie d'une transaction.
Permissions : reservation.refund, reservation.manage, card.correction ou card.manage
En-tête : Idempotency-Key
Corps de la requête
| Champ | Type | Requis | Description |
|---|---|---|---|
card_id | uuid | Oui | Carte recréditée |
amount_cents | integer | Oui | Montant (> 0) |
reason | string | Oui | Motif |
reservation_id | uuid | Non | Réservation concernée |
original_transaction_id | uuid | Non | Transaction d'origine |
site_id | uuid | Non | Site |
point_of_sale_id | uuid | Non | Point de vente |
register_id | uuid | Non | Session de caisse |
POST /api/v1/transactions/refund
Content-Type: application/json
{
"card_id": "uuid",
"amount_cents": 1000,
"reason": "Réservation annulée",
"reservation_id": "uuid"
}
Réponse (200)
Même enveloppe que purchase, avec "type": "refund".
POST /transactions/{id}/void
Annule une transaction : inverse son effet financier et restaure le stock le cas échéant.
Permissions : card.void ou card.correction
En-tête : Idempotency-Key (défaut void:{id})
Corps de la requête
| Champ | Type | Requis | Description |
|---|---|---|---|
reason | string | Oui | Motif de l'annulation |
site_id | uuid | Non | Site |
point_of_sale_id | uuid | Non | Point de vente |
register_id | uuid | Non | Session de caisse |
Réponse (200)
Contrairement aux autres réponses de transaction, void n'applique pas de monetize : type est le nom de la variante Rust ("Correction", "DebitCorrection"), les montants sont des entiers bruts, et performed_by est un objet { id, kind }.
{
"message": "Transaction annulée",
"transaction": {
"id": "uuid",
"card_id": "uuid",
"type": "Correction",
"amount_cents": 2000,
"balance_before_cents": 5000,
"balance_after_cents": 7000,
"performed_by": { "id": "uuid", "kind": "TenantUser" },
"point_of_sale_id": "uuid",
"site_id": "uuid",
"register_id": null,
"reference_order_id": "uuid",
"idempotency_key": "void:uuid",
"metadata": {},
"created_at": "2025-06-01T10:10:00Z"
}
}
Éligibilité au void
La type de reversal dépend du type d'origine :
| Type d'origine | Type de reversal |
|---|---|
debit_purchase, debit_reservation | Correction |
credit_cash, credit_mobile_money, correction | DebitCorrection |
debit_correction | Correction |
Une transaction déjà annulée est rejetée (validation).
POST /transactions/{id}/modify
Modifie une vente : annule l'ancienne et crée une nouvelle de façon atomique.
Permissions : card.correction
En-tête : Idempotency-Key
Corps de la requête
| Champ | Type | Requis | Description |
|---|---|---|---|
card_id | uuid | Oui | Carte |
items | array | Oui | Nouveaux articles |
reason | string | Oui | Motif de la modification |
point_of_sale_id | uuid | Non | Point de vente |
site_id | uuid | Non | Site |
register_id | uuid | Non | Session de caisse |
Réponse (200)
{
"message": "Vente modifiée",
"transaction": {
"id": "uuid",
"card_id": "uuid",
"type": "debit_purchase",
"amount_cents": 1500,
"balance_before_cents": 5000,
"balance_after_cents": 3500,
"created_at": "2025-06-01T10:15:00Z",
"amount": 1500,
"amount_formatted": "1 500 FCFA",
"balance_before": 5000,
"balance_before_formatted": "5 000 FCFA",
"balance_after": 3500,
"balance_after_formatted": "3 500 FCFA",
"currency": "XOF"
},
"new_balance_cents": 3500,
"new_balance": 3500,
"new_balance_formatted": "3 500 FCFA",
"currency": "XOF"
}
modify est un « void + re-create » : les anciens articles sont remis en stock, l'ancien total recrédité, une nouvelle vente créée, et la transaction d'origine marquée comme remplacée.
Erreurs courantes
| Code | Description |
|---|---|
400 | Montant invalide (≤ 0 ou > plafond) |
403 | Permission ou plafond insuffisant |
404 | Carte ou transaction introuvable |
409 | Transaction déjà annulée |
422 | customer_phone manquant pour Mobile Money |