DENY or REVIEW and someone is asking 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 its audit event can be checked 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
POST /v1/validations answers with the full decision. A new request answers 201; repeating a requestId answers 200 with the decision Tracer already recorded for that key.
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.
reason carries the same text on REVIEW decisions — 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 because Tracer stops before the limit check — not because no limit applies to the account.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?”. A rule absent from evaluatedRuleIds was not evaluated for this transaction, so start with its status and its scope rather than its expression.
Step 4: Name the spending limit
On a
limit_exceeded denial, limitUsageDetails holds one entry per limit Tracer checked, and the entries marked "exceeded": true are the ones the amount would push past:
attemptedAmount against limitAmount, with currentUsage reporting what that limit’s current period and matched scope would hold if this transaction were allowed. 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.
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 as they were when the decision was made, so 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
Every decision is stored under its
validationId:
transactionType, amount, currency, 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. See Retrieve a validation.
This is the record to read out in a dispute: it is the input and the outcome in one document, and 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 stopped
validationId, decision, reason, amount, currency, transactionType, accountId, matchedRuleIds, exceededLimitIds, 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 the decision under the
validationId as the event’s resourceId:
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 every record from the first one through the one you named still matches the hash stored with it. Each also links to the hash of the record before it, so within that span no record was removed, re-ordered, or re-dated. totalChecked reports how many records 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.

