DENY or REVIEW and someone asks why. This guide takes you from the decision Tracer returned to a named rule or a named spending limit. From there it reaches the stored record and the entry you can hand an auditor months later.
What changes in your operation: the answer to “why was this blocked?” stops being a log search. Each decision arrives with the identifiers of what produced it. The same record answers by id years later, and you can check its audit event against the hash chain.
Before you start
- Tracer running and reachable, with an API key. See Getting started
- A
DENYorREVIEWresponse to work from, or thevalidationIdof one - Familiarity with what rules and limits do. See the Rules engine and Spending limits
X-API-Key.
Step 1: Read the decision Tracer returned
A decision reaches you in one of two ways. A transaction that the Midaz ledger reserved has a validation record with its
transactionId: find it with GET /v1/validations?transaction_id={transactionId} (Step 6), then retrieve it with Step 5. The record carries the same decision fields as the response below.
POST /v1/validations answers with the full decision. A new request answers 201. A repeated requestId with the same body answers 200 with the decision Tracer already recorded for that key. POST /v1/validations/rules-only answers the same way, and checks no limit.
For the full request and response schema, see Validate a transaction.
Step 2: Tell a rule denial from a limit denial
Read
reason first. It names what produced the decision, and it tells you which of the next two steps to take.
On a
REVIEW from a matched rule, reason carries the same text (Rule matched with REVIEW action), and Step 3 reads a review the same way it reads a denial.
On a rule denial,
limitUsageDetails comes back empty, not because no limit applies to the account. Tracer checks no limit once a rule decides DENY or REVIEW, and a rules-only validation checks no limit.Step 3: Name the rule
matchedRuleIds lists every rule that matched, whatever action each one carries. A DENY rule and an ALLOW rule can both appear on the same denial. Retrieve each one and read its action to find the rule behind the decision:
name, description, expression, action, and scopes a colleague needs to see why it fired. See Retrieve a rule.
Compare matchedRuleIds with evaluatedRuleIds when the question is the opposite one: “why did my rule not fire?”. For a rule absent from both lists, start with its status and its scope rather than its expression.
Step 4: Name the spending limit
A
limit_exceeded denial comes from POST /v1/validations or from a reserve of the Midaz ledger. The response, and the validation record of a reserve, hold limitUsageDetails, with one entry per limit counter Tracer checked, and the entries marked "exceeded": true are the ones the amount would push past:
attemptedAmount against limitAmount. The currentUsage field reports what that limit’s current period and matched scope would hold with this transaction included. It also counts the capacity that unconfirmed Midaz ledger reservations hold. In the entry above, a 1500 purchase would take a 50000 daily cap to 51500.
A PER_TRANSACTION limit keeps no count, so its entry reports currentUsage as 0 and attemptedAmount against limitAmount is the whole comparison.
When a reserve has more than one source account, the record also carries sources: for each source, the account, its amount, its decision, its matched rules and the limits it exceeded. A limit that covers several sources has one entry for each counter, with the same limitId. To see the holds that a reserve placed, call GET /v1/validations/{id}/reservations. Each hold reports its state: RESERVED, CONFIRMED, RELEASED or EXPIRED. See List the holds of a validation’s transaction.
GET /v1/limits/{limitId} gives the limit’s current name and configuration. See Retrieve a limit. The decision record keeps the ceiling, period, and scope that applied when Tracer made the decision. A limit changed since then does not change what the record says.
For how each period counts and how consumption accumulates, see Spending limits.
Step 5: Retrieve the record later
Tracer stores every decision under its
validationId:
scheme, transactionType, amount, asset, transactionTimestamp, account, and the optional segment, portfolio, merchant, and metadata), plus the same decision, reason, matchedRuleIds, evaluatedRuleIds, and limitUsageDetails the original response carried, and a createdAt. The record of a reserve from the Midaz ledger also carries its transactionId, organizationId, and ledgerId. See Retrieve a validation.
Read out this record in a dispute. It holds the input and the outcome in one document. It does not change when rules or limits change afterwards.
Step 6: Find records when you do not have the id
GET /v1/validations lists stored decisions, newest first, with cursor pagination:
matched_rule_id={ruleId}: every stored decision this rule matchedexceeded_limit_id={limitId}: every stored decision this limit stoppedtransaction_id={transactionId}: the decision on one Midaz ledger transaction. A value that is not a UUID answers400
validationId, decision, reason, amount, asset, scheme, transactionType, accountId, segmentId, portfolioId, transactionId, organizationId, ledgerId, matchedRuleIds, exceededLimitIds, sources, processingTimeMs, and createdAt. Take the validationId of the one you want and retrieve it with Step 5 for the full record. See List validations for every filter and the pagination fields.
Step 7: Pull the audit event behind the decision
The audit trail records a decision of
POST /v1/validations or POST /v1/validations/rules-only under the validationId as the event’s resourceId:
A decision from a reserve of the Midaz ledger has no
TRANSACTION_VALIDATED event. Its evidence is the validation record from Step 5, and the limits it held appear as RESERVATION_RESERVED events.
The same endpoint carries the lifecycle of rules and limits: who created, activated, or deleted them. See List audit events for the filters, and Audit and compliance for the event types and retention periods.
Step 8: Verify the audit event in an audit
Pass the
eventId to the verification endpoint:
isValid: true establishes that the record you named and its links in the hash chain still match the hashes stored with them. totalChecked reports how many links the check covered.
On a failed check, isValid is false, message reports tampering, and firstInvalidId carries an internal sequence number for the diverging record. That number is not an audit-event id, so it is not a value to pass to GET /v1/audit-events/{id}.
See Verify an audit event.
Hand over the two together: the retrieved event from Step 7 is the content of the decision, and the verification result is the evidence that the chain holding it is intact. The verification call reports on the chain. It does not return the record.
Common pitfalls
Error codes
The complete list is in the Tracer error list.

