PayinferenceDocs
payinference.com

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-key with scope decision:write
  • Headers: content-type: application/json, optional Idempotency-Key
  • Idempotency: optional Idempotency-Key header (max 255 chars). Retries with the same key replay the stored response (marked idempotency-replayed: true) for one hour. Without a key, every call creates a new decision_id. Keep order_id stable 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_ms reports 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#

bash
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 via POST /v1/pricing/rules, or webhook-derived observed fees). The cost and economics blocks are populated on those decisions; on deterministic-v1 decisions both fields are null.

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#

json
{
  "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_id next 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-key with scope outcome:write
  • Idempotency: reporting again with the same decision_id updates 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#

bash
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"
  }'
json
{
  "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-key with scope health:read
bash
curl -s http://localhost:4000/v1/provider-health -H "x-api-key: $PAYINFERENCE_API_KEY"
json
{
  "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-key with scope policy:read
  • Query parameters: page_size (default 25, max 100), action (filter to one instruction, e.g. hold), decision_mode (shadow or enforce), unresolved=true (only decisions with neither an outcome nor a review — with action=hold this is the held-payment review queue; see Hold and review integration), range (24h, 7d, 30d, 90d, or all — the default; an unknown value falls back to all), and order_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. Pass starting_after=<decision_id> for rows older than that decision, or ending_before=<decision_id> for rows newer; has_more says whether more rows exist in the direction you are walking. Cursor responses omit total (no count scan per page). Offset paging (page, default 1) is still accepted for shallow reads and includes total, 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.
bash
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"
json
{
  "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-key with scope policy:read
bash
curl -s "http://localhost:4000/v1/decisions/aggregate" -H "x-api-key: $PAYINFERENCE_API_KEY"
json
{
  "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-key with scope policy:read
  • Query parameters: days (window, default 30, max 90)
json
{
  "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 poll GET /v1/decisions instead.

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-key with scope policy:read
bash
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):

json
{
  "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-key with scope policy: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#

bash
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-key with scope policy:write
bash
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-key with scope policy: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-key with scope policy:read
bash
curl -s http://localhost:4000/v1/policies/m_123 -H "x-api-key: $PAYINFERENCE_API_KEY"
json
{
  "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.

bash
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.