API reference
Complete reference for the PayInference HTTP API. All requests and responses are JSON. All amounts are integers in minor units. All identifiers and enum values are snake_case.
Base URL. Production is https://api.payinference.com. Local development uses http://localhost:4000; the curl examples below use the local base URL so they run against pnpm dev as-is.
Authentication. Send an API key in the x-api-key header. Each endpoint lists its required scope. See Authentication.
Common response headers. Every response carries x-request-id, useful when reporting issues.
Errors. 400 validation errors return { "error": "validation_error", "issues": [...] }. See Errors.
Endpoints:
| Method and path | Purpose | Scope |
|---|---|---|
POST /v1/decision |
Create a payment decision | decision:write |
POST /v1/outcomes |
Report what happened after execution | outcome:write |
GET /v1/provider-health |
Current cached provider health snapshots | health:read |
GET /v1/decisions |
List decisions (decision log) | policy:read |
GET /v1/decisions/:decisionId |
Full detail for one decision | policy:read |
POST /v1/decisions/:decisionId/review |
Approve or reject a held decision | dashboard session (owner/admin/analyst) |
POST /v1/policies |
Create (and optionally publish) a policy | policy:write |
POST /v1/policies/translate |
Draft a policy from plain English | policy:write |
POST /v1/policies/simulate |
Evaluate a policy against a sample transaction | policy:read |
GET /v1/policies/:merchantId |
List a merchant's policies | policy:read |
GET /health |
Liveness probe | none (public) |
GET /ready |
Readiness probe (Postgres and Redis checks) | none (public) |
Endpoints not covered here: /v1/auth/*, /v1/api-keys, /v1/team, /v1/analytics/summary, /v1/alerts, /v1/integrations (including PUT /v1/integrations/:provider/webhook-secret, which sets or rotates a per-merchant webhook signing secret, write-only), and /v1/admin/* require a dashboard session (not an API key) and back the dashboard UI. POST /v1/provider-webhooks/:providerSlug/:merchantId is the signed provider webhook intake described in Payment Intelligence.
POST /v1/decision#
Creates one decision for one payment attempt. Designed for a sub-50 ms server-side budget; call it from your backend immediately before executing the payment.
- Auth:
x-api-keywith scopedecision:write - Headers:
content-type: application/json, optionalIdempotency-Key - Idempotency: optional
Idempotency-Keyheader (max 255 chars). Retries with the same key replay the stored response (markedidempotency-replayed: true) for one hour. Without a key, every call creates a newdecision_id. Keeporder_idstable across attempts of the same order. See Idempotency and retries. - Latency guidance: set a hard client timeout (50 to 250 ms) and a fallback path. The response's
decision_latency_msreports server-side time.
Request body#
| Field | Type | Required | Description | Example |
|---|---|---|---|---|
merchant_id |
string | yes | Your merchant identifier within the workspace | "m_123" |
order_id |
string | yes | Your identifier for this order; stable across retries. Max 128 chars | "order_456" |
transaction |
object | yes | The payment context (below) | |
available_providers |
string[] | yes | Providers you can execute on. At least one of stripe, adyen, paypal, checkout, flutterwave |
["stripe", "adyen"] |
previous_attempt |
object | no | Context of the prior failed attempt for retry policy (below) | |
risk_signals |
object | no | Merchant-supplied safe risk signals (below) | |
decision_mode |
enum | no | shadow or enforce. Shadow marks the instruction advisory: computed and recorded identically, but your existing logic executes. Omitted → the workspace default from the dashboard, then enforce |
"shadow" |
The body is strict: unknown fields anywhere are rejected with 400 validation_error. This is the mechanism that keeps card data out of PayInference.
transaction:
| Field | Type | Required | Description | Example |
|---|---|---|---|---|
amount |
integer | yes | Amount in minor units, positive | 14900 |
currency |
string | yes | ISO 4217, 3 letters | "USD" |
country |
string | yes | ISO 3166-1 alpha-2 | "US" |
payment_method |
enum | yes | card, wallet, bank_transfer, mobile_money |
"card" |
card_network |
enum | no | visa, mastercard, amex, discover, verve, other |
"visa" |
transaction_type |
enum | no | authorization (default), sale, capture |
"authorization" |
customer_type |
enum | no | new, returning, guest |
"returning" |
risk_level |
enum | no | Your fraud tool's band: low, medium, high |
"medium" |
risk_score |
integer | no | Your fraud tool's score, 0 to 100 | 42 |
previous_attempt:
| Field | Type | Required | Description | Example |
|---|---|---|---|---|
provider |
enum | yes | Provider that handled the failed attempt | "stripe" |
outcome |
enum | yes | timeout, declined, error |
"timeout" |
provider_reason_code |
string | no | Raw provider code, max 64 chars | "gateway_timeout" |
risk_signals (all optional, all non-sensitive aggregates):
| Field | Type | Description | Example |
|---|---|---|---|
velocity_count_10m |
integer | Attempts by this customer in the last 10 minutes | 2 |
velocity_count_1h |
integer | Attempts in the last hour | 3 |
velocity_amount_1h |
integer | Total minor units attempted in the last hour | 44700 |
previous_success_count |
integer | Lifetime successful payments for this customer | 12 |
previous_decline_count |
integer | Lifetime declines | 1 |
order_age_seconds |
integer | Seconds since the order/cart was created | 95 |
device_risk_band |
enum | low, medium, high from your device intelligence |
"low" |
ip_country_match |
boolean | Whether IP geolocation matches the transaction country | true |
merchant_supplied_risk_score |
integer | Your composite risk score, 0 to 100 | 18 |
Example request#
curl -s http://localhost:4000/v1/decision \
-H "content-type: application/json" \
-H "x-api-key: $PAYINFERENCE_API_KEY" \
-d '{
"merchant_id": "m_123",
"order_id": "order_456",
"transaction": {
"amount": 14900,
"currency": "USD",
"country": "US",
"payment_method": "card",
"card_network": "visa",
"transaction_type": "authorization",
"customer_type": "returning",
"risk_score": 42
},
"available_providers": ["stripe", "adyen", "paypal"],
"risk_signals": {
"velocity_count_1h": 3,
"previous_success_count": 12,
"device_risk_band": "low",
"ip_country_match": true
}
}'
Response body#
| Field | Type | Description | Example |
|---|---|---|---|
decision_id |
string | Unique id for this decision; join key for outcomes and the decision log | "dec_9f3a1c0b22d14e55" |
merchant_id |
string | Echoed merchant id | "m_123" |
mode |
enum | test or live, from the API key |
"live" |
decision_mode |
enum | shadow (advisory) or enforce (execute the instruction), echoed from the request |
"enforce" |
action |
enum | The instruction: route, use_default_route, step_up, hold, retry, failover, block |
"route" |
route |
object or null | Selected route; null when action is block |
|
route.primary_provider |
enum | Provider to execute on | "adyen" |
route.fallback_provider |
enum or null | Pre-approved second option | "stripe" |
risk |
object | Deterministic risk assessment (below) | |
provider_health |
object | Health summary per requested provider (below) | |
policy |
object | Which policy shaped the decision (below) | |
model |
object | Engine version and per-provider route scores (below) | |
cost |
object or null | Reserved for cost-aware routing (deterministic-v2), a future implementation — currently always null |
|
economics |
object or null | Reserved for cost-aware routing (deterministic-v2), a future implementation — currently always null |
|
reason_codes |
string[] | UPPER_SNAKE machine-readable explanation | ["ADYEN_HEALTHY_FOR_SEGMENT"] |
decision_latency_ms |
number | Server-side decision time | 11.4 |
ttl_ms |
integer | Freshness window; request a new decision after it elapses | 3000 |
risk:
| Field | Type | Description | Example |
|---|---|---|---|
score |
integer | 0 to 100 | 22 |
band |
enum | low, medium, high, extreme |
"low" |
recommended_action |
enum | route, step_up, block_review |
"route" |
reason_codes |
string[] | Risk-specific reason codes | ["NORMAL_AMOUNT_FOR_MERCHANT"] |
provider_health (map keyed by provider, only providers you listed):
| Field | Type | Description | Example |
|---|---|---|---|
score |
number | 0 to 100 health score | 94 |
state |
enum | healthy, watch, degraded, critical, unavailable, unknown |
"healthy" |
confidence |
number | 0 to 100, how much data backs the score | 91 |
source |
string | Snapshot provenance: probe, live, partial, demo, unknown |
"probe" |
policy:
| Field | Type | Description | Example |
|---|---|---|---|
policy_id |
string or null | Published policy applied, if any | "policy_m_123_v1" |
policy_version |
string or null | Version of that policy | "1.0.0" |
matched_rules |
string[] | Names of rules that matched | ["avoid_degraded_providers"] |
model:
| Field | Type | Description | Example |
|---|---|---|---|
model_version |
string | Inference engine generation | "deterministic-v1" |
route_scores |
object | Score per evaluated provider | { "adyen": 0.91 } |
Cost-aware routing (
deterministic-v2) activates for a merchant when its policy declares cost controls or resolved pricing is cached (contract upload viaPOST /v1/pricing/rules, or webhook-derived observed fees). Thecostandeconomicsblocks are populated on those decisions; ondeterministic-v1decisions both fields arenull.
cost (when present):
| Field | Type | Description | Example |
|---|---|---|---|
estimated_cost_minor |
integer | Estimated processing cost in minor units | 312 |
effective_bps |
number | Cost as basis points of the amount | 209.4 |
currency |
string | Cost currency | "USD" |
pricing_source |
enum | Where pricing came from; estimated_fallback and unknown are never treated as real pricing |
"merchant_contract" |
confidence_score |
number | 0 to 100 | 95 |
reason_codes |
string[] | Cost-specific codes, for example MERCHANT_CONTRACT_PRICING_USED |
economics (when present):
| Field | Type | Description | Example |
|---|---|---|---|
expected_value_minor |
number | Expected value of the chosen route in minor units | 13742 |
model_version |
string | Economics model | "deterministic-v2" |
optimization_mode |
enum | approval, cost, balanced, margin |
"balanced" |
Example response#
{
"decision_id": "dec_9f3a1c0b22d14e55",
"merchant_id": "m_123",
"mode": "live",
"decision_mode": "enforce",
"action": "route",
"route": { "primary_provider": "adyen", "fallback_provider": "stripe" },
"risk": {
"score": 0,
"band": "low",
"recommended_action": "route",
"reason_codes": [
"NORMAL_AMOUNT_FOR_MERCHANT",
"RETURNING_CUSTOMER",
"PRIOR_SUCCESSFUL_PAYMENTS",
"MERCHANT_SUPPLIED_ELEVATED_RISK"
]
},
"provider_health": {
"adyen": { "score": 94, "state": "healthy", "confidence": 91, "source": "probe" },
"stripe": { "score": 68, "state": "degraded", "confidence": 88, "source": "probe" },
"paypal": { "score": 82, "state": "watch", "confidence": 74, "source": "probe" }
},
"policy": {
"policy_id": "policy_m_123_v1",
"policy_version": "1.0.0",
"matched_rules": ["avoid_degraded_providers"]
},
"model": {
"model_version": "deterministic-v1",
"route_scores": { "adyen": 0.91, "paypal": 0.77, "stripe": 0.64 }
},
"cost": null,
"economics": null,
"reason_codes": ["ADYEN_HEALTHY_FOR_SEGMENT", "HIGHER_EXPECTED_APPROVAL_RATE", "RISK_ACCEPTABLE"],
"decision_latency_ms": 11.4,
"ttl_ms": 3000
}
Example responses for every instruction (use_default_route, step_up, hold, retry, failover, block) are on Decision instructions.
Errors#
| Status | Cause |
|---|---|
400 |
Schema violation, including unknown fields such as card data |
401 |
Missing or invalid API key |
403 |
Key lacks decision:write, is scoped to another merchant, or merchant_id is not in your workspace |
429 |
Rate limit exceeded |
Production notes#
- Log
decision_idnext to your PSP charge id on every attempt. - Treat a timeout as an outage, not a
block. Fall back per your own policy. See Production integration. - Respect
ttl_ms: decide, then execute immediately.
POST /v1/outcomes#
Reports the result of executing a decision. Idempotent per decision_id (upsert).
- Auth:
x-api-keywith scopeoutcome:write - Idempotency: reporting again with the same
decision_idupdates the stored outcome.
Request body#
| Field | Type | Required | Description | Example |
|---|---|---|---|---|
decision_id |
string | yes | The decision this outcome belongs to | "dec_9f3a1c0b22d14e55" |
order_id |
string | yes | Same order id used on the decision | "order_456" |
provider_used |
enum | yes | Provider you executed on | "adyen" |
outcome |
enum | yes | See outcome values below | "approved" |
provider_latency_ms |
integer | yes | How long the provider call took | 842 |
provider_reason_code |
string | no | Raw provider response code, max 64 chars | "do_not_honor" |
technical_failure_type |
enum | no | timeout, 5xx, connection, rate_limited, unknown |
"timeout" |
amount |
integer | yes | Minor units, matches the attempt | 14900 |
currency |
string | yes | ISO 4217 | "USD" |
created_at |
string | no | ISO 8601 timestamp for backfills | "2026-07-01T12:00:00Z" |
Outcome values: approved, soft_declined, hard_declined, technical_failed, timeout, rate_limited, blocked, step_up_completed, step_up_failed. Legacy aliases declined, error, cancelled are accepted and normalized. See Outcome feedback for when to use each.
Example#
curl -s http://localhost:4000/v1/outcomes \
-H "content-type: application/json" \
-H "x-api-key: $PAYINFERENCE_API_KEY" \
-d '{
"decision_id": "dec_9f3a1c0b22d14e55",
"order_id": "order_456",
"provider_used": "adyen",
"outcome": "soft_declined",
"provider_latency_ms": 604,
"provider_reason_code": "insufficient_funds",
"amount": 14900,
"currency": "USD"
}'
{
"outcome_id": "out_7c1d92",
"decision_id": "dec_9f3a1c0b22d14e55",
"outcome": "soft_declined",
"failure_class": "soft_decline",
"technical_failure_type": null,
"approved": false
}
Errors#
| Status | Cause |
|---|---|
400 |
Schema violation |
403 |
Key lacks outcome:write, or the decision belongs to a different merchant than the key's scope |
404 |
Unknown decision_id (or it belongs to another workspace) |
GET /v1/provider-health#
Returns the current cached health snapshot per provider segment. This endpoint reads stored snapshots only; it never probes a provider.
- Auth:
x-api-keywith scopehealth:read
curl -s http://localhost:4000/v1/provider-health -H "x-api-key: $PAYINFERENCE_API_KEY"
{
"providers": [
{
"provider": "adyen",
"segment_key": "adyen:card:USD",
"score": 94,
"state": "healthy",
"confidence": 91,
"p95_latency_ms": 412,
"timeout_rate": 0.002,
"error_5xx_rate": 0.001,
"throttle_429_rate": 0,
"approval_rate": 0.93,
"approval_rate_baseline": 0.91,
"webhook_lag_seconds": 4,
"sample_size": 1240,
"window_seconds": 300,
"reason_codes": ["HEALTHY_FOR_SEGMENT"],
"updated_at": "2026-07-01T12:00:00.000Z"
}
]
}
Field meanings are covered in Payment Intelligence.
GET /v1/decisions#
Lists decisions for your workspace, newest first. Merchant-scoped keys see only their merchant's decisions.
- Auth:
x-api-keywith scopepolicy:read - Query parameters:
page_size(default 25, max 100),action(filter to one instruction, e.g.hold),decision_mode(shadoworenforce),unresolved=true(only decisions with neither an outcome nor a review — withaction=holdthis is the held-payment review queue; see Hold and review integration),range(24h,7d,30d,90d, orall— the default; an unknown value falls back toall), andorder_id(exact match on your own order id — returns every decision recorded for that order, including retries). Shadow decisions are advisory and never appear in the unresolved queue. - Pagination: cursor-based, keyed by
decision_id. Passstarting_after=<decision_id>for rows older than that decision, orending_before=<decision_id>for rows newer;has_moresays whether more rows exist in the direction you are walking. Cursor responses omittotal(no count scan per page). Offset paging (page, default 1) is still accepted for shallow reads and includestotal, but is capped at the first 10 000 rows — walk cursors for deeper history. At enterprise volumes cursor cost stays flat at any depth; offset cost does not.
curl -s "http://localhost:4000/v1/decisions?page_size=25" -H "x-api-key: $PAYINFERENCE_API_KEY"
# next (older) page:
curl -s "http://localhost:4000/v1/decisions?page_size=25&starting_after=dec_9f3a1c0b22d14e55" -H "x-api-key: $PAYINFERENCE_API_KEY"
{
"page": 1,
"page_size": 25,
"total": 1204,
"has_more": true,
"decisions": [
{
"decision_id": "dec_9f3a1c0b22d14e55",
"merchant_id": "m_123",
"order_id": "order_456",
"created_at": "2026-07-01T12:00:00.000Z",
"mode": "live",
"decision_mode": "enforce",
"action": "route",
"primary_provider": "adyen",
"fallback_provider": "stripe",
"amount": 14900,
"amount_bucket": "100_500",
"currency": "USD",
"country": "US",
"payment_method": "card",
"risk_score": 22,
"risk_band": "low",
"provider_health_source": "probe",
"decision_latency_ms": 11.4,
"reason_codes": ["ADYEN_HEALTHY_FOR_SEGMENT"],
"model_version": "deterministic-v1",
"matched_rules": [],
"review": null,
"outcome": {
"outcome": "approved",
"provider_used": "adyen",
"provider_latency_ms": 842,
"failure_class": "none"
}
}
]
}
GET /v1/decisions/aggregate#
Lifetime decision aggregates for your workspace — instruction mix, provider share, risk bands, outcome counts, approval/feedback rates, and latency percentiles — computed by the database over your full recorded history and cached briefly server-side. Use this for headline stats instead of paging the list and folding rows yourself.
- Auth:
x-api-keywith scopepolicy:read
curl -s "http://localhost:4000/v1/decisions/aggregate" -H "x-api-key: $PAYINFERENCE_API_KEY"
{
"total": 1204,
"by_action": [{ "action": "route", "count": 800 }],
"by_provider": [{ "provider": "adyen", "count": 640 }],
"by_risk_band": [{ "band": "low", "count": 900 }],
"top_reason_codes": [{ "code": "ADYEN_HEALTHY_FOR_SEGMENT", "count": 512 }],
"reason_code_sample": 500,
"outcome_counts": [{ "outcome": "approved", "count": 700 }],
"with_outcome": 950,
"approved": 720,
"approval_rate": 0.7579,
"feedback_rate": 0.789,
"avg_latency_ms": 9.2,
"p95_latency_ms": 41.5,
"recent": [],
"generated_at": "2026-07-21T12:00:00.000Z",
"source": "live"
}
top_reason_codes is computed over the reason_code_sample most recent decisions (reason codes live in a JSON array, so exact lifetime counts would require a full-table scan); every other figure is exact over your full history. recent carries the 8 newest decisions in the list-row shape. source is cache when served from the short-lived server cache, live when computed for this request.
GET /v1/decisions/shadow-report#
Evaluates shadow traffic: how the would-have-been instructions compare with what your own logic actually did. Comparable wherever an outcome was reported — provider_used is what you executed, the decision row is what PayInference would have instructed. Use it to judge decision quality on real traffic before switching to enforce.
- Auth:
x-api-keywith scopepolicy:read - Query parameters:
days(window, default 30, max 90)
{
"window_days": 30,
"since": "2026-06-19T00:00:00.000Z",
"total_shadow_decisions": 1820,
"decisions_with_outcome": 1714,
"truncated": false,
"actions": { "route": 1650, "step_up": 120, "hold": 38, "block": 12 },
"route_comparison": {
"comparable": 1602,
"merchant_used_recommended": 1231,
"merchant_used_fallback": 88,
"merchant_diverged": 283,
"agreement_rate": 0.8234,
"approval_rate_when_followed": 0.914,
"approval_rate_when_diverged": 0.861
}
}
agreement_rate is the share of comparable decisions where your stack used the recommended (or pre-approved fallback) provider. The two approval rates answer the question shadow mode exists for: did payments do better when your logic agreed with the instruction than when it diverged?
POST /v1/decisions/:decisionId/review#
Resolves a held decision from the built-in review queue: { "resolution": "approve" } or { "resolution": "reject", "note": "…" } (note optional, ≤ 500 chars). Approve marks the review resolved — your backend still executes and reports the outcome. Reject records outcome blocked with reason code HOLD_REVIEW_REJECTED. One review per decision; conflicts (already reviewed, outcome already reported, not a hold, or a shadow decision — shadow holds are advisory and cannot be reviewed) return 409.
- Auth: dashboard session only, roles
owner/admin/analyst— API keys cannot resolve reviews. Merchant backends pollGET /v1/decisionsinstead.
GET /v1/decisions/:decisionId#
Full detail for one decision: the route, risk assessment, matched policy rules, per-provider evaluations with exclusion reasons, model scores, the review resolution for held decisions, and the joined outcome. This is the endpoint to use when debugging why a decision went the way it did.
- Auth:
x-api-keywith scopepolicy:read
curl -s http://localhost:4000/v1/decisions/dec_9f3a1c0b22d14e55 -H "x-api-key: $PAYINFERENCE_API_KEY"
Response (abridged; see Observability for how to read it):
{
"decision_id": "dec_9f3a1c0b22d14e55",
"merchant_id": "m_123",
"order_id": "order_456",
"created_at": "2026-07-01T12:00:00.000Z",
"mode": "live",
"action": "route",
"route": { "primary_provider": "adyen", "fallback_provider": "stripe" },
"provider_health_source": "probe",
"risk": { "score": 22, "band": "low", "recommended_action": "route", "reason_codes": [] },
"transaction": { "amount": 14900, "currency": "USD", "country": "US", "payment_method": "card" },
"policy": {
"policy_id": "policy_m_123_v1",
"policy_version": "1.0.0",
"matched_rules": ["avoid_degraded_providers"]
},
"model": { "model_version": "deterministic-v1", "provider_scores": {} },
"reason_codes": ["ADYEN_HEALTHY_FOR_SEGMENT"],
"decision_latency_ms": 11.4,
"ttl_ms": 3000,
"provider_evaluations": [
{
"provider": "stripe",
"eligible": false,
"route_score": 0,
"health_score": 68,
"health_state": "degraded",
"exclusion_reason": "POLICY_EXCLUDED_PROVIDER",
"components": null
}
],
"outcome": {
"outcome": "approved",
"provider_used": "adyen",
"provider_latency_ms": 842,
"provider_reason_code": "approved",
"failure_class": "none",
"technical_failure_type": null,
"approved": true,
"reported_at": "2026-07-01T12:00:03.000Z"
}
}
Errors: 404 for an unknown decision or one outside your workspace or merchant scope.
POST /v1/policies#
Creates a policy, optionally publishing it immediately. Publishing archives any previously published policy: one live policy per merchant.
- Auth:
x-api-keywith scopepolicy:write
Request body#
| Field | Type | Required | Description | Example |
|---|---|---|---|---|
merchant_id |
string | yes | Merchant the policy belongs to | "m_123" |
name |
string | yes | Display name, max 120 chars | "EU card routing" |
default_provider |
enum | no | Provider used when no rule selects one | "stripe" |
rules |
array | yes | Up to 100 rules (see Policy Manager for the rule shape) | |
publish |
boolean | no | Publish immediately (default false, saved as draft) |
true |
Example#
curl -s http://localhost:4000/v1/policies \
-H "content-type: application/json" \
-H "x-api-key: $PAYINFERENCE_API_KEY" \
-d '{
"merchant_id": "m_123",
"name": "High value protection",
"default_provider": "stripe",
"publish": true,
"rules": [
{
"rule_id": "high_value_step_up",
"name": "Step up high value new customers",
"priority": 10,
"enabled": true,
"when": { "amount_gte": 50000, "customer_type_in": ["new", "guest"] },
"then": { "type": "step_up" }
}
]
}'
Response: { "policy": { …full validated policy… }, "db_id": "…", "warnings": [] }.
Validation errors return 400 with { "error": "policy_validation_failed", "issues": [...] }. Errors always reject; warnings reject only on publish (drafts may be saved incomplete).
POST /v1/policies/translate#
Drafts a structured policy from plain English. The translator is rule-based, runs nowhere near the decision path, and its output must round-trip strict schema validation before it can be shown or saved. Nothing is persisted by this endpoint.
- Auth:
x-api-keywith scopepolicy:write
curl -s http://localhost:4000/v1/policies/translate \
-H "content-type: application/json" \
-H "x-api-key: $PAYINFERENCE_API_KEY" \
-d '{ "merchant_id": "m_123", "text": "Prefer Adyen for EU cards. Require 3DS above 500 EUR. Never retry hard declines." }'
Response: { "draft": { …policy… } | null, "warnings": [...], "valid": true | false }.
POST /v1/policies/simulate#
Evaluates a policy (a draft you pass inline, or the merchant's published policy when omitted) against a sample transaction using the same engine and cached provider health as the live Decision API. Nothing is persisted, and no live decision is affected.
- Auth:
x-api-keywith scopepolicy:read
Request body#
| Field | Type | Required | Description |
|---|---|---|---|
merchant_id |
string | yes | Merchant context |
policy |
object | no | Draft policy to test; omit to simulate the published policy |
transaction |
object | yes | Same shape as the decision request's transaction |
available_providers |
string[] | yes | Providers to evaluate |
previous_attempt |
object | no | Same shape as the decision request's previous_attempt; lets a simulation exercise previous_* conditions |
risk_signals |
object | no | Same shape as the decision request's risk_signals; lets a simulation exercise velocity and device conditions |
Response includes action, route, risk, matched_rules, excluded_providers, provider_evaluations, route_scores, reason_codes, and validation for the simulated policy.
GET /v1/policies/:merchantId#
Lists all policies for a merchant with their status (draft, published, archived), version, and rules.
- Auth:
x-api-keywith scopepolicy:read
curl -s http://localhost:4000/v1/policies/m_123 -H "x-api-key: $PAYINFERENCE_API_KEY"
{
"policies": [
{
"policy_id": "policy_m_123_01h9…",
"name": "High value protection",
"status": "published",
"version": "1.0.0",
"default_provider": "stripe",
"rules": [ … ],
"updated_at": "2026-07-01T12:00:00.000Z"
}
]
}
GET /health and GET /ready#
Public liveness and readiness probes, intended for load balancers and orchestration.
curl -s http://localhost:4000/health
# { "status": "ok", "service": "payinference-api", "time": "…" }
curl -s http://localhost:4000/ready
# { "status": "ready", "checks": { "postgres": "ok", "redis": "ok" } }
/ready returns 503 with per-dependency status when Postgres or Redis is unreachable.