Infracciones PIX (flujo completo)
Vision end-to-end del ciclo de vida de una infraccion PIX en Owem Pay — como nace, como es procesada automaticamente, cuando bloquea saldo, como te defiendes y como reconciliar en tu sistema.
Resumen en una frase
Una infraccion PIX es una impugnacion reportada por la contraparte contra un PIX que tu recibiste. Puede generar bloqueo cautelar del valor impugnado hasta la resolucion. Suscribe los webhooks pix.infraction.* para actuar dentro del plazo.
1. Que es una infraccion PIX?
Una infraccion (del tipo REFUND_REQUEST en el vocabulario BACEN) es un relato formal registrado en el DICT por la institucion del pagador alegando que el PIX recibido por ti presenta indicio de fraude, estafa, error operativo o solicitud de reembolso.
El canal oficial es el BACEN DICT — ningun cliente puede abrir infraccion directamente contra ti; ella siempre pasa por la institucion pagadora y por el BACEN antes de llegar a Owem.
| Campo tecnico | Descripcion |
|---|---|
infraction_type | Tipo de la infraccion en BACEN. Valores: REFUND_REQUEST (solicitud de devolucion) y REFUND_CANCELLED (cancelacion de solicitud anterior) |
status | Estado en BACEN. Valores en UPPERCASE: ACKNOWLEDGED (reconocida, en analisis), CLOSED (finalizada), CANCELLED (cancelada por la contraparte antes del analisis) |
amount | Valor impugnado en subcentavos (no necesariamente el valor total del PIX original) |
e2e_id | End-to-End ID del PIX original que fue impugnado |
defense_deadline | Plazo BACEN para sumision de defensa (ISO 8601 UTC) |
counterpart_ispb | ISPB de la institucion del pagador que abrio la infraccion |
analysis_result | Decision final al cerrar (CLOSED): AGREED (devolucion ejecutada) o DISAGREED (defensa aceptada, sin devolucion) |
Fuentes: schemas/pix_compliance/pix_infraction.ex.
No confundas con MED
MED (Mecanismo Especial de Devolucion) es el mecanismo regulatorio que ejecuta la devolucion cuando una infraccion con fraude confirmado necesita ser resarcida — es consecuencia de una infraccion aceptada, no el evento inicial. Ver la seccion Relacion infraccion ↔ MED abajo.
2. Como Owem recibe infracciones
Owem no recibe infracciones via push. El backend hace polling activo en el DICT OnZ/BACEN cada 15 minutos via el worker Fluxiq.Workers.ComplianceSyncWorker.
Flujo de descubrimiento:
BACEN DICT
▲
│ GET /v3/dict/infracao/list (paginado 1000/call)
│
ComplianceSyncWorker (*/15 * * * *)
│
├─ upsert_infraction/2 → persiste en pix_infractions
│ ├─ Si nueva (is_new=true): dispatch "pix.infraction.created"
│ └─ Si actualizacion (status/analysis_result cambio): dispatch "pix.infraction.resolved"
│
└─ maybe_auto_deny_or_block/2 → enruta decision (ver seccion 3)Fuentes: workers/compliance_sync_worker.ex, use_cases/pix_compliance/pix_compliance.ex.
Latencia BACEN → Webhook
Una infraccion abierta en la contraparte llega a Owem en hasta 15 min (proximo ciclo del ComplianceSyncWorker). El webhook pix.infraction.created se dispara tan pronto la fila se inserta/actualiza en pix_infractions.
3. Clasificacion y acciones automaticas
Tan pronto una infraccion REFUND_REQUEST con status ACKNOWLEDGED u OPEN es detectada, el backend clasifica en tres caminos automaticos sin intervencion manual:
3.1 E2E inexistente en tus transacciones → auto-deny
Si la infraccion referencia un e2e_id que no esta en tu tabla transactions (ningun PIX IN con ese E2E llego a tu sistema), el backend asume que es un error operativo de la contraparte y realiza auto-deny inmediato.
- Accion OnZ:
POST /v3/dict/infracao/{id}conAnalysisResult=DISAGREED - Actualiza
pix_infractions→status=CLOSED,analysis_result=DISAGREED,resolved_at=now - Webhook
pix.infraction.resolvedes disparado - Ningun bloqueo cautelar es creado
- Ninguna devolucion es ejecutada
3.2 Valor ≤ R$ 1.000 (threshold configurable) → auto-deny
Infracciones de bajo valor son automaticamente negadas. El limite por defecto es R$ 1.000,00 (10_000_000 subcentavos, constante @default_auto_deny_threshold), pero puede ser configurado por cuenta o por merchant en la tabla med_configurations (campo min_threshold_amount).
- La misma accion de auto-deny del item 3.1 es aplicada
- La justificativa estandarizada enviada al BACEN es: "Verificado pelo time de compliance e sem evidencias concretas nao temos como fazer devolucao"
- Webhook
pix.infraction.resolvedes disparado - Ningun bloqueo cautelar es creado
- El valor del PIX recibido continua disponible en el saldo
Threshold personalizado por cuenta
Cuentas que trabajan con tickets altos legitimos (ej: marketplaces de alto valor) pueden solicitar aumento del threshold via admin Owem. Cuentas que prefieren defensa manual para cualquier valor pueden solicitar threshold=0 (nada es auto-negado).
3.3 Valor > R$ 1.000 Y E2E existe → bloqueo cautelar MED
Este es el camino que impacta tu saldo. Al detectar una infraccion elegible, el backend:
- Actualiza
pix_infractions.statusaPROCESSING(evita re-entrada en el proximo ciclo del sync) - Crea una fila en
med_cautelar_blockscon el valor impugnado - Crea un phantom hold en TigerBeetle — transferencia pendiente contra la wallet de la cuenta afectada, que reduce el saldo disponible (
availableen Saldo) en el valor impugnado - Dispara el webhook
pix.refund.requested(ver payload) - Dispara el webhook
pix.infraction.created(ver payload)
El valor queda bloqueado hasta que:
- El plazo de defensa BACEN expire (ver seccion 6)
- Tu envies una defensa via portal (ver seccion 5)
- El BACEN resuelva la infraccion (seccion 7)
- La contraparte cancele (
status=CANCELLED) antes del analisis
Fuentes: use_cases/pix_compliance/pix_compliance.ex:495-537, use_cases/pix_compliance/med/processor.ex.
4. Relacion infraccion ↔ MED
La infraccion y el MED (Mecanismo Especial de Devolucion) son capas distintas del mismo ciclo:
| Capa | Que es | Tabla | Webhook principal |
|---|---|---|---|
| Infraccion | Relato formal en el BACEN DICT | pix_infractions | pix.infraction.created |
| Bloqueo cautelar MED | Reserva de saldo en TigerBeetle durante analisis | med_cautelar_blocks | pix.refund.requested |
| Devolucion MED | PACS.004 ejecutado despues de decision AGREED | transactions (type=pix_return) | pix.refund.completed + pix.payout.returned |
[Infraccion creada en BACEN]
│
▼
ComplianceSyncWorker detecta (15 min polling)
│
├─ pix.infraction.created (siempre, cuando is_new=true)
│
│ Si > R$ 1.000 Y E2E existe:
▼
MED Processor → cautelar block + TB phantom hold
│
├─ pix.refund.requested (bloqueo creado)
│
│ Aguarda decision (merchant defiende O plazo vence O auto-accept session 135)
▼
[Resolucion]
│
├─ AGREED → PACS.004 dispatched
│ ├─ pix.refund.completed
│ └─ pix.payout.returned (prefijo D en el E2E)
│
└─ DISAGREED → bloqueo liberado
└─ BlockRelease.release_for_e2e — void del TB phantom + release del saldo
│
▼
pix.infraction.resolved (siempre al cerrar — AGREED o DISAGREED)La relacion es 1-a-1 por E2E, no 1-a-1 por ID
Una misma transaccion PIX (mismo e2e_id) no puede tener dos infracciones activas al mismo tiempo — el ComplianceSyncWorker hace guard en cautelar_block_exists_for_infraction?/1 para evitar duplicacion de bloqueos. Si la contraparte cancela y abre nueva infraccion con el mismo E2E, el nuevo bloqueo sustituye al anterior.
Ver tambien: med-list y med-detail para consultar los bloqueos cautelares via External API.
5. Como el merchant se defiende
La defensa NO esta disponible via External API
El envio de defensa (subida de evidencias + justificativa) solo es posible via merchant portal (https://merchant.owem.com.br). No hay endpoint External API POST /api/external/infractions/:id/defense hoy. Esta es una limitacion conocida — ver seccion 8.
Flujo de defensa en el merchant portal:
- El operador autenticado accede a Compliance → Infracciones en el portal (
/compliance) - Ve la lista de infracciones en
ACKNOWLEDGEDcondefense_deadlineaproximandose - Hace clic en una infraccion y rellena:
- Texto de la defensa (hasta ~5000 caracteres, libre — sera enviado al BACEN como
AnalysisDetails) - Adjuntos (hasta 5 archivos, 10MB cada uno — evidencias como capturas, contratos, historial de conversacion)
- Texto de la defensa (hasta ~5000 caracteres, libre — sera enviado al BACEN como
- Envia → backend:
- Sube los adjuntos al storage GCS
- Llama OnZ
POST /v3/dict/infracao/{id}/defensecon el texto + URLs de los adjuntos - Actualiza
pix_infractions.status=defense_submittedlocalmente - Dispara webhook
pix.infraction.defense_submitted
Fuentes: controllers/merchant/infractions/defense_controller.ex.
Quien puede defender
Cualquier usuario con permiso infractions:write en el merchant portal. Subcuenta operators (sin ser primary holder) tambien pueden enviar defensa siempre que la cuenta afectada este en sus account_ids. Primary holders defienden cualquier cuenta del merchant.
6. Plazos y auto-expiracion
6.1 defense_deadline (plazo BACEN)
El BACEN define un plazo para que el merchant responda. Viene en el campo defense_deadline (ISO 8601 UTC) del webhook pix.infraction.created y se propaga a pix_infractions.defense_deadline.
Tipicamente, ese plazo es de 7 dias corridos a partir de la creacion de la infraccion, pero puede variar segun el tipo (REFUND_REQUEST vs fraude grave).
6.2 Auto-expiracion por el worker MedDefenseExpiration
Para evitar exposicion regulatoria de la institucion (multa BACEN por no responder en plazo), Owem ejecuta el worker Fluxiq.Workers.MedDefenseExpiration cada 5 minutos via Oban cron:
- Busca bloqueos cautelares con
deadlineen los proximos 30 minutos yanalysis_statusaun enpendingodefense_submitted - Para cada bloqueo, fuerza el veredicto
founded(acepta la impugnacion) y disparaMed.execute_return/1→ PACS.004 es enviado al BACEN - Libera el saldo bloqueado (ver TB phantom void)
- El actor del audit log es
system, no un admin
Resultado: si no defiendes a tiempo, el sistema auto-acepta la impugnacion y devuelve el valor antes que expire el plazo BACEN.
Fuentes: workers/med_defense_expiration.ex (cron */5 * * * *).
Auto-aceptacion es conservadora
El comportamiento por defecto es proteger a la institucion del riesgo regulatorio — por eso el default es aceptar la impugnacion y devolver. Si necesitas logica diferente (ej: siempre defender automaticamente), contacta compliance@owem.com.br.
7. Resolucion y webhook final
La infraccion siempre termina en uno de estos tres estados terminales:
| Status final | Analysis result | Que sucedio con el saldo | Webhook disparado |
|---|---|---|---|
CLOSED | AGREED | Valor devuelto a la contraparte via PACS.004 | pix.infraction.resolved + pix.refund.completed + pix.payout.returned |
CLOSED | DISAGREED | Bloqueo liberado, valor regresa al available | pix.infraction.resolved |
CANCELLED | null | Contraparte cancelo antes del analisis — bloqueo liberado | pix.infraction.resolved |
En todos los casos, el bloqueo cautelar TB es liberado via Fluxiq.UseCases.PixCompliance.Med.BlockRelease.release_for_e2e/2, que hace el void de la pending transfer en TigerBeetle y actualiza el med_cautelar_blocks.status.
Fuentes: use_cases/pix_compliance/med/block_release.ex.
Diferencia entre AGREED y DISAGREED
- AGREED: concordaste (explicitamente o via auto-accept) con la devolucion. El PIX OUT (
pacs.004) es ejecutado, el valor SALE de tu cuenta, recibespix.payout.returnedconstatus="returned". - DISAGREED: defendiste y el BACEN acato la defensa, O la infraccion fue auto-negada (≤R$1k o E2E inexistente). El valor queda contigo, ningun PIX OUT es ejecutado.
8. Limitaciones actuales (External API)
Feature gap — operaciones via External API
Hoy, solo consulta indirecta via MED esta disponible en el namespace /api/external/*:
- Existe
GET /api/external/med(Listar MED) yGET /api/external/med/:id(Detalle MED) — ambos retornan bloqueos MED, no infracciones directamente - NO existe
GET /api/external/infractionsniPOST /api/external/infractions/:id/defense - La lista completa de infracciones y el flujo de defensa estan disponibles solo via merchant portal (JWT-based, no API Key)
- Webhooks
pix.infraction.*son la forma oficial de notificacion automatica
Si tu caso de uso exige automatizacion via External API (ej: enviar defensa programaticamente), contacta compliance@owem.com.br para priorizacion.
9. Webhooks relacionados
Los tres eventos abajo son tus ojos y oidos en el ciclo de infraccion. Suscribe todos en Registrar Webhook:
| Evento | Cuando dispara | Payload completo |
|---|---|---|
pix.infraction.created | Nueva infraccion detectada Y bloqueo cautelar creado (>R$1k con E2E existente) | webhooks-payloads#pix-infraction-created |
pix.infraction.resolved | Status paso a CLOSED o CANCELLED (AGREED, DISAGREED, auto-deny, auto-accept) | webhooks-payloads#pix-infraction-resolved |
pix.infraction.defense_submitted | Defensa enviada via portal (merchant o admin) | webhooks-payloads#pix-infraction-defense-submitted |
Eventos correlatos (capa MED):
| Evento | Cuando dispara |
|---|---|
pix.refund.requested | Bloqueo cautelar fue creado en med_cautelar_blocks (cuenta tuvo saldo reservado) |
pix.refund.completed | Devolucion PACS.004 fue ejecutada (AGREED o auto-accept) |
pix.payout.returned | PIX devuelto con E2E prefijo D — tu saldo fue debitado para resarcir al pagador |
Auto-deny silencioso en ≤R$1k
El webhook pix.infraction.created no es disparado en el camino de auto-deny ≤R$1k/E2E inexistente (caminos 3.1 y 3.2 en la seccion 3). Solo pix.infraction.resolved es disparado al cerrar. Esto evita ruido para el merchant en casos donde no hay decision que tomar.
Si quieres visibilidad total (incluyendo auto-denies), consulta via merchant portal /compliance — alli aparecen todas las infracciones independiente del camino.
10. Reconciliacion en tu sistema
Recomendaciones para conciliar infracciones en tu DRE / ERP:
10.1 Suscribe los webhooks
pix.infraction.created→ marca PIX IN original como "impugnado" (flag interno)pix.refund.requested→ reserva el valor en tu ledger como "disputado"pix.infraction.resolved:- Si
analysis_result=DISAGREED→ libera reserva, PIX IN permanece definitivo - Si
analysis_result=AGREED→ marca PIX IN como "devuelto", crea asiento de salida
- Si
pix.refund.completed→ confirma la salida del PIX OUT de devolucion
10.2 Usa el e2e_id como clave de correlacion
Toda infraccion lleva e2e_id apuntando al PIX IN original. Usa ese campo como FK en tu sistema para:
- Localizar el PIX IN via
GET /transactions/ref/:external_id(si almacenasteexternal_id) o via E2E directo - Cruzar con
med_cautelar_blocks(campooriginal_end_to_end_iden med-detail) - Auditar el ciclo completo
10.3 Manten historial del analysis_details
El campo analysis_details viene con la justificativa enviada al BACEN. En casos de auto-deny, viene el mensaje estandarizado. En casos de defensa enviada, viene el texto que tu operador escribio. Esos datos son auditables y deben ser preservados para rastreabilidad regulatoria (LGPD Art. 46, BCB Resolucion 4893).
Referencias tecnicas
backend/lib/fluxiq/use_cases/pix_compliance/pix_compliance.ex— orquestacionbackend/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 bloqueobackend/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— schemabackend/lib/fluxiq/schemas/pix_compliance/med/med_cautelar_block.ex— schema del bloqueo
FAQ
Puedo abrir una infraccion contra otro merchant via API Owem?
No. Infracciones son abiertas solo por la institucion del pagador, no por el receptor. Si eres el pagador de un PIX y quieres impugnar, usa tu propio canal de atencion (app del banco, defensoria). Owem solo actua en el lado receptor.
Que sucede si no suscribo los webhooks pix.infraction.*?
El sistema continua funcionando — auto-deny y auto-accept ocurren automaticamente. Pero solo descubriras el bloqueo mirando el available en /balance (estara menor que el balance seguro). Para listar bloqueos activos, consulta GET /api/external/med.
Por que mi saldo cayo sin explicacion?
Si ves available menor que el balance, probablemente hay bloqueos cautelares MED activos (> R$1k + E2E valido). Consulta GET /api/external/med para ver todos. Suscribe pix.refund.requested para ser notificado en tiempo real cuando un nuevo bloqueo es creado.
Puedo aumentar o desactivar el threshold de R$1.000?
Si. Contacta compliance@owem.com.br. El threshold es almacenado en med_configurations.min_threshold_amount y puede ser configurado:
- Por merchant (aplica en todas las cuentas)
- Por cuenta especifica (override del merchant)
- Default del sistema: R$1.000 (10.000.000 subcentavos)
Aumentar el threshold reduce el numero de bloqueos cautelares (mas auto-denies). Cerar el threshold elimina auto-deny y hace que cualquier valor disputado pase a analisis manual.
Owem ya tomo decisiones en mi nombre?
Si, en dos escenarios explicitamente automaticos:
- Auto-deny ≤R$1k o E2E inexistente — el sistema niega automaticamente en nombre del merchant. La justificativa por defecto enviada al BACEN es "Verificado pelo time de compliance e sem evidencias concretas nao temos como fazer devolucao".
- Auto-accept cerca del plazo BACEN — si el bloqueo cautelar esta a <30 min del
defense_deadliney el merchant no ha enviado defensa, el workerMedDefenseExpirationacepta la impugnacion como verdictfoundedy dispara la devolucion.
Ambos comportamientos son conservadores para proteger a la institucion de multa regulatoria BACEN.
El historial del merchant es visible via API?
Parcialmente — via GET /api/external/med (bloqueos) y GET /api/external/transactions/ref/:external_id (transacciones originales y devoluciones). El historial completo de infracciones con todos los campos del BACEN (incluyendo analysisDetails, fraudType, situationType) hoy esta disponible solo en el merchant portal (/compliance).