Skip to main content

Policies and Rules

Policies let you configure preventive rules for PIX operations. They are set in the Backoffice panel and evaluated automatically by the API. Everything the screen offers is evaluated at runtime: PIX Out, PIX In, QR Code, keys, refunds, operating hours, and DICT limits.

Configuration Hierarchy

Policies have two levels:

Tenant (base) -> Account (overrides the tenant)

Resolution happens per section: if the account defines pixOut, the account's whole pixOut section is used and the tenant's is ignored. Sections not defined on the account are inherited from the tenant. A section that exists at neither level is simply not evaluated — there is no "default" level behind it holding implicit values.

The only exception are the DICT limits, which have platform-wide defaults when the policy does not define them — see DICT Lookup Limits.

PIX Out Rules

Rules are evaluated in this order:

OrderRuleruleDescription
1Kill SwitchpixOutDisabledenabled: false rejects every outgoing transfer. It is absolute — it beats the whitelist
2WhitelistRecipient document on the whitelist: allows and skips every rule below
3BlacklistcpfCnpjBlacklistRejects if the recipient document is blacklisted
4Same OwnershipsameOwnershipOnlyOnly allows transfers to the account holder's own CPF/CNPJ
5Operating HoursoperatingHoursRejects outside the configured window
6Per-transaction LimitmaxAmountMaximum amount per transfer
7Night LimitnightMaxAmountMaximum amount between 20:00 and 06:00 (BRT)
8Person TypeallowedPersonTypesRestricts the recipient to PF or PJ (by document length)

Unlike the whitelist, which short-circuits, all other rules are evaluated: a transaction can violate more than one, and the violations array carries all of them.

Two evaluation phases

The recipient's document does not always exist at POST time:

  • At POST (edge): kill switch, operating hours, and amount limits. Document rules are evaluated too when the mode is BANK_ACCOUNT, the only one that carries documentNumber in the body.
  • After resolving the recipient: in KEY and QRCODE modes the document only appears once the key is looked up in DICT or the BR Code is decoded. Document rules are evaluated at that point, before the payment is submitted to the partner. The same applies to amount limits when a QR Code is paid without amount in the body: the value comes from the BR Code, and the limit is applied as soon as it is known.

In practice this means a blacklist violation in key mode does not return 422 on the POST: the payment is accepted, the paymentId is created, and the outcome is FAILED with errorCode: policy_denied. The policy.violation and pix.out.failed webhooks report the result.

Rejection response (POST phase)

HTTP/1.1 422 Unprocessable Entity
{
"errorCode": "policy_denied",
"message": "amount R$ 5000.00 exceeds the per-transaction limit of R$ 1000.00",
"violations": [
{ "rule": "maxAmount", "message": "amount R$ 5000.00 exceeds the per-transaction limit of R$ 1000.00" }
]
}

It is not 403 (which means missing permission) nor 400 (the payload is valid): the request is correct and was rejected by a rule the tenant itself configured.

Monitor mode (NOTIFY_ONLY)

The violationAction field of the pixOut section defines what to do with violations:

ValueEffect
BLOCK (default)Rejects the payment
NOTIFY_ONLYRecords the violation and sends the policy.violation webhook, but the payment goes through

NOTIFY_ONLY is the rehearsal before blocking: save the rule in monitor mode, watch the warnings for a few days in the panel and in the webhook, and promote it to BLOCK once you are convinced the rule does not catch legitimate traffic.

In monitor mode the violation does not appear in the transaction timeline — the payment went through and will have its normal outcome (pix_out.completed). The violation lives in the webhook and in the panel.

PIX In Rules

An incoming PIX cannot be refused

The settlement bank credits the account and only then tells us. By the time the policy is evaluated, the money is already in — there is no moment at which we could have rejected it. That is why this section has no kill switch, and the choice is about what happens next: notify, or send it back.

OrderRuleruleDescription
1WhitelistPayer document on the whitelist: accepts and skips every rule below
2Blacklist (document)pixIn.cpfCnpjBlacklistPayer document is blacklisted
3Same OwnershippixIn.sameOwnershipOnlyOnly accepts PIX from the account holder's own CPF/CNPJ
4Per-transaction LimitpixIn.maxAmountMaximum amount received per transaction
5Person TypepixIn.allowedPersonTypesRestricts the payer to PF or PJ
6Allowed BankspixIn.allowedBankCodesOnly accepts PIX from the listed banks
7Blocked BankspixIn.blacklistedBankCodesRejects PIX from the listed banks

Bank lists accept either a COMPE code (3 digits) or an ISPB (8 digits) — whichever you have at hand. When the settlement webhook does not identify the payer's bank, the bank rules are skipped rather than reported as a violation. The same goes for document rules when the payer arrives without a CPF/CNPJ.

PIX In actions

violationActionEffect
ALLOW_AND_NOTIFY (default)Records the violation and sends policy.violation. The credit stands
AUTO_REFUNDBesides notifying, sends the full amount back to the payer

AUTO_REFUND starts a regular PIX refund right after the credit, with reason unauthorized-transaction. It shows up in your customer's statement as a return and cannot be undone — start with ALLOW_AND_NOTIFY to see which rules would fire before sending real money back.

The pix.in.completed webhook is delivered in both cases, and before policy.violation: the credit happened, and hiding it would create a mismatch between your balance and ours. When there is a refund, it produces the usual PIX out events (pix.out.completed or pix.out.failed).

QR Code Rules

Evaluated when a QR code is created (static and dynamic), before the QR exists at the partner.

RuleruleDescription
Static creationqrCode.canCreateStaticfalse rejects static QR creation
Dynamic creationqrCode.canCreateDynamicfalse rejects dynamic QR creation
Minimum amountqrCode.minAmountMinimum QR amount
Maximum amountqrCode.maxAmountMaximum QR amount

Amount limits only apply to a QR created with a value. An open static QR — where the payer types the amount in their own bank app — has nothing to compare at creation time, so both amount rules are skipped.

PIX Key Rules

Evaluated when a key is created (POST /v1/accounts/{accountId}/pix/keys).

RuleruleDescription
Creationkeys.canCreatefalse rejects the creation of new keys
Max keyskeys.maxKeysRejects when the account already holds that many keys

The key count is fetched from the partner at creation time. If that call fails, the creation goes through: an outage on our side should not look like a policy violation. Keys that already exist are never removed by a policy change.

Refund Rules

Evaluated on refunds you request via POST /v1/accounts/{accountId}/pix/out/refund, whether full or partial.

RuleruleDescription
Kill switchrefund.disabledenabled: false rejects every refund requested through the API
Maximum amountrefund.maxAmountMaximum amount per refund

Refunds that are not your choice stay out of this section: an accepted MED and the AUTO_REFUND of the PIX In section are obligations, and a tenant rule does not override an obligation. For the same reason, PIX Out document rules (blacklist, whitelist, ownership, person type) do not apply to refunds — the recipient of a refund is whoever paid, not a choice of yours.

Rejection in the three sections above

QR Code, keys, and refunds always block: there is no monitor mode. All three are synchronous operations that have not moved money yet when the rule is evaluated, so the rejection fits in the response — 422 with errorCode: policy_denied, in the same shape as PIX Out. Notifying after handing back a BR Code, publishing a key in DICT, or submitting a refund would be a warning about something already done.

Operating Hours

Window during which outgoing transfers are allowed. Supports windows crossing midnight (e.g. 22:00-06:00).

FieldDescriptionExample
startStart time06:00
endEnd time22:00
timezoneTime zone (defaults to America/Sao_Paulo)America/Sao_Paulo

A half-configured window (only start or only end) is treated as absent and blocks nobody.

DICT Lookup Limits

Looking up a PIX key has its own limits, evaluated on every lookup. When exceeded, the response is 429 with errorCode: dict_lookup_limit_exceeded, and a policy.violation webhook with phase: "dict_lookup" is sent.

Not every 429 is your quota

The same route also answers 429 with errorCode: partner_rate_limited, which is the banking provider throttling CorpX's traffic and has nothing to do with the limits on this page — no counter of yours has been exhausted. Check the errorCode before concluding that your quota ran out.

Both ways of asking DICT who owns a key count as a lookup:

  • The explicit lookupGET /v1/accounts/{accountId}/pix/key/{pixKey}.
  • PIX Out in KEY mode (rolling out) — POST /v1/accounts/{accountId}/pix/out, /async and /bigpix. The key has to be resolved before the money leaves, and that question goes to the same DICT. See "Paying by key also consumes quota" below.

There are three absolute-count limits, all measured per account (see "Every limit is per account" below), and the most restrictive one wins — any of them going over returns 429:

LimitWindowGlobal defaultWhat it protects
maxLookupsPerDayBRT calendar day (from 00:00)50Absolute cap of lookups per day
maxLookupsPerMinuterolling 60s15Burst: fits the daily cap, but arrives all at once
maxNotFoundPer5minrolling 5 min10Key enumeration: blind scanning produces lots of NOT_FOUND well before the daily cap is hit

On top of those there are the per-time-window rate caps described below. Those always apply: an account that declares no numbers of its own inherits the fleet baseline.

maxLookupRatio has been discontinued

There used to be a fourth limit, maxLookupRatio — lookups allowed per transfer completed in the last 24h. It was removed in v2.53.0. It was the same calculation as the 24h window's usage rate, at worse resolution: a single window, with no sample floor of its own, relying on a global floor of 50 lookups to avoid locking out an account with no transfers. The windows do the same thing across twelve ranges. The field is still accepted in the policy body and ignored; the ratio denial reason no longer appears in 429 responses.

Rate caps per time window

The three limits above answer how much and how fast. None of them answers the question the DICT bucket actually measures: how many lookups you spend per payment you complete, and what fraction of your lookups resolves no key at all. An account can sit comfortably inside 500 lookups a day and still spend 40 lookups per PIX — the profile of someone scanning keys, not someone paying.

Each time window can cap both rates:

FieldWhat it measures
maxUsageRatioLookups ÷ completed PIX Out in the window
maxFailureRatio(NOT_FOUND + refused) ÷ lookups in the window. A fraction, not a percentage: 0.4 = 40%
minLookupsSample floor. Below that many lookups in the window, neither rule can refuse

Available windows are 5m, 15m, 30m, 1h, 3h, 6h, 12h, 24h, 3d, 7d, 14d and 30d.

{
"dictLookup": {
"windows": {
"5m": { "maxFailureRatio": 0.6, "minLookups": 10 },
"1h": { "maxUsageRatio": 6, "maxFailureRatio": 0.4, "minLookups": 50 },
"30d": { "maxUsageRatio": 4, "minLookups": 500 }
}
}
}

minLookups exists so the rate does not lock out whoever has barely started: an account with two lookups and no payment has an infinite usage rate and, without the floor, would stay blocked forever.

A window with no cap declared is not a window turned off. It inherits the fleet baseline, which today is maxUsageRatio 2.2 and maxFailureRatio 0.3 across every range, with a sample floor of 20 for windows up to 30 minutes, 50 from 1h to 12h, and 500 per day of window from 24h on (500 at 24h, 1500 at 3d, and so on). Those numbers came out of measuring 14 days of real traffic and blocked no account in that history. Declaring your own values specialises the account or tenant; it is not what turns the protection on.

When more than one window is over its cap, the response cites the shortest one — it is the immediate cause and the first to clear. The 429 message says which window, what was measured and what the cap was:

DICT lookup usage too high for this account in the last 1h:
120 lookups for 10 successful transfers (12.0 per transfer); max 6.0
DICT lookup failure rate too high for this account in the last 5m:
18 of 24 lookups did not resolve a key (75%); max 40%

The errorCode is still dict_lookup_limit_exceeded — nothing changes for anyone already handling that code.

Every limit is per account

All four limits are measured account by account, always. There is no aggregate tenant cap: one account's lookups never enter another's count, even when both belong to the same tenant.

That holds even when the limit was configured at the tenant level. A tenant policy is a template applied to each account individually, not a shared bucket:

ConfigurationCorrect readingWrong reading
Tenant policy with maxLookupsPerDay: 50Each account under the tenant may do 50 lookups per dayThe whole tenant may do 50 per day, split across its accounts
Tenant policy with windows.24h.maxUsageRatio: 2.2Each account's rate is measured against that account's transfersThe rate is measured against the tenant's total transfers

An account policy still wins over the tenant policy when present — what differs between the two is not the counting unit, only where the numbers come from.

The account is the unit because it is what BACEN meters in the DICT rate-limit bucket: that is where the burst happens and where the penalty lands. Summing a tenant's accounts produced both errors at once — an idle account inherited its neighbour's consumption, and in a large tenant one account's excess was diluted into the total until no limit ever closed.

Paying by key also consumes quota

Rolling out

This section describes behaviour that is coming. Today paying by key does not consume quota yet and will not return 429 for this reason. We will announce it well before turning it on.

A PIX Out in KEY mode has to know who owns the key before sending the money. That resolution draws on the same quota as the explicit lookup, so a payment can come back 429 with errorCode: dict_lookup_limit_exceeded. When that happens no payment is created: there is no paymentId, no transaction, and retrying with the same Idempotency-Key remains safe.

The 24h cache is what prevents double counting. The most common flow — look the key up to show the recipient, then pay once the user confirms — spends one lookup, not two: the second is served from cache. Paying directly, without looking up first, also spends one. Either way, 100 payments by key consume roughly 100 of the quota, leaving the rest of the budget for standalone lookups.

BigPix resolves the key once for the whole batch, no matter how many instalments the amount is split into.

Two outcomes are worth noting on the payment POST:

  • An invalid or non-existent key returns the error in the response, without creating a payment. That beats a payment that is born and fails seconds later.
  • DICT unavailable (settlement partner error or timeout) refuses nothing: the payment proceeds and the key is resolved by the settlement partner, as before. Downtime on our side or theirs turns into neither a refused payment nor consumed quota.

The denominator of the usage rate

A window's usage rate is lookups ÷ transfers completed in the same window. Only transfers that actually happened go into the denominator, and only that account's: COMPLETED and REFUNDED (which settled and were only returned afterwards). Payments that failed, were rejected or ended up undetermined do not widen the cap — otherwise anyone stuck retrying a failing attempt would earn more lookup quota with every failure.

A new or idle account has a denominator of zero, which would make the rate infinite. What protects that case is the window's minLookups: below the sample floor neither rule can refuse, so the account makes its first lookups without being blocked by a rate that does not mean anything yet.

Up to v2.52.0 that job belonged to a global floor of 50 lookups/24h tied to maxLookupRatio. It was removed along with the limit; the message (bootstrap floor of 50 lookups/24h applied) no longer appears.

Blank is not unlimited

A limit left blank in the policy does not mean unlimited: the global default from the table above applies. Removing a cap has to be explicit (the No limit option in the Backoffice, which stores a negative value). Burst limits stay on either way — removing the daily cap does not license bursts.

What consumes quota

The daily quota (maxLookupsPerDay) and the ratio count the lookups that reached DICT because the integrator asked for them: key found, key non-existent, and key refused by DICT (invalid request, or DICT's own limit for that requester). Technical failures on the settlement partner's side and attempts already refused with 429 are left out — you should not burn quota on our downtime, nor dig yourself deeper after being blocked. The same applies to the explicit lookup and to the resolution done inside a PIX Out.

The burst limits are the opposite: maxLookupsPerMinute counts every outcome, refused ones included, because what it measures is the pressure being applied, not the value obtained. maxNotFoundPer5min counts only non-existent keys.

24h cache

Repeated lookups of the same key are served from a 24h cache, and a cache hit does not consume quota. The cache is shared by both paths: an explicit lookup serves the next payment, and the resolution done inside a payment serves the next lookup. To force a round trip to DICT — stale data, ownership that may have changed — use ?noCache=true on the explicit lookup, keeping in mind that the lookup then counts normally.

policy.violation Webhook

Every violation — rejected or merely reported — fires the policy.violation event to whoever subscribed to it.

{
"phase": "edge",
"action": "BLOCK",
"blocked": true,
"accountId": "acc_...",
"identifier": "ORDER-1234",
"mode": "KEY",
"amount": 5000.00,
"violations": [
{ "rule": "maxAmount", "message": "amount R$ 5000.00 exceeds the per-transaction limit of R$ 1000.00" }
],
"counterparty": { "document": "12345678900", "name": "JOHN DOE" }
}
FieldDescription
phaseedge (at request time), counterparty (after resolving the recipient), pix_in (after the credit), or dict_lookup
actionBLOCK, NOTIFY_ONLY, ALLOW_AND_NOTIFY, or AUTO_REFUND, as configured
blockedtrue when the operation was actually rejected. Always false for pix_in
violationsEvery rule violated, with the identifier in rule
counterpartyThe recipient (PIX Out) or the payer (PIX In), when known

In phase: "edge" there is no paymentId: the gate runs before the payment is created, which is exactly why the rejection fits in the HTTP response. Use identifier to correlate. In the counterparty phase, which runs inside the workflow, paymentId already exists and comes in the payload.

For phase: "pix_in" the payload swaps paymentId for transactionId and endToEnd, and carries refunded telling whether the amount was sent back.

Delivery, retries, and signature details are in the Webhooks guide.

Violation Visibility

  • policy.violation webhook — on every violation, in any section.
  • Transaction timeline (GET /v1/transactions/{id}/timeline) — when the violation rejects a PIX Out, the pix_out.failed event carries errorCode: policy_denied and the violated rules in policyViolations. For a rejection at the edge, where no paymentId exists yet, the event is anchored on the operation's deterministic workflowId, which is the same id returned in PIX out responses. The endpoint accepts both, plus endToEndId, txid, and the partner id.
  • Panel — the Violations tab lists the history, separating what was blocked, what was only reported, and what was refunded, with totals per rule.
The statement does not show violations

An operation rejected by policy never reaches the partner bank, and the statement (GET /v1/accounts/{accountId}/statement) reflects what happened in the account. Violations do not show up there. Use the webhook, the timeline, or the panel. The exception is a PIX In refunded by AUTO_REFUND: that is a real movement and appears in the statement as a return.

IP allowlist and MED

The IP allowlist that actually blocks traffic is the WAF one, configured on the Security screen — it is not a policy. MED controls have their own guide in Disputes (MED).