Fraud Detection for Autonomous Transactions: Signals, Scores, and Holds
Fraud controls for agent-initiated payments: Radar signals and risk scores, rules and review queues, the authorize-then-capture hold, idempotent retries, 3-D Secure step-ups, and dispute feedback.
Published on • October 4, 2026
AI Assistant

Fraud Detection for Autonomous Transactions: Signals, Scores, and Holds
When a human checks out there is a hesitation point: a pause before clicking Pay, a phone call when something looks wrong. Autonomous transactions remove it. An agent can create hundreds of charges in the time a person spends reading an invoice, with a consistency that fraud models and chargeback analysts find suspicious. If your payment flow is driven by software, your fraud controls must be too — signals in, decision out, and a reversible step between authorizing money and taking it.
Stripe gives you three layers: Radar’s real-time risk evaluation, the PaymentIntents hold pattern with idempotent retries, and disputes that feed chargeback outcomes back into the loop.
Key technologies: Stripe Radar, PaymentIntent (capture_method, status, amount_capturable, amount_received), Radar rules (:risk_level:, :amount_in_usd:, ::metadata::), 3-D Secure / SCA, Idempotency-Key, dispute reason codes.
Signals: What the Evaluation Sees
Radar evaluates three API objects — Charges, PaymentIntents, and SetupIntents — and takes one of four actions per object: request 3D Secure, allow, block, or review. Its models weigh hundreds of risk factors plus network data; Stripe estimates a 92% chance it has already seen a given card (82% for SEPA accounts, 71% for ACH). Account-level models, which produce :account_fraud_risk_level:, draw on KYC information, transactions, geographic risk factors, and behavior patterns.
| Signal family | Attributes and checks | Relevance to agent traffic |
|---|---|---|
| Card verification | if CVC verification fails based on risk score, if Postal code verification fails based on risk score | Failed checks do not guarantee a decline |
| Card history | :seconds_since_card_first_seen:, :is_new_card_on_customer: | Automated flows often present fresh cards |
| Geography | :card_country: vs :ip_country: | Mismatch is rule material, not automatic fraud |
| Funding and identity | :card_funding: = 'prepaid', :is_disposable_email: | Combined, they review far fewer payments than funding alone |
| Amount and method | :amount_in_usd:, :payment_method_type:, :is_off_session:, :digital_wallet: | Agent charges are typically off-session, which rules must carve out |
| Authentication outcome | is_3d_secure, is_3d_secure_authenticated, has_liability_shift | Post-hoc: known only after a 3DS attempt |
| Your metadata | ::order_id::, ::customer:trusted:: | Usable in your rules, invisible to Stripe’s fraud models |
That last row is an asymmetry: metadata “isn’t shown to customers or factored into whether or not a charge is declined or blocked by our fraud prevention system,” yet custom rules can read it. Use it for provenance — run ID, policy version — in rules and review triage, not as a model signal.
Risk Scores, Levels, and Thresholds
Each payment carries a risk_level. When available, a granular score from 0 to 99 accompanies it; by default 65 or above indicates elevated risk and 75 or above indicates high risk.
| Score band | risk_level in the Charge outcome | Default behavior |
|---|---|---|
| 75–99 | highest | Blocked; type: blocked, reason: highest_risk_level |
| 65–74 | elevated | Allowed, queued for manual review (type: manual_review) |
| Below 65 | normal | Authorized |
| — | not_assessed | Non-card, historical, or opted-out payments |
| — | unknown | Evaluation failed; Stripe is notified |
"outcome": {
"risk_level": "elevated",
"risk_score": 68,
"reason": "elevated_risk_level",
"type": "manual_review",
"seller_message": "Stripe evaluated this charge as having elevated risk, and placed it in your manual review queue."
}
risk_score appears only on certain Radar plans, so branch on risk_level first. Handle not_assessed and unknown explicitly rather than defaulting them to safe.
Rules and Reviews
Rule syntax is {action} if {attribute} {operator} {value}. Real examples from Stripe’s documentation:
Request 3DS if :risk_level: != 'normal' and :amount_in_usd: > 25
Review if :card_funding: = 'prepaid' and :is_disposable_email:
Block if :card_country: != 'US' and :risk_level: = 'elevated'
if CVC verification fails based on risk score
Rules apply only to future payments; you get up to 200 transaction rules and 100 account rules. Three details matter for autonomous systems:
- Traffic allocation. Roll out at 5%, then raise; at 0% the rule runs in shadow mode and reports what would have happened. Retries stay consistent — Radar groups attempts by invoice, then customer and amount, then card and amount — so a retry gets the original’s outcome.
- Ordering. Request-3DS rules evaluate before review, block, and allow rules; review rules evaluate after block rules. Allow rules override everything, including Stripe’s defaults, so keep them minimal and append
and :risk_level: != 'highest'. - The review queue holds only successful payments. Matches declined by the issuer never appear, so queue size is not a match count.
The Hold: Authorize Now, Capture Later
The hold pattern is capture_method=manual. Stripe’s enum has three values: automatic, automatic_async (the default), and manual, which “place[s] a hold on the funds when the customer authorizes the payment, but [doesn’t] capture the funds until later.” The bank authorizes; you capture after fulfillment checks, a human glance, or a policy pass.
import os
import stripe
stripe.api_key = os.environ["STRIPE_SECRET_KEY"]
order_key = "ord_9182" # deterministic — never model-generated entropy
intent = stripe.PaymentIntent.create(
amount=4500,
currency="usd",
capture_method="manual",
metadata={"order_id": order_key, "policy_version": "2026-10"},
idempotency_key=f"create_{order_key}",
)
stripe.PaymentIntent.confirm(intent.id)
pi = stripe.PaymentIntent.retrieve(intent.id)
assert pi.status == "requires_capture"
assert pi.amount_capturable == 4500 # authorized, not yet taken
The status enum is requires_payment_method, requires_confirmation, requires_action, processing, requires_capture, canceled, succeeded. amount_capturable is what may still be captured; amount_received is what has been collected. Capture up to that amount:
curl https://api.stripe.com/v1/payment_intents/pi_3MtwBwLkdIwHu7ix28a3tqPa/capture \
-u "$STRIPE_SECRET_KEY:" \
-H "Idempotency-Key: capture_ord_9182" \
-d amount_to_capture=4500
Drive captures from payment_intent.amount_capturable_updated; also wire payment_intent.requires_action (3DS pending), payment_intent.payment_failed, payment_intent.succeeded, and payment_intent.canceled. On abort, cancellation_reason accepts fraudulent, duplicate, abandoned, or requested_by_customer.
Idempotency on Retries
Autonomous loops retry, and Stripe’s idempotency exists so “you can safely repeat the request without risk of creating a second object.” Key mechanics:
- Send an
Idempotency-Keyheader (or SDK request option). Keys are up to 255 characters; V4 UUIDs are suggested, and sensitive data must not be used as keys. - Stripe saves the first response for a key — including
500errors — and replays it on retries. - Keys may be pruned after 24 hours; a pruned key reused later starts a new request.
- Reusing a key with different parameters errors, stopping a mutated retry.
- All
POSTrequests accept keys; never send them onGETorDELETE.
Derive keys from your order identifier, never from anything the model invents.
3-D Secure and SCA as a Step-Up
Treat 3DS as risk-based escalation, not a blanket tax. When 3DS authenticates a payment, liability for fraud-related disputes typically shifts from the seller to the issuer. Stripe triggers it automatically on soft decline codes and where regulations such as PSD2’s Strong Customer Authentication mandate require it — disabling Radar does not stop those triggers — and your rules can request it earlier:
Request 3DS if :risk_level: != 'normal' and :amount_in_usd: > 25
Afterward, evaluate is_3d_secure (supported and attempted), is_3d_secure_authenticated (full success), and has_liability_shift (Stripe expects the shift — possible without 3DS, e.g. Apple Pay in some regions). Two cautions: indiscriminate 3DS lowers conversion, and a payment can succeed even when CVC or postal verification fails. Keep :is_off_session: exclusions in block rules so recurring machine-initiated charges survive rules written for interactive checkout.
Disputes as the Feedback Loop
A dispute is a chargeback: the cardholder questions the payment with their issuer, the issuer opens a formal dispute on the card network, the payment reverses immediately, and Stripe debits your balance for the amount plus one or more network dispute fees. Close the loop in three directions:
- Respond. Submit evidence per reason code through the Dashboard or the Disputes API; network categories define acceptable proof.
- Learn. Refund with
reason=fraudulent, or setfraud_details: {user_report: "fraudulent"}on the charge. That adds the email address and card fingerprint to the default block lists, and Radar’s models respond to the feedback. - Prevent. Use early fraud warnings and the Verifi and Ethoca programs to stop disputes before filing, and watch the card networks’ monitoring programs when your dispute rate climbs.
How Agent-Initiated Transactions Differ
- Velocity is the anomaly. Humans cluster around office hours and round amounts; agents do not. Express velocity as rules on amount and payment-method attributes.
- Off-session is the norm. 3DS-heavy rules need
:is_off_session:carve-outs or conversion collapses. - New instruments are common. First-time cards, and new cards on a customer, are your highest-yield review triggers here.
- Never let the agent choose the threshold. The capture decision is policy, not inference.
amount_to_capture, the auto-capture ceiling, and whether a human reviews are versioned constants in your service code. An agent that can rewrite its own threshold — or be talked into it by injected text in an invoice description — can turn a hold into an instant capture at any amount. Give it a tool that captures within bounds and errors above them.
Common Pitfalls
- Skipping manual capture for simplicity. With
automaticcapture there is no review window. Start high-value agent flows withcapture_method=manual. - Gating on
risk_scorealone. It is plan-dependent; branch onrisk_leveland handlenot_assessedandunknown. - Block rules without traffic allocation. Stripe’s own fix for a too-broad country block is adding
and :risk_level: = 'elevated', rolled out gradually. - Reusing one idempotency key for create and capture. Different operations need different keys; the parameter-mismatch error exists for this.
- Treating AVS/CVC failure as a decline. Issuers may approve anyway; blocking on it eats false positives.
- Treating disputes as support chores. Unresponded disputes cost the payment plus fees and push you toward network monitoring programs.
- Letting the model mint retry keys. Derive them from the order identifier so a rewritten plan cannot manufacture a second charge.
Wrapping Up
Fraud detection for autonomous transactions is about putting decisions where the agents are not. Radar supplies signals and a risk level; rules translate your risk appetite into allow, block, review, and 3DS actions; capture_method=manual with amount_capturable gives you a reversible hold; idempotency keys make retries safe; disputes feed the cycle. The invariant to protect: thresholds, capture bounds, and policy versions live in reviewed code, and the agent only calls the door — it never decides how wide it opens.