transaction_state_changed tells your backend that the outcome of a
transaction you sent to POST /v2/transactions has
changed — the rules put it on review or declined it, an analyst sent it to
review, or a label was set or removed. Use it to
hold a bonus while a claim is under review and to withhold or release it when
the outcome is settled, without polling.
The event is delivered by the same webhooks as every other event — see webhooks.md for setup, signatures, retries and limits. Transactions themselves exist only on the Business plan or higher, so the event does too.
| What happened | Event |
|---|---|
POST /v2/transactions decided REVIEW or DECLINE | yes |
POST /v2/transactions decided APPROVE | no |
an analyst sent the transaction to review (REVIEW) in the dashboard | yes |
a label was set — in the dashboard or with PUT /v2/transaction-labels | yes |
| a label was replaced with a different label — even one of the same polarity (the state stays the same, the label changed) | yes |
a label was removed — in the dashboard or with DELETE /v2/transaction-labels | yes |
an analyst sent a labeled transaction to review (REVIEW) in the dashboard | yes, two events: the label removal, then the REVIEW change |
| the same label was set again | no |
REVIEW was set over REVIEW on a transaction without a label | no |
Add transaction_state_changed to the event types of a webhook in the
dashboard (Settings → Webhooks). The wildcard * includes it.
The event is delivered only to webhooks on payload version 2 or 3. Version 3
carries the same fields as version 2, with every key in snake_case; it is the
default for new webhooks. A webhook on version 1 does not receive this event at
all: schema 1 is frozen and has no body for it — switch the webhook to version 2
or 3 on its card in the dashboard.
A webhook bound to an environment receives these events only for that environment. Events from calls made with a key that has no environment go only to webhooks not bound to an environment.
Change: a webhook bound to an environment
no longer receives transaction_state_changed for other environments or for
calls made with a key that has no environment. A webhook not bound to an
environment receives every event, as before.
Version 2 (examples/transaction-state-changed.json):
the rules decided REVIEW, then an analyst labeled the transaction as bonus
abuse through the API.
{ "version": 2, "event": "transaction_state_changed", "eventId": "txn-state:7d1c2b3a-0000-4000-8000-000000000042:118", "timestamp": "2026-10-15T10:04:00Z", "transaction": { "id": "7d1c2b3a-0000-4000-8000-000000000042", "transaction_id": "bonus-claim-1842", "state": "REVIEW", "fraud_score": 50, "applied_rules": [ { "id": "B02", "name": "same_campaign_same_ring", "operation": "+", "score": 40 }, { "id": "B05", "name": "visit_guidance", "operation": "+", "score": 10 } ], "effective_state": "DECLINE", "recommendations": [ "withhold_bonus" ], "evidence": [ { "rule_id": "B02", "code": "same_campaign_same_ring", "score": 40, "message": "Another account of the same fraud ring already took this campaign." }, { "rule_id": "B05", "code": "visit_guidance", "score": 10, "message": "The visit verdict is deny, review or challenge." } ], "label": "fraud_detection_bonus_abuse", "label_source": "api", "label_at": "2026-10-15T10:04:00Z", "previous_effective_state": "REVIEW" }}The same event in version 3 is
examples/transaction-state-changed-v3.json:
"version": 3 and event_id instead of eventId; the transaction block is
identical.
Envelope:
| Key | Meaning |
|---|---|
version | 2 or 3 — the payload version of your webhook |
event | always transaction_state_changed |
eventId (event_id in version 3) | the event id and the idempotency key; equals the X-Tracio-Event-Id header — see Idempotency |
timestamp | when the change happened (UTC, RFC 3339) |
The transaction block is the object returned by
GET /v2/transactions/{transaction_id}
without calculation_time and idempotent_replay, plus one key:
| Key | Meaning |
|---|---|
id, transaction_id, state, fraud_score, applied_rules, partial, partial_sources | as in the POST /v2/transactions response; state is always the decision of the rules |
effective_state | the outcome to act on: the analyst's state or the label's state when one is set, otherwise state |
recommendations | what to do with the bonus, from effective_state |
evidence | up to 5 reasons behind the decision of the rules |
label, label_source, label_at | the current label, where it was set (api or dashboard) and when; all three are null when the transaction has no label |
previous_effective_state | effective_state before this change; null for the event sent on the automatic decision of POST /v2/transactions. When a labeled transaction is sent to review, the label removal comes first, so the second event's previous_effective_state is the decision of the rules — it can be REVIEW with effective_state also REVIEW |
The block shows the transaction as it is when the event is sent, not as it
was at the moment of the change. If two changes follow each other quickly, both
events carry the latest state of the transaction, and each carries its own
previous_effective_state and timestamp.
Order is not guaranteed: sort by timestamp; the current state is
GET /v2/transactions/{transaction_id}. Events are sent in batches, possibly
by more than one sender, and a retried delivery can arrive after a later one.
Events are written together with the decision and the labels and are sent by a background relay: expect one up to about 5 seconds after the change. Delivery is at least once, with the retries described in webhooks.md — the same event can reach you more than once.
eventId equals the X-Tracio-Event-Id header and is stable across retries.
Its form is txn-state:<id>:created for the event sent on the automatic
decision and txn-state:<id>:<n> for every later change, where <id> is the
id of the transaction. Store the ids you have processed and skip a repeat.
Signatures are the same as for every other event: see
webhooks.md, "Verifying the signature",
and check your implementation against
examples/signature-vector.json.
The event comes with the headers of schema 2 (see
webhooks.md, "Headers"), with
X-Tracio-Event-Type: transaction_state_changed. There is no
X-Tracio-Request-Id: the event belongs to a transaction, not to a visit.
The event does not tell you who set a label or changed the state — only where
the label came from (label_source: api or dashboard).