Decisions API · v2
One call. A decision and a way to yes.
Send a payment, get back allow, step up, hold or block. In the analyst view you also get the calibrated fraud probability, the reasons, and the cheapest changes that would get it approved, each with the PayPal calls that would carry it out. Interactive schema: /api/docs.
Quickstart
curl -X POST "https://yes-if-silk.vercel.app/api/decide?view=analyst" \
-H "Content-Type: application/json" \
-d '{"TransactionAmt": 834, "TransactionDT": 13910400, "card6": "credit",
"prior_cnt": 30, "prior_mean_amt": 95, "prev_dt": 13800000,
"card_device_prior": 12, "DeviceInfo_distinct_uids": 1, "dist1": 8}'
POST /api/decide
POST /api/decide?view=user|analyst
- view=user (default):
{"action", "guidance"}only. No scores, thresholds or exact counterfactuals, so the decision boundary cannot be mapped from the buyer side. - view=analyst: everything below. Rate-limited to 60 requests a minute per client.
Transaction fields
Only TransactionAmt and TransactionDT are required. History and graph fields would come from your feature store; anything you leave out is filled from training-set defaults (per email domain where known).
| Field | Meaning |
|---|---|
| TransactionAmt | Amount in USD (required) |
| TransactionDT | Seconds from a reference point; hour of day is (DT // 3600) % 24 (required) |
| card6 | debit or credit; actionable |
| card4, ProductCD, DeviceType, P_emaildomain | Card network, product type, mobile/desktop, buyer email domain |
| dist1 | Distance between billing address and the payment's location (km) |
| prior_cnt, prior_mean_amt, prev_dt | Earlier payments on this card, their mean amount, and the previous payment's timestamp |
| card_device_prior | How many times this card has used this device before |
| DeviceInfo_distinct_uids, DeviceInfo_prior_cnt | Distinct cards and payments seen on this device so far (ring signal) |
| P_emaildomain_*, addr1_* | The same counts for the email domain and billing region |
Analyst response
{
"action": "HOLD", // ALLOW | STEP_UP | HOLD | BLOCK
"guidance": "This payment is being reviewed. You'll hear back shortly.",
"risk": 0.1316, // calibrated fraud probability
"thresholds": {"tau_allow": 0.0286, "tau_hold": 1.0, "tau_block": 1.0},
"top_reasons": [{"reason": "Time of day", "impact": 0.833, "direction": "raises"},
{"reason": "Amount vs. this card's usual spend", "impact": 0.814, "direction": "raises"}, …],
"counterfactuals": [{
"label": "yes, if it goes through 8 hours later",
"change": {"amount_mult": 1.0, "delay_h": 8, "card6": "credit"},
"risk": 0.018, "cost": 0.167, "plausible": true, "amount": 834.0,
"paypal_plan": [{"kind": "wait", "minutes": 480, …}, {"kind": "capture", "paypal_call": true, …}]
}, …],
"paypal_plan": [{"kind": "review", …}, {"kind": "capture", "amount": 834.0, "paypal_call": true, …}],
"transaction": { …the payment with defaults filled in… },
"latency_ms": 4.1
}
How the action is chosen. Below tau_allow the payment is allowed. Above it, a learned policy picks the cheapest friction (step-up, hold or block) from the calibrated risk, the amount, and what it has learned about how often each friction catches fraud or loses a good customer. tau_block forces a block.
Counterfactuals come from a grid of 83 changes: pay 10–90% now, delay 1–24 hours, switch debit/credit. Valid options fall under tau_allow; they are ranked by change cost (|log amount ratio| + 0.5 × days delayed + 0.3 × card switch) and kept diverse. They describe the model, not causal truth.
PayPal plans
Every plan assumes a PayPal order created with intent AUTHORIZE. Steps marked paypal_call map to the Payments API; the others are handled by your app.
| Step | Meaning |
|---|---|
| capture | POST /v2/payments/authorizations/{id}/capture; partial captures send final_capture: false |
| void | POST /v2/payments/authorizations/{id}/void |
| verify | One-time code or app confirmation, then continue |
| review | Keep the authorization; an analyst approves (capture) or rejects (void) |
| wait | Keep the authorization and capture after minutes |
| switch_funding | Ask the buyer to approve again with a different funding source |
Checkout endpoints
Used by the sandbox checkout. Amounts always come from the server-side catalog, never from the browser.
| Endpoint | Does |
|---|---|
| GET /api/checkout/config | Public client id, catalog and demo buyers |
| POST /api/checkout/quote | {product, persona} → what the buyer sees and what the merchant sees |
| POST /api/checkout/orders | {product, persona, offer?} → creates a PayPal AUTHORIZE order (403 if blocked with no offer) |
| POST …/orders/{id}/authorize | Checks approval and amount, authorizes, runs the plan until it needs the buyer, a reviewer or time |
| POST …/orders/{id}/continue | {code} for verification, {review: approve|reject} for holds, {} to skip a wait in the sandbox |
| GET …/orders/{id} | Current status and captures |
Other endpoints
| GET /api/health | Model version, metrics, thresholds and policy |
| GET /api/samples | Six labelled test-set payments (clean, large, borderline, takeover, ring, card testing) |
| GET /api/results | Every experiment result (E1–E7, fairness) behind this site |
Limits & safety
- Analyst view: 60 requests per minute per client. Checkout orders: 20 per minute.
- PayPal sandbox only: the client refuses any non-sandbox host.
- The model is trained on synthetic data and friction outcomes are simulated. Do not use it for real payment decisions.
- No LLM is involved in any decision.