Skip to content

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 techniqueDescription
infraction_typeType 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)
amountValeur contestée en subcentavos (pas nécessairement la valeur totale du PIX original)
e2e_idEnd-to-End ID du PIX original qui a été contesté
defense_deadlineDélai BACEN pour soumission de la défense (ISO 8601 UTC)
counterpart_ispbISPB de l'institution du payeur qui a ouvert l'infraction
analysis_resultDé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} avec AnalysisResult=DISAGREED
  • Met à jour pix_infractionsstatus=CLOSED, analysis_result=DISAGREED, resolved_at=now
  • Webhook pix.infraction.resolved est 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.resolved est 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 :

  1. Met à jour pix_infractions.status vers PROCESSING (évite la ré-entrée au prochain cycle du sync)
  2. Crée une ligne dans med_cautelar_blocks avec la valeur contestée
  3. Crée un phantom hold dans TigerBeetle — transfert en attente contre la wallet du compte affecté, qui réduit le solde disponible (available dans Solde) de la valeur contestée
  4. Déclenche le webhook pix.refund.requested (voir payload)
  5. 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 :

CoucheCe que c'estTableWebhook principal
InfractionSignalement formel dans le BACEN DICTpix_infractionspix.infraction.created
Blocage conservatoire MEDRéservation de solde dans TigerBeetle pendant l'analysemed_cautelar_blockspix.refund.requested
Remboursement MEDPACS.004 exécuté après décision AGREEDtransactions (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 :

  1. L'opérateur authentifié accède à Compliance → Infractions dans le portail (/compliance)
  2. Voit la liste des infractions en ACKNOWLEDGED avec defense_deadline qui approche
  3. 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)
  4. Soumet → backend :
    • Upload des pièces jointes vers le storage GCS
    • Appelle OnZ POST /v3/dict/infracao/{id}/defense avec le texte + URLs des pièces jointes
    • Met à jour pix_infractions.status=defense_submitted localement
    • 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 deadline dans les prochaines 30 minutes et analysis_status encore en pending ou defense_submitted
  • Pour chaque blocage, force le verdict founded (accepte la contestation) et déclenche Med.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 finalAnalysis resultCe qui s'est passé avec le soldeWebhook déclenché
CLOSEDAGREEDValeur remboursée à la counterparty via PACS.004pix.infraction.resolved + pix.refund.completed + pix.payout.returned
CLOSEDDISAGREEDBlocage libéré, valeur revient dans l'availablepix.infraction.resolved
CANCELLEDnullContrepartie 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 recevez pix.payout.returned avec status="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) et GET /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/infractions ni POST /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énementQuand il se déclenchePayload complet
pix.infraction.createdNouvelle infraction détectée ET blocage conservatoire créé (>R$1k avec E2E existant)webhooks-payloads#pix-infraction-created
pix.infraction.resolvedStatus est passé à CLOSED ou CANCELLED (AGREED, DISAGREED, auto-deny, auto-accept)webhooks-payloads#pix-infraction-resolved
pix.infraction.defense_submittedDéfense soumise via le portail (merchant ou admin)webhooks-payloads#pix-infraction-defense-submitted

Événements corrélés (couche MED) :

ÉvénementQuand il se déclenche
pix.refund.requestedBlocage conservatoire a été créé dans med_cautelar_blocks (le compte a eu le solde réservé)
pix.refund.completedRemboursement PACS.004 a été exécuté (AGREED ou auto-accept)
pix.payout.returnedPIX 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
  • 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 :

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


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 :

  1. 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 ».
  2. Auto-accept près du délai BACEN — si le blocage conservatoire est à <30 min du defense_deadline et que le merchant n'a pas soumis de défense, le worker MedDefenseExpiration accepte la contestation comme verdict founded et 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).

Owem Pay Instituição de Pagamento — ISPB 37839059