PayinferenceDocs
payinference.com

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/decisions lists decisions with pagination and the joined outcome.
  • GET /v1/decisions/:decisionId returns 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-id header. 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_ms on 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 benchmark fires 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#

  1. Find the decision_id in your logs (or list recent decisions with GET /v1/decisions).
  2. Fetch GET /v1/decisions/:decisionId.
  3. Read action and reason_codes for the headline.
  4. Read policy.matched_rules to see which of your rules fired.
  5. Read provider_evaluations for per-provider eligibility and exclusion_reason.
  6. Check risk for the deterministic risk assessment and its reason codes.
  7. Check outcome to see what actually happened after execution.