PIX Infractions (end-to-end flow)
End-to-end view of the lifecycle of a PIX infraction in Owem Pay — how it is created, how it is processed automatically, when it blocks balance, how you submit a defense, and how to reconcile it in your system.
One-sentence summary
A PIX infraction is a dispute reported by the counterparty against a PIX that you received. It may trigger a preventive block on the disputed amount until resolution. Subscribe to the pix.infraction.* webhooks to act within the deadline.
1. What is a PIX infraction?
An infraction (type REFUND_REQUEST in the BACEN vocabulary) is a formal report registered in DICT by the payer's institution alleging that the PIX received by you shows evidence of fraud, scam, operational error, or a refund request.
The official channel is BACEN DICT — no end customer can open an infraction against you directly; it always passes through the payer's institution and through BACEN before reaching Owem.
| Technical field | Description |
|---|---|
infraction_type | Infraction type at BACEN. Values: REFUND_REQUEST (refund request) and REFUND_CANCELLED (cancellation of a previous request) |
status | State at BACEN. UPPERCASE values: ACKNOWLEDGED (acknowledged, under analysis), CLOSED (finalized), CANCELLED (cancelled by the counterparty before analysis) |
amount | Disputed value in subcentavos (not necessarily the full amount of the original PIX) |
e2e_id | End-to-End ID of the original PIX that was disputed |
defense_deadline | BACEN deadline for defense submission (ISO 8601 UTC) |
counterpart_ispb | ISPB of the payer's institution that opened the infraction |
analysis_result | Final decision when closed (CLOSED): AGREED (refund executed) or DISAGREED (defense accepted, no refund) |
Sources: schemas/pix_compliance/pix_infraction.ex.
Do not confuse with MED
MED (Special Refund Mechanism) is the regulatory mechanism that executes the refund when an infraction with confirmed fraud needs to be reimbursed — it is a consequence of an accepted infraction, not the initial event. See the Infraction ↔ MED relationship section below.
2. How Owem receives infractions
Owem does not receive infractions via push. The backend actively polls the DICT OnZ/BACEN every 15 minutes via the Fluxiq.Workers.ComplianceSyncWorker.
Discovery flow:
BACEN DICT
▲
│ GET /v3/dict/infracao/list (paginated 1000/call)
│
ComplianceSyncWorker (*/15 * * * *)
│
├─ upsert_infraction/2 → persists to pix_infractions
│ ├─ If new (is_new=true): dispatch "pix.infraction.created"
│ └─ If updated (status/analysis_result changed): dispatch "pix.infraction.resolved"
│
└─ maybe_auto_deny_or_block/2 → routes decision (see section 3)Sources: workers/compliance_sync_worker.ex, use_cases/pix_compliance/pix_compliance.ex.
BACEN → Webhook latency
An infraction opened at the counterparty reaches Owem within 15 min (next ComplianceSyncWorker cycle). The pix.infraction.created webhook is dispatched as soon as the row is inserted/updated in pix_infractions.
3. Classification and automatic actions
As soon as a REFUND_REQUEST infraction with status ACKNOWLEDGED or OPEN is detected, the backend classifies it into three automatic paths without manual intervention:
3.1 E2E not found in your transactions → auto-deny
If the infraction references an e2e_id that is not in your transactions table (no PIX IN with that E2E has arrived in your system), the backend assumes it is an operational error on the counterparty's side and performs an immediate auto-deny.
- OnZ action:
POST /v3/dict/infracao/{id}withAnalysisResult=DISAGREED - Updates
pix_infractions→status=CLOSED,analysis_result=DISAGREED,resolved_at=now - Webhook
pix.infraction.resolvedis dispatched - No preventive block is created
- No refund is executed
3.2 Amount ≤ R$ 1,000 (configurable threshold) → auto-deny
Low-value infractions are automatically denied. The default threshold is R$ 1,000.00 (10_000_000 subcentavos, constant @default_auto_deny_threshold), but can be configured per account or per merchant in the med_configurations table (field min_threshold_amount).
- The same auto-deny action from 3.1 is applied
- The standardized justification sent to BACEN is: "Verificado pelo time de compliance e sem evidencias concretas nao temos como fazer devolucao" (Verified by the compliance team with no concrete evidence; we cannot process the refund)
- Webhook
pix.infraction.resolvedis dispatched - No preventive block is created
- The amount of the received PIX remains available in the balance
Custom threshold per account
Accounts that work with legitimate high tickets (e.g., high-value marketplaces) may request an increase in the threshold via Owem admin. Accounts that prefer manual defense for any value may request threshold=0 (nothing is auto-denied).
3.3 Amount > R$ 1,000 AND E2E exists → MED preventive block
This is the path that impacts your balance. When detecting an eligible infraction, the backend:
- Updates
pix_infractions.statustoPROCESSING(prevents re-entry in the next sync cycle) - Creates a row in
med_cautelar_blockswith the disputed amount - Creates a phantom hold in TigerBeetle — a pending transfer against the affected account's wallet that reduces the available balance (
availablein Balance) by the disputed amount - Dispatches the
pix.refund.requestedwebhook (see payload) - Dispatches the
pix.infraction.createdwebhook (see payload)
The amount stays blocked until:
- The BACEN defense deadline expires (see section 6)
- You submit a defense via the portal (see section 5)
- BACEN resolves the infraction (section 7)
- The counterparty cancels (
status=CANCELLED) before analysis
Sources: use_cases/pix_compliance/pix_compliance.ex:495-537, use_cases/pix_compliance/med/processor.ex.
4. Infraction ↔ MED relationship
The infraction and the MED (Special Refund Mechanism) are distinct layers of the same cycle:
| Layer | What it is | Table | Main webhook |
|---|---|---|---|
| Infraction | Formal report at BACEN DICT | pix_infractions | pix.infraction.created |
| MED preventive block | Balance reservation in TigerBeetle during analysis | med_cautelar_blocks | pix.refund.requested |
| MED refund | PACS.004 executed after AGREED decision | transactions (type=pix_return) | pix.refund.completed + pix.payout.returned |
[Infraction created at BACEN]
│
▼
ComplianceSyncWorker detects (15-min polling)
│
├─ pix.infraction.created (always, when is_new=true)
│
│ If > R$ 1,000 AND E2E exists:
▼
MED Processor → preventive block + TB phantom hold
│
├─ pix.refund.requested (block created)
│
│ Awaits decision (merchant defends OR deadline expires OR auto-accept session 135)
▼
[Resolution]
│
├─ AGREED → PACS.004 dispatched
│ ├─ pix.refund.completed
│ └─ pix.payout.returned (prefix D in E2E)
│
└─ DISAGREED → block released
└─ BlockRelease.release_for_e2e — void of TB phantom + balance release
│
▼
pix.infraction.resolved (always on close — AGREED or DISAGREED)The relationship is 1-to-1 by E2E, not 1-to-1 by ID
The same PIX transaction (same e2e_id) cannot have two active infractions simultaneously — ComplianceSyncWorker guards with cautelar_block_exists_for_infraction?/1 to prevent duplicate blocks. If the counterparty cancels and opens a new infraction with the same E2E, the new block replaces the previous one.
See also: med-list and med-detail to query preventive blocks via External API.
5. How the merchant submits a defense
Defense is NOT available via External API
Defense submission (evidence upload + justification) is only possible via the merchant portal (https://merchant.owem.com.br). There is no External API endpoint POST /api/external/infractions/:id/defense today. This is a known limitation — see section 8.
Defense flow in the merchant portal:
- The authenticated operator accesses Compliance → Infractions in the portal (
/compliance) - Sees the list of
ACKNOWLEDGEDinfractions with approachingdefense_deadline - Clicks on an infraction and fills in:
- Defense text (up to ~5000 characters, free-form — will be sent to BACEN as
AnalysisDetails) - Attachments (up to 5 files, 10MB each — evidence such as screenshots, contracts, conversation history)
- Defense text (up to ~5000 characters, free-form — will be sent to BACEN as
- Submits → backend:
- Uploads attachments to GCS storage
- Calls OnZ
POST /v3/dict/infracao/{id}/defensewith the text + attachment URLs - Updates
pix_infractions.status=defense_submittedlocally - Dispatches webhook
pix.infraction.defense_submitted
Sources: controllers/merchant/infractions/defense_controller.ex.
Who can defend
Any user with infractions:write permission in the merchant portal. Subaccount operators (non primary holders) can also submit a defense provided the affected account is in their account_ids. Primary holders defend any account of the merchant.
6. Deadlines and auto-expiration
6.1 defense_deadline (BACEN deadline)
BACEN sets a deadline for the merchant to respond. It comes in the defense_deadline field (ISO 8601 UTC) of the pix.infraction.created webhook and is propagated to pix_infractions.defense_deadline.
Typically, this deadline is 7 calendar days from the infraction's creation, but may vary by type (REFUND_REQUEST vs serious fraud).
6.2 Auto-expiration by the MedDefenseExpiration worker
To avoid regulatory exposure of the institution (BACEN fine for not responding in time), Owem runs the Fluxiq.Workers.MedDefenseExpiration worker every 5 minutes via Oban cron:
- Searches for preventive blocks with
deadlinewithin the next 30 minutes andanalysis_statusstillpendingordefense_submitted - For each block, forces the
foundedverdict (accepts the dispute) and dispatchesMed.execute_return/1→ PACS.004 is sent to BACEN - Releases the blocked balance (via TB phantom void)
- The audit log actor is
system, not an admin
Result: if you do not defend in time, the system auto-accepts the dispute and refunds the amount before the BACEN deadline expires.
Sources: workers/med_defense_expiration.ex (cron */5 * * * *).
Auto-accept is conservative
The default behavior is to protect the institution from regulatory risk — so the default is to accept the dispute and refund. If you need different logic (e.g., always defend automatically), contact compliance@owem.com.br.
7. Resolution and final webhook
The infraction always ends in one of these three terminal states:
| Final status | Analysis result | What happened to the balance | Webhook dispatched |
|---|---|---|---|
CLOSED | AGREED | Amount refunded to the counterparty via PACS.004 | pix.infraction.resolved + pix.refund.completed + pix.payout.returned |
CLOSED | DISAGREED | Block released, amount returns to available | pix.infraction.resolved |
CANCELLED | null | Counterparty cancelled before analysis — block released | pix.infraction.resolved |
In all cases, the TB preventive block is released via Fluxiq.UseCases.PixCompliance.Med.BlockRelease.release_for_e2e/2, which voids the pending transfer in TigerBeetle and updates med_cautelar_blocks.status.
Sources: use_cases/pix_compliance/med/block_release.ex.
Difference between AGREED and DISAGREED
- AGREED: you agreed (explicitly or via auto-accept) with the refund. The PIX OUT (
pacs.004) is executed, the amount LEAVES your account, you receivepix.payout.returnedwithstatus="returned". - DISAGREED: you defended and BACEN accepted the defense, OR the infraction was auto-denied (≤R$1k or E2E not found). The amount stays with you, no PIX OUT is executed.
8. Current limitations (External API)
Feature gap — operations via External API
Today, only indirect query via MED is available in the /api/external/* namespace:
GET /api/external/med(List MED) andGET /api/external/med/:id(MED Details) exist — both return MED blocks, not infractions directly- There is NO
GET /api/external/infractionsnorPOST /api/external/infractions/:id/defense - The complete list of infractions and the defense flow are available only via merchant portal (JWT-based, not API Key)
pix.infraction.*webhooks are the official form of automatic notification
If your use case requires automation via External API (e.g., submitting a defense programmatically), contact compliance@owem.com.br for prioritization.
9. Related webhooks
The three events below are your eyes and ears in the infraction cycle. Subscribe to all of them at Register Webhook:
| Event | When it fires | Full payload |
|---|---|---|
pix.infraction.created | New infraction detected AND preventive block created (>R$1k with existing E2E) | webhooks-payloads#pix-infraction-created |
pix.infraction.resolved | Status moved to CLOSED or CANCELLED (AGREED, DISAGREED, auto-deny, auto-accept) | webhooks-payloads#pix-infraction-resolved |
pix.infraction.defense_submitted | Defense submitted via portal (merchant or admin) | webhooks-payloads#pix-infraction-defense-submitted |
Related events (MED layer):
| Event | When it fires |
|---|---|
pix.refund.requested | A preventive block was created in med_cautelar_blocks (account balance was reserved) |
pix.refund.completed | PACS.004 refund was executed (AGREED or auto-accept) |
pix.payout.returned | PIX returned with E2E prefix D — your balance was debited to reimburse the payer |
Silent auto-deny on ≤R$1k
The pix.infraction.created webhook is not dispatched on the auto-deny path for ≤R$1k / E2E not found (paths 3.1 and 3.2 in section 3). Only pix.infraction.resolved is dispatched when closing. This avoids noise for the merchant in cases where there is no decision to make.
If you want full visibility (including auto-denies), check the merchant portal /compliance — all infractions appear there regardless of path.
10. Reconciliation in your system
Recommendations for reconciling infractions in your DRE / ERP:
10.1 Subscribe to the webhooks
pix.infraction.created→ mark the original PIX IN as "disputed" (internal flag)pix.refund.requested→ reserve the amount in your ledger as "disputed"pix.infraction.resolved:- If
analysis_result=DISAGREED→ release reserve, PIX IN remains final - If
analysis_result=AGREED→ mark PIX IN as "refunded", create outflow entry
- If
pix.refund.completed→ confirms the outflow of the refund PIX OUT
10.2 Use the e2e_id as the correlation key
Every infraction carries e2e_id pointing to the original PIX IN. Use this field as an FK in your system to:
- Locate the PIX IN via
GET /transactions/ref/:external_id(if you storedexternal_id) or via E2E directly - Cross-reference with
med_cautelar_blocks(fieldoriginal_end_to_end_idin med-detail) - Audit the complete cycle
10.3 Keep the history of analysis_details
The analysis_details field comes with the justification sent to BACEN. In auto-deny cases, it carries the standardized message. In submitted defense cases, it carries the text your operator wrote. This data is auditable and must be preserved for regulatory traceability (LGPD Art. 46, BCB Resolution 4893).
Technical references
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— block releasebackend/lib/fluxiq/workers/compliance_sync_worker.ex— DICT polling (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— block schema
FAQ
Can I open an infraction against another merchant via the Owem API?
No. Infractions are opened only by the payer's institution, not by the recipient. If you are the payer of a PIX and want to dispute it, use your own customer support channel (bank app, ombudsman). Owem only operates on the recipient side.
What happens if I do not subscribe to the pix.infraction.* webhooks?
The system keeps working — auto-deny and auto-accept occur automatically. But you will only discover the block by looking at available in /balance (it will be lower than the safe balance). To list active blocks, query GET /api/external/med.
Why did my balance drop without explanation?
If you see available lower than balance, there are likely active MED preventive blocks (> R$1k + valid E2E). Query GET /api/external/med to see all of them. Subscribe to pix.refund.requested to be notified in real time when a new block is created.
Can I raise or disable the R$1,000 threshold?
Yes. Contact compliance@owem.com.br. The threshold is stored in med_configurations.min_threshold_amount and can be configured:
- Per merchant (applies to all accounts)
- Per specific account (overrides the merchant)
- System default: R$1,000 (10,000,000 subcentavos)
Raising the threshold reduces the number of preventive blocks (more auto-denies). Setting threshold=0 eliminates auto-deny and makes any disputed amount go to manual analysis.
Has Owem already made decisions on my behalf?
Yes, in two explicitly automatic scenarios:
- Auto-deny ≤R$1k or E2E not found — the system denies automatically on behalf of the merchant. The default justification sent to BACEN is "Verificado pelo time de compliance e sem evidencias concretas nao temos como fazer devolucao".
- Auto-accept near the BACEN deadline — if the preventive block is less than 30 min from
defense_deadlineand the merchant has not submitted a defense, theMedDefenseExpirationworker accepts the dispute asfoundedverdict and dispatches the refund.
Both behaviors are conservative to protect the institution from regulatory fines from BACEN.
Is merchant history visible via API?
Partially — via GET /api/external/med (blocks) and GET /api/external/transactions/ref/:external_id (original transactions and refunds). Full infraction history with all BACEN fields (including analysisDetails, fraudType, situationType) is today available only in the merchant portal (/compliance).