Observability
Observability records what happened and why: every decision, the evidence behind it, the outcome you reported, and the operational metrics around the loop. Nothing here is optional telemetry bolted on later; the decision log and reason codes are part of the product contract.
Decision logs#
Every decision is persisted (off the critical path) with its context summary, instruction, matched policy rules, per-provider evaluations, reason codes, model version, and latency.
GET /v1/decisionslists decisions with pagination and the joined outcome.GET /v1/decisions/:decisionIdreturns the full picture for one decision.- The dashboard renders the same data with filtering and a per-decision explanation view.
The single most useful habit: log decision_id in your own systems next to your order id and PSP charge id. It is the join key for everything below.
Reason codes#
Every decision carries reason_codes: stable, UPPER_SNAKE, machine-readable strings. They are the only explanation surface on the decision path, never free text and never PII, so you can safely log, aggregate, and alert on them.
Examples by category:
| Category | Examples |
|---|---|
| Provider health | ADYEN_HEALTHY_FOR_SEGMENT, STRIPE_TIMEOUT_RATE_ELEVATED, CIRCUIT_OPEN |
| Routing | HIGHER_EXPECTED_APPROVAL_RATE, DEFAULT_ROUTE_APPLIED, NO_HEALTHY_PROVIDER_AVAILABLE |
| Policy | POLICY_PREFERRED_PROVIDER, POLICY_EXCLUDED_PROVIDER, STEP_UP_REQUIRED_BY_POLICY, RETRY_SUPPRESSED_BY_POLICY |
| Risk | RISK_ACCEPTABLE, RISK_ELEVATED, RISK_HIGH_BLOCK_REVIEW, RISK_SCORE_ABOVE_BLOCK_THRESHOLD |
| Cost | MERCHANT_CONTRACT_PRICING_USED, ESTIMATED_FALLBACK_PRICING_USED, COST_ABOVE_POLICY_LIMIT |
| Fallback | SDK_FALLBACK_USED, PAYINFERENCE_TIMEOUT, PAYINFERENCE_UNREACHABLE (emitted by the SDK's local fallback) |
Policy results#
Each decision records which policy version applied and which rules matched (policy.policy_id, policy.policy_version, policy.matched_rules). The decision detail additionally lists provider_evaluations: for every provider you offered, whether it was eligible, its health at decision time, its route score, and the exclusion_reason if it was ruled out. This is how you answer "why not Stripe?" for any specific payment.
Request IDs and traces#
- Every API response carries an
x-request-idheader. Capture it in your logs. - The platform is instrumented with OpenTelemetry. Spans cover auth, health lookup, route scoring, and decision persistence; metrics cover decision latency, action mix, route selection, outcome mix, and auth failures. Traces and metrics carry safe fields only (
decision_id, hashed merchant id, amount bucket, provider, reason codes), never payloads.
Outcome feedback in the loop#
Reported outcomes join the decision log (outcome on both list and detail endpoints), which turns the log from "what we decided" into "what we decided and whether it worked". Approval rates, failure classes, and provider latency distributions all derive from this join. See Outcome feedback.
Audit trail#
Sensitive actions are audit-logged with actor, action, resource, and metadata: policy publishing, API key creation and revocation, team and workspace changes, and platform-level operations. Dashboard users can review the trail under workspace audit logs.
Latency visibility#
decision_latency_mson every response tells you the server-side cost of that decision.- The dashboard tracks average and p95 decision latency against the 50 ms design budget.
pnpm benchmarkfires 1,000 requests at a local instance and prints average, p50, p95, p99, and max.
Watch your client-side latency separately; network time is on top of decision_latency_ms, which is why client timeouts and fallbacks matter. See Production integration.
Provider health context#
Health snapshots are part of observability too: each decision embeds the health summary of the providers you offered, and the decision log records provider_health_source, the provenance (probe, live, partial, demo, unknown) of the snapshot behind the chosen route. Current snapshots are always available at GET /v1/provider-health.
Debugging a decision, step by step#
- Find the
decision_idin your logs (or list recent decisions withGET /v1/decisions). - Fetch
GET /v1/decisions/:decisionId. - Read
actionandreason_codesfor the headline. - Read
policy.matched_rulesto see which of your rules fired. - Read
provider_evaluationsfor per-provider eligibility andexclusion_reason. - Check
riskfor the deterministic risk assessment and its reason codes. - Check
outcometo see what actually happened after execution.