Skip to main content
A compliance officer hands over a sentence: “decline card-not-present purchases above BRL 5,000 from a device we have not seen before.” This guide turns that sentence into a rule that evaluates the way the policy reads, rehearses it where it reaches nobody, and puts it live. What changes in your operation: the policy stops living in a ticket and starts living in an endpoint. The person who wrote the sentence can read the rule back, the rehearsal runs through real evaluation instead of a spreadsheet, and each change to the rule leaves an audit event behind it.
Who is this guide for? Risk and fraud analysts authoring rules, and the developers who wire the policy fields into the validation request. Steps 1 and 2 are about the policy; steps 3 to 8 are API calls.

Before you start


  • Tracer running and reachable, with an API key — see Getting started
  • The variables and the scope model — see Rules engine
  • A test account id you can send validations for, that no customer traffic uses
  • The policy sentence, written down, with whoever wrote it available for one question
All calls below send the API key as X-API-Key.

Step 1: Map the sentence onto fields


A rule reads what the validation request carries. Take the sentence apart clause by clause and put each one in the column it belongs to. The first two are fields Tracer defines. The last two are fields your integration has to send — Tracer has no opinion about what “card-not-present” or “new device” mean. That is the question to take back to whoever wrote the policy: which flag in our payload says the device is new?
subType reaches expressions in lower case, so "card_not_present" is the form to compare against. If your integration already uses subType for something else, carry the entry mode in metadata instead and match on that — both work the same way in an expression.
For the full variable list and the fields each context map carries, see Rules engine.

Step 2: Split the rule between scope and expression


Two things in that table — the transaction type and the account — are things Tracer can filter on before an expression runs. Those belong in the rule’s scopes. The value comparisons belong in the expression. Scope, which decides whether the rule is considered at all:
Expression, which decides whether the rule fires:
Read the expression against the sentence: entry mode, threshold, and the device flag. amount > 5000 is strictly above, so a transaction of exactly 5000.00 does not fire it — check that against the policy before you go further, because “above” and “from” are different rules. A rule that reads a metadata key the request does not carry does not match, and the other rules still run. So the expression above needs no presence test for deviceFirstSeen; see Rules engine.
Do not repeat scope conditions inside the expression. transactionType == "CARD" in both places is not wrong, but it leaves two places to edit when the policy changes — and the expression is the one that needs a lifecycle round-trip to edit.

Step 3: Create the rule as a draft


POST /v1/rules creates the rule in DRAFT. A draft is not evaluated, so nothing you do here reaches traffic.
The accountId in that scope is your test account. It is what keeps Step 4 off customer traffic; Step 5 takes it out. A 201 answers with the stored rule:
Tracer stores the rule name in a normalized form, so the name it returns can differ from the string you sent. Take the ruleId from the response — that is the handle every call below uses. See Create a rule. The expression is compiled on this call, so an expression that cannot run never becomes a draft. A syntax error answers 0340, an expression that does not return a boolean answers 0341, and one whose estimated cost is above CEL_COST_LIMIT answers 0342.

Step 4: Rehearse it on an account nobody else uses


The scope you set is what keeps the rehearsal contained: the rule is considered only for transactions on that one test account, so activating it puts it in front of exactly the traffic you send it.
1

Activate the scoped rule

The response comes back with status: "ACTIVE" and an activatedAt. See Activate a rule.
2

Send a transaction the policy should decline

See Validate a transaction.
3

Read the decision

Your ruleId in matchedRuleIds is the rehearsal passing.
4

Send the cases that should not fire

Change one value at a time and repeat the call with a fresh requestId: "amount": "5000.00" for the boundary, "deviceFirstSeen": false for a known device, "subType": "purchase" for a card-present sale. Each should come back without your ruleId in matchedRuleIds.
Send a new requestId for every attempt. requestId is the idempotency key: repeat one and Tracer answers 200 with the decision it already recorded for that key, so the change you just made will look like it did nothing.
A rehearsal is a real validation. It stores a decision record and writes an audit event, and an ALLOW decision consumes the spending limits that cover that account. That is why the test account matters.
If your ruleId is not in matchedRuleIds, work through it in this order: is the rule ACTIVE (GET /v1/rules/{id}), does the transaction match the scope you set, and do the values you sent satisfy the expression.

Step 5: Put it live


Going live means one edit: drop the test account from the scope so the rule applies to the population the policy names.
1

Stop evaluating the rehearsal version

A scope edit does not require INACTIVE. Deactivating first makes the switch take effect at a moment you control and records a visible gap in the audit trail. Each instance serves rules from a cache it refreshes on a poll (every 10 seconds by default, RULE_SYNC_POLL_INTERVAL_SECONDS), so allow that window for the deactivation to reach every instance; GET /v1/rules confirms the stored status, not that every instance has caught up.
Status goes to INACTIVE. See Deactivate a rule.
2

Replace the scope

scopes replaces the whole array — send every scope object you want the rule to keep. See Update a rule.
3

Activate

Activation reaches the instance that served this call as soon as it commits. When you run several instances behind a load balancer, the others pick the change up on their next rule sync (RULE_SYNC_POLL_INTERVAL_SECONDS, default 10). Deactivation travels the same way. Allow that same window after activation before you treat the rule as enforcing on every instance.
4

Confirm what is live

The listing answers “what is enforcing on card traffic right now” — see List rules. For one rule, GET /v1/rules/{id} returns the expression and scopes as stored (Retrieve a rule). Both report the stored state, not what each instance’s cache holds.
A validation loads its rule set once, from the cache of the instance that serves it, when the call starts. A rule that activates while a validation is in flight is not part of that decision, and decisions already recorded do not change when rules change afterwards.

Step 6: Know where your rule sits among the others


Rules carry no priority field and no ordering to configure. Rules whose scope matches a transaction are evaluated together, and the decision comes from the strictest action that fired: a DENY rule first, then an exceeded spending limit, then REVIEW, then ALLOW, then the configured no-match default. matchedRuleIds carries every rule that matched, whatever action each one holds. Two consequences for the rule you just wrote:
  • An ALLOW rule does not exempt anyone from a DENY rule. If the policy has an exception — VIP customers, a partner merchant — the exception belongs inside the DENY expression, as one more condition that makes it narrower:
    Note what that costs: the narrowed rule now reads metadata.customerTier, and a request that does not carry that key does not match it.
  • Your rule joins the set every matching transaction evaluates. MAX_RULES_PER_REQUEST caps how many rules one validation evaluates; when the set is larger, the response reports truncated: true — see environment variables.
The precedence table and the reasoning behind it are on the Rules engine page.

Step 7: Change the rule when the policy changes


What you do depends on the field, not on how the rule is doing.
The expression accepts an edit only while the rule is DRAFT. Sending one to a rule in another status answers 422 with error code 0351 — deactivating is not enough on its own, because INACTIVE is not DRAFT. POST /v1/rules/{id}/draft is the step people miss; see Draft a rule.
A PATCH is stored when it answers, and reaches evaluation on the next rule sync. When the change matters to the minute, deactivate first and activate again after — that sequence also puts a visible gap in the audit trail where the rule was not enforcing, which is what a reviewer will look for. The lifecycle is a closed set of moves: DRAFT activates or is deleted; ACTIVE deactivates; INACTIVE goes back to DRAFT, back to ACTIVE, or is deleted; DELETED is the end. Anything else answers 422 with error code 0349 — including a request to draft a rule that is still ACTIVE.

Step 8: Retire or delete the rule


To stop enforcing without losing anything, deactivate. The rule keeps its expression, its scopes, and its history, stops being evaluated, and POST /v1/rules/{id}/activate brings it back. That is the move for a policy that is suspended, seasonal, or under review. To remove it, delete — and only after deactivating, because a rule in ACTIVE cannot be deleted:
A 204 answers on success. See Delete a rule. What deleting removes:
  • The rule stops answering on GET /v1/rules/{id}, which returns 404 with error code 0347 afterwards.
  • It no longer appears in GET /v1/rules, and DELETED is not a value the status filter accepts.
  • DELETED is the end of the lifecycle. No endpoint moves a rule out of it — a deleted rule comes back only as a new rule you create again.
What deleting leaves behind:
  • The audit trail keeps the rule’s lifecycle, and the RULE_DELETED event carries the definition — name, description, expression, action, scopes — as it stood at deletion. See Audit and compliance.
  • Decisions the rule produced keep its ruleId in matchedRuleIds. A denial from six months ago still names it — see Reviewing a denied transaction.
  • The name becomes available again for a new rule in the same context.
Deactivate, then read the audit trail, then delete. Deactivating is reversible in one call and deleting is not reversible at all, so there is no reason to skip the intermediate state.

Common pitfalls


What usually goes wrong turning a policy into a rule:
  • “The rule is ACTIVE but nothing matches.” Check the field the policy leans on hardest. A rule reading metadata.deviceFirstSeen matches nothing if your integration never sends that key — the rule is correct and the payload is incomplete.
  • “It fired on a transaction the policy exempts.” An ALLOW rule does not override a DENY. Put the exemption inside the DENY expression (Step 6).
  • “My second rehearsal returned the first decision.” requestId is the idempotency key. Send a new UUID per attempt.
  • “PATCH rejected my expression with 422.” The rule was not in DRAFT. Move it there first (Step 7).
  • “The name I sent is not the name I get back.” Tracer stores names in a normalized form. Reference the rule by ruleId.

Error codes

The complete list is in the Tracer error list.

Quick reference