PayinferencePayinferenceDocs
payinference.com
Documentation / Integration guides

Testing and the sandbox

The sandbox is the hosted API addressed with a test-mode key. It is not a separate deployment: PayInference never moves money and never calls a real PSP, so the same service answers test and live traffic and the key decides how a decision is recorded.

Test mode#

API keys carry a mode. A pi_test_… key marks every decision it creates with "mode": "test", keeping test traffic separate in the decision log, the analytics, and billing. Use a test key everywhere except production.

Item Value
API https://api.payinference.com
Dashboard https://dashboard.payinference.com
Key mode test

Create a test key under Developers → API keys in the dashboard. The raw secret is shown once, at creation, and never again.

What the sandbox does and does not do#

Every provider is a mock driven by synthetic probes. No real PSP credentials exist, and no payment can occur in either mode — PayInference returns an instruction, and your stack executes it. What test mode changes is bookkeeping, not behaviour: the decision path, policies, provider health and reason codes are identical, so a handler proven against a test key behaves the same against a live one.

Testing decision handling#

Exercise every instruction path in your handler. Instructions are driven by policy rules, retry context, and provider health, so drive them deliberately:

  • block and step_up: these come from matched policy rules, so publish a test policy with low thresholds, for example { "when": { "amount_gte": 1 }, "then": { "type": "step_up" } }, or a rule on risk_score_gte combined with a transaction.risk_score in your request. Sending risk_signals alone raises the advisory risk object in the response but does not change the instruction.
  • hold: publish a rule with { "then": { "type": "hold" } }; hold outranks step_up when both match.
  • retry: publish a retry_on_timeout rule and include previous_attempt with outcome: "timeout" in the request.
  • failover: include previous_attempt with outcome: "timeout" naming a provider whose cached health state is degraded or worse.
  • block via retry suppression: publish a no_retry rule listing a reason code, then send previous_attempt.provider_reason_code matching it.
  • use_default_route: make every offered provider ineligible (for example a require_min_health rule with a score of 100) while the policy defines a default_provider.

Verify each handled decision lands in the decision log with the outcome you reported: GET /v1/decisions/:decisionId.

Simulating policies#

Test a policy against sample transactions without publishing or affecting live decisions:

bash
curl -s https://api.payinference.com/v1/policies/simulate \
  -H "content-type: application/json" \
  -H "x-api-key: $PAYINFERENCE_API_KEY" \
  -d '{
    "merchant_id": "m_123",
    "transaction": { "amount": 75000, "currency": "EUR", "country": "DE", "payment_method": "card" },
    "available_providers": ["stripe", "adyen"]
  }'

The response shows the resulting action, matched rules, excluded providers, and per-provider evaluations, using the same engine and cached health as the live path.

Unit testing your integration#

Inject a fake fetch into the Node SDK to test your handler without any network:

ts
import { PayInferenceClient } from '@payinference/sdk';

const client = new PayInferenceClient({
  apiKey: 'pi_test_x',
  fetch: async () =>
    new Response(
      JSON.stringify({
        decision_id: 'dec_test',
        merchant_id: 'm_123',
        mode: 'test',
        action: 'route',
        route: { primary_provider: 'stripe', fallback_provider: null },
        provider_health: {},
        policy: { policy_id: null, policy_version: null, matched_rules: [] },
        model: { model_version: 'test', route_scores: {} },
        reason_codes: [],
        decision_latency_ms: 1,
        ttl_ms: 3000,
      }),
      { status: 200 },
    ),
});

Test your outage path too: point baseUrl at an unroutable address and assert that your fallback executes.

Latency verification#

You do not need a load-testing harness to check the budget: every decision reports its own cost. decision_latency_ms is the server-side compute time for that decision — policy, provider health, scoring — and excludes network transit and the persistence that happens after the response is sent.

bash
curl -s -w '\ntotal %{time_total}s | connect %{time_connect}s\n' \
  https://api.payinference.com/v1/decision \
  -H "content-type: application/json" \
  -H "x-api-key: $PAYINFERENCE_API_KEY" \
  -d '{ "merchant_id": "m_123", "order_id": "lat_1",
        "transaction": { "amount": 14900, "currency": "USD", "country": "US", "payment_method": "card" },
        "available_providers": ["stripe", "adyen"] }'

The decision prints first, then the timing line — so you see both numbers from one call. Read them together:

Measurement What it tells you
decision_latency_ms (in the body) PayInference's own compute. Design budget: under 50 ms p95
your wall clock (time_total) Compute plus your network round trip and TLS
the difference Your distance from the API — the part you can act on

That difference is usually the larger number, and it is the one worth optimising: colocate your backend near the API, keep connections warm, and set the SDK's timeoutMs against your observed total, not against the compute budget.

For a distribution rather than a single sample, point any HTTP load tool (hey, oha, k6, autocannon) at /v1/decision with a test key. Discard the first few requests: policy and provider health are served from short-lived caches that a cold process has to populate once.

Track the same figure in production from the decision_latency_ms you already receive on every call — see Observability.