Infractions PIX (flux complet)
Vision end-to-end du cycle de vie d'une infraction PIX chez Owem Pay — comment elle naît, comment elle est traitée automatiquement, quand elle bloque le solde, comment vous vous défendez et comment la réconcilier dans votre système.
Résumé en une phrase
Une infraction PIX est une contestation signalée par la contrepartie contre un PIX que vous avez reçu. Elle peut générer un blocage conservatoire de la valeur contestée jusqu'à la résolution. Abonnez-vous aux webhooks pix.infraction.* pour agir dans les délais.
1. Qu'est-ce qu'une infraction PIX ?
Une infraction (de type REFUND_REQUEST dans le vocabulaire BACEN) est un signalement formel enregistré dans le DICT par l'institution du payeur alléguant que le PIX reçu par vous présente un indice de fraude, escroquerie, erreur opérationnelle ou demande de remboursement.
Le canal officiel est le BACEN DICT — aucun client ne peut ouvrir une infraction directement contre vous ; elle passe toujours par l'institution payeuse et le BACEN avant d'arriver chez Owem.
| Champ technique | Description |
|---|---|
infraction_type | Type de l'infraction dans le BACEN. Valeurs : REFUND_REQUEST (demande de remboursement) et REFUND_CANCELLED (annulation d'une demande précédente) |
status | État dans le BACEN. Valeurs en UPPERCASE : ACKNOWLEDGED (reconnue, en analyse), CLOSED (finalisée), CANCELLED (annulée par la contrepartie avant analyse) |
amount | Valeur contestée en subcentavos (pas nécessairement la valeur totale du PIX original) |
e2e_id | End-to-End ID du PIX original qui a été contesté |
defense_deadline | Délai BACEN pour soumission de la défense (ISO 8601 UTC) |
counterpart_ispb | ISPB de l'institution du payeur qui a ouvert l'infraction |
analysis_result | Décision finale à la clôture (CLOSED) : AGREED (remboursement exécuté) ou DISAGREED (défense acceptée, sans remboursement) |
Sources : schemas/pix_compliance/pix_infraction.ex.
Ne confondez pas avec le MED
MED (Mécanisme Spécial de Restitution) est le mécanisme réglementaire qui exécute le remboursement quand une infraction avec fraude confirmée doit être indemnisée — c'est la conséquence d'une infraction acceptée, pas l'événement initial. Voir la section Relation infraction ↔ MED ci-dessous.
2. Comment Owem reçoit les infractions
Owem ne reçoit pas d'infractions via push. Le backend fait un polling actif sur le DICT OnZ/BACEN toutes les 15 minutes via le worker Fluxiq.Workers.ComplianceSyncWorker.
Flux de découverte :
BACEN DICT
▲
│ GET /v3/dict/infracao/list (paginé 1000/call)
│
ComplianceSyncWorker (*/15 * * * *)
│
├─ upsert_infraction/2 → persiste dans pix_infractions
│ ├─ Si nouvelle (is_new=true) : dispatch "pix.infraction.created"
│ └─ Si mise à jour (status/analysis_result a changé) : dispatch "pix.infraction.resolved"
│
└─ maybe_auto_deny_or_block/2 → route la décision (voir section 3)Sources : workers/compliance_sync_worker.ex, use_cases/pix_compliance/pix_compliance.ex.
Latence BACEN → Webhook
Une infraction ouverte chez la contrepartie arrive chez Owem dans un délai maximum de 15 min (prochain cycle du ComplianceSyncWorker). Le webhook pix.infraction.created est déclenché dès que la ligne est insérée/mise à jour dans pix_infractions.
3. Classification et actions automatiques
Dès qu'une infraction REFUND_REQUEST avec status ACKNOWLEDGED ou OPEN est détectée, le backend classe en trois chemins automatiques sans intervention manuelle :
3.1 E2E inexistant dans vos transactions → auto-deny
Si l'infraction référence un e2e_id qui n'est pas dans votre table transactions (aucun PIX IN avec cet E2E n'est arrivé à votre système), le backend suppose que c'est une erreur opérationnelle de la contrepartie et effectue un auto-deny immédiat.
- Action OnZ :
POST /v3/dict/infracao/{id}avecAnalysisResult=DISAGREED - Met à jour
pix_infractions→status=CLOSED,analysis_result=DISAGREED,resolved_at=now - Webhook
pix.infraction.resolvedest déclenché - Aucun blocage conservatoire n'est créé
- Aucun remboursement n'est exécuté
3.2 Valeur ≤ R$ 1 000 (seuil configurable) → auto-deny
Les infractions de faible valeur sont automatiquement refusées. Le seuil par défaut est R$ 1 000,00 (10_000_000 subcentavos, constante @default_auto_deny_threshold), mais il peut être configuré par compte ou par merchant dans la table med_configurations (champ min_threshold_amount).
- La même action d'auto-deny du point 3.1 est appliquée
- La justification standardisée envoyée au BACEN est : « Verificado pelo time de compliance e sem evidencias concretas nao temos como fazer devolucao »
- Webhook
pix.infraction.resolvedest déclenché - Aucun blocage conservatoire n'est créé
- La valeur du PIX reçu reste disponible dans le solde
Seuil personnalisé par compte
Les comptes qui travaillent avec des tickets élevés légitimes (ex. : marketplaces de haute valeur) peuvent demander une augmentation du seuil via l'admin Owem. Les comptes qui préfèrent une défense manuelle pour toute valeur peuvent demander seuil=0 (rien n'est auto-refusé).
3.3 Valeur > R$ 1 000 ET E2E existe → blocage conservatoire MED
C'est le chemin qui impacte votre solde. En détectant une infraction éligible, le backend :
- Met à jour
pix_infractions.statusversPROCESSING(évite la ré-entrée au prochain cycle du sync) - Crée une ligne dans
med_cautelar_blocksavec la valeur contestée - Crée un phantom hold dans TigerBeetle — transfert en attente contre la wallet du compte affecté, qui réduit le solde disponible (
availabledans Solde) de la valeur contestée - Déclenche le webhook
pix.refund.requested(voir payload) - Déclenche le webhook
pix.infraction.created(voir payload)
La valeur reste bloquée jusqu'à ce que :
- Le délai de défense BACEN expire (voir section 6)
- Vous soumettiez une défense via le portail (voir section 5)
- Le BACEN résolve l'infraction (section 7)
- La contrepartie annule (
status=CANCELLED) avant l'analyse
Sources : use_cases/pix_compliance/pix_compliance.ex:495-537, use_cases/pix_compliance/med/processor.ex.
4. Relation infraction ↔ MED
L'infraction et le MED (Mécanisme Spécial de Restitution) sont des couches distinctes du même cycle :
| Couche | Ce que c'est | Table | Webhook principal |
|---|---|---|---|
| Infraction | Signalement formel dans le BACEN DICT | pix_infractions | pix.infraction.created |
| Blocage conservatoire MED | Réservation de solde dans TigerBeetle pendant l'analyse | med_cautelar_blocks | pix.refund.requested |
| Remboursement MED | PACS.004 exécuté après décision AGREED | transactions (type=pix_return) | pix.refund.completed + pix.payout.returned |
[Infraction créée dans le BACEN]
│
▼
ComplianceSyncWorker détecte (polling 15 min)
│
├─ pix.infraction.created (toujours, quand is_new=true)
│
│ Si > R$ 1 000 ET E2E existe :
▼
MED Processor → cautelar block + TB phantom hold
│
├─ pix.refund.requested (blocage créé)
│
│ Attend la décision (merchant se défend OU délai expire OU auto-accept session 135)
▼
[Résolution]
│
├─ AGREED → PACS.004 dispatched
│ ├─ pix.refund.completed
│ └─ pix.payout.returned (préfixe D dans l'E2E)
│
└─ DISAGREED → blocage libéré
└─ BlockRelease.release_for_e2e — void du TB phantom + libération du solde
│
▼
pix.infraction.resolved (toujours à la clôture — AGREED ou DISAGREED)La relation est 1-à-1 par E2E, pas 1-à-1 par ID
Une même transaction PIX (même e2e_id) ne peut pas avoir deux infractions actives en même temps — le ComplianceSyncWorker fait un guard dans cautelar_block_exists_for_infraction?/1 pour éviter la duplication de blocages. Si la contrepartie annule et ouvre une nouvelle infraction avec le même E2E, le nouveau blocage remplace le précédent.
Voir aussi : med-list et med-detail pour consulter les blocages conservatoires via External API.
5. Comment le merchant se défend
La défense N'est PAS disponible via External API
La soumission de défense (upload de preuves + justification) n'est possible que via le merchant portal (https://merchant.owem.com.br). Il n'y a pas d'endpoint External API POST /api/external/infractions/:id/defense aujourd'hui. C'est une limitation connue — voir section 8.
Flux de défense dans le merchant portal :
- L'opérateur authentifié accède à Compliance → Infractions dans le portail (
/compliance) - Voit la liste des infractions en
ACKNOWLEDGEDavecdefense_deadlinequi approche - Clique sur une infraction et remplit :
- Texte de la défense (jusqu'à ~5000 caractères, libre — sera envoyé au BACEN comme
AnalysisDetails) - Pièces jointes (jusqu'à 5 fichiers, 10MB chacun — preuves comme captures d'écran, contrats, historique de conversation)
- Texte de la défense (jusqu'à ~5000 caractères, libre — sera envoyé au BACEN comme
- Soumet → backend :
- Upload des pièces jointes vers le storage GCS
- Appelle OnZ
POST /v3/dict/infracao/{id}/defenseavec le texte + URLs des pièces jointes - Met à jour
pix_infractions.status=defense_submittedlocalement - Déclenche le webhook
pix.infraction.defense_submitted
Sources : controllers/merchant/infractions/defense_controller.ex.
Qui peut se défendre
Tout utilisateur avec la permission infractions:write dans le merchant portal. Les subconta operators (sans être primary holder) peuvent également soumettre une défense tant que le compte affecté est dans leurs account_ids. Les primary holders défendent n'importe quel compte du merchant.
6. Délais et auto-expiration
6.1 defense_deadline (délai BACEN)
Le BACEN définit un délai pour que le merchant réponde. Il vient dans le champ defense_deadline (ISO 8601 UTC) du webhook pix.infraction.created et est propagé vers pix_infractions.defense_deadline.
Typiquement, ce délai est de 7 jours calendaires à partir de la création de l'infraction, mais peut varier selon le type (REFUND_REQUEST vs fraude grave).
6.2 Auto-expiration par le worker MedDefenseExpiration
Pour éviter l'exposition réglementaire de l'institution (amende BACEN pour non-réponse dans les délais), Owem fait tourner le worker Fluxiq.Workers.MedDefenseExpiration toutes les 5 minutes via Oban cron :
- Cherche les blocages conservatoires avec
deadlinedans les prochaines 30 minutes etanalysis_statusencore enpendingoudefense_submitted - Pour chaque blocage, force le verdict
founded(accepte la contestation) et déclencheMed.execute_return/1→ PACS.004 est envoyé au BACEN - Libère le solde bloqué (voir TB phantom void)
- L'acteur de l'audit log est
system, pas un admin
Résultat : si vous ne vous défendez pas à temps, le système auto-accepte la contestation et rembourse la valeur avant l'expiration du délai BACEN.
Sources : workers/med_defense_expiration.ex (cron */5 * * * *).
L'auto-accept est conservateur
Le comportement par défaut est de protéger l'institution du risque réglementaire — c'est pourquoi le défaut est d'accepter la contestation et rembourser. Si vous avez besoin d'une logique différente (ex. : toujours se défendre automatiquement), contactez compliance@owem.com.br.
7. Résolution et webhook final
L'infraction termine toujours dans l'un de ces trois états terminaux :
| Status final | Analysis result | Ce qui s'est passé avec le solde | Webhook déclenché |
|---|---|---|---|
CLOSED | AGREED | Valeur remboursée à la counterparty via PACS.004 | pix.infraction.resolved + pix.refund.completed + pix.payout.returned |
CLOSED | DISAGREED | Blocage libéré, valeur revient dans l'available | pix.infraction.resolved |
CANCELLED | null | Contrepartie a annulé avant l'analyse — blocage libéré | pix.infraction.resolved |
Dans tous les cas, le blocage conservatoire TB est libéré via Fluxiq.UseCases.PixCompliance.Med.BlockRelease.release_for_e2e/2, qui fait le void de la pending transfer dans TigerBeetle et met à jour le med_cautelar_blocks.status.
Sources : use_cases/pix_compliance/med/block_release.ex.
Différence entre AGREED et DISAGREED
- AGREED : vous avez accepté (explicitement ou via auto-accept) le remboursement. Le PIX OUT (
pacs.004) est exécuté, la valeur SORT de votre compte, vous recevezpix.payout.returnedavecstatus="returned". - DISAGREED : vous vous êtes défendu et le BACEN a accepté la défense, OU l'infraction a été auto-refusée (≤R$1k ou E2E inexistant). La valeur reste avec vous, aucun PIX OUT n'est exécuté.
8. Limitations actuelles (External API)
Feature gap — opérations via External API
Aujourd'hui, seule la consultation indirecte via MED est disponible dans le namespace /api/external/* :
- Il y a
GET /api/external/med(Liste MED) etGET /api/external/med/:id(Détail MED) — les deux retournent des blocages MED, pas les infractions directement - Il n'y a PAS de
GET /api/external/infractionsniPOST /api/external/infractions/:id/defense - La liste complète des infractions et le flux de défense ne sont disponibles que via le merchant portal (JWT-based, pas API Key)
- Les webhooks
pix.infraction.*sont la manière officielle de notification automatique
Si votre cas d'usage exige l'automatisation via External API (ex. : soumettre une défense programmatiquement), contactez compliance@owem.com.br pour priorisation.
9. Webhooks liés
Les trois événements ci-dessous sont vos yeux et vos oreilles dans le cycle d'infraction. Abonnez-vous à tous dans Enregistrer Webhook :
| Événement | Quand il se déclenche | Payload complet |
|---|---|---|
pix.infraction.created | Nouvelle infraction détectée ET blocage conservatoire créé (>R$1k avec E2E existant) | webhooks-payloads#pix-infraction-created |
pix.infraction.resolved | Status est passé à CLOSED ou CANCELLED (AGREED, DISAGREED, auto-deny, auto-accept) | webhooks-payloads#pix-infraction-resolved |
pix.infraction.defense_submitted | Défense soumise via le portail (merchant ou admin) | webhooks-payloads#pix-infraction-defense-submitted |
Événements corrélés (couche MED) :
| Événement | Quand il se déclenche |
|---|---|
pix.refund.requested | Blocage conservatoire a été créé dans med_cautelar_blocks (le compte a eu le solde réservé) |
pix.refund.completed | Remboursement PACS.004 a été exécuté (AGREED ou auto-accept) |
pix.payout.returned | PIX remboursé avec E2E préfixe D — votre solde a été débité pour indemniser le payeur |
Auto-deny silencieux à ≤R$1k
Le webhook pix.infraction.created n'est pas déclenché dans le chemin d'auto-deny ≤R$1k/E2E inexistant (chemins 3.1 et 3.2 de la section 3). Seul pix.infraction.resolved est déclenché à la clôture. Cela évite du bruit pour le merchant dans les cas où il n'y a pas de décision à prendre.
Si vous voulez une visibilité totale (y compris auto-denies), consultez via le merchant portal /compliance — là apparaissent toutes les infractions indépendamment du chemin.
10. Réconciliation dans votre système
Recommandations pour réconcilier les infractions dans votre DRE / ERP :
10.1 Abonnez-vous aux webhooks
pix.infraction.created→ marque le PIX IN original comme « contesté » (flag interne)pix.refund.requested→ réserve la valeur dans votre ledger comme « en dispute »pix.infraction.resolved:- Si
analysis_result=DISAGREED→ libère la réserve, PIX IN reste définitif - Si
analysis_result=AGREED→ marque PIX IN comme « remboursé », crée une écriture de sortie
- Si
pix.refund.completed→ confirme la sortie du PIX OUT de remboursement
10.2 Utilisez l'e2e_id comme clé de corrélation
Toute infraction porte un e2e_id pointant vers le PIX IN original. Utilisez ce champ comme FK dans votre système pour :
- Localiser le PIX IN via
GET /transactions/ref/:external_id(si vous avez stockéexternal_id) ou via E2E direct - Croiser avec
med_cautelar_blocks(champoriginal_end_to_end_iddans med-detail) - Auditer le cycle complet
10.3 Conservez l'historique du analysis_details
Le champ analysis_details vient avec la justification envoyée au BACEN. Dans les cas d'auto-deny, vient le message standardisé. Dans les cas de défense soumise, vient le texte que votre opérateur a écrit. Ces données sont auditables et doivent être préservées pour la traçabilité réglementaire (LGPD Art. 46, BCB Résolution 4893).
Références techniques
backend/lib/fluxiq/use_cases/pix_compliance/pix_compliance.ex— orchestrationbackend/lib/fluxiq/use_cases/pix_compliance/med/processor.ex— MED Processor (phantom hold + PACS.004)backend/lib/fluxiq/use_cases/pix_compliance/med/block_release.ex— release de blocagebackend/lib/fluxiq/workers/compliance_sync_worker.ex— polling DICT (15 min)backend/lib/fluxiq/workers/med_defense_expiration.ex— auto-expiration (5 min)backend/lib/fluxiq/schemas/pix_compliance/pix_infraction.ex— schémabackend/lib/fluxiq/schemas/pix_compliance/med/med_cautelar_block.ex— schéma du blocage
FAQ
Puis-je ouvrir une infraction contre un autre merchant via API Owem ?
Non. Les infractions sont ouvertes uniquement par l'institution du payeur, pas par le bénéficiaire. Si vous êtes le payeur d'un PIX et voulez contester, utilisez votre propre canal de support (application de la banque, médiateur). Owem n'agit que du côté bénéficiaire.
Que se passe-t-il si je ne m'abonne pas aux webhooks pix.infraction.* ?
Le système continue à fonctionner — auto-deny et auto-accept se produisent automatiquement. Mais vous ne découvrirez le blocage qu'en regardant l'available dans /balance (il sera plus petit que le balance sécurisé). Pour lister les blocages actifs, consultez GET /api/external/med.
Pourquoi mon solde a-t-il baissé sans explication ?
Si vous voyez available plus petit que le balance, il y a probablement des blocages conservatoires MED actifs (> R$1k + E2E valide). Consultez GET /api/external/med pour les voir tous. Abonnez-vous à pix.refund.requested pour être notifié en temps réel quand un nouveau blocage est créé.
Puis-je augmenter ou désactiver le seuil de R$1 000 ?
Oui. Contactez compliance@owem.com.br. Le seuil est stocké dans med_configurations.min_threshold_amount et peut être configuré :
- Par merchant (s'applique à tous les comptes)
- Par compte spécifique (override du merchant)
- Default du système : R$1 000 (10 000 000 subcentavos)
Augmenter le seuil réduit le nombre de blocages conservatoires (plus d'auto-denies). Zéro seuil élimine l'auto-deny et fait que toute valeur contestée passe à l'analyse manuelle.
Owem a-t-il déjà pris des décisions en mon nom ?
Oui, dans deux scénarios explicitement automatiques :
- Auto-deny ≤R$1k ou E2E inexistant — le système refuse automatiquement au nom du merchant. La justification par défaut envoyée au BACEN est « Verificado pelo time de compliance e sem evidencias concretas nao temos como fazer devolucao ».
- Auto-accept près du délai BACEN — si le blocage conservatoire est à <30 min du
defense_deadlineet que le merchant n'a pas soumis de défense, le workerMedDefenseExpirationaccepte la contestation comme verdictfoundedet déclenche le remboursement.
Les deux comportements sont conservateurs pour protéger l'institution d'une amende réglementaire BACEN.
L'historique du merchant est-il visible via API ?
Partiellement — via GET /api/external/med (blocages) et GET /api/external/transactions/ref/:external_id (transactions originales et remboursements). L'historique complet des infractions avec tous les champs du BACEN (y compris analysisDetails, fraudType, situationType) est aujourd'hui disponible uniquement dans le merchant portal (/compliance).