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
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.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:
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.
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.
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:
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
status: "ACTIVE" and an activatedAt. See Activate a rule.2
Send a transaction the policy should decline
3
Read the decision
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.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.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 Status goes to
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.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
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
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
ALLOWrule does not exempt anyone from aDENYrule. If the policy has an exception — VIP customers, a partner merchant — the exception belongs inside theDENYexpression, as one more condition that makes it narrower:Note what that costs: the narrowed rule now readsmetadata.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_REQUESTcaps how many rules one validation evaluates; when the set is larger, the response reportstruncated: true— see environment variables.
Step 7: Change the rule when the policy changes
What you do depends on the field, not on how the rule is doing.
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:
204 answers on success. See Delete a rule.
What deleting removes:
- The rule stops answering on
GET /v1/rules/{id}, which returns404with error code0347afterwards. - It no longer appears in
GET /v1/rules, andDELETEDis not a value thestatusfilter accepts. DELETEDis 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.
- The audit trail keeps the rule’s lifecycle, and the
RULE_DELETEDevent carries the definition — name, description, expression, action, scopes — as it stood at deletion. See Audit and compliance. - Decisions the rule produced keep its
ruleIdinmatchedRuleIds. 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.
Common pitfalls
Error codes
The complete list is in the Tracer error list.

