PAYKX Decision API
Independent pre-execution intelligence for high-friction payment corridors. Send a corridor and amount, get back a clear GO / DEGRADED / NO-GO decision — proceed, pause, or reroute — with a confidence score and a seven-signal breakdown.
Checking API status… View status →Base URL
https://api.paykx.co.uk
Make your first call
Get an API key
Request access and we'll issue a live key. Until then, use the public demo endpoint. No key required.
Call the verify endpoint
Send a POST request with your corridor and amount, authenticated with your key.
curl https://api.paykx.co.uk/api/v1/verify \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"corridor": "GB-NG", "amount": 1500, "currency": "GBP"}'Read the decision
The response gives you a decision (GO / DEGRADED / NO-GO), a score, and the seven signals behind it. See Decisions for how to act on each.
{
"decision": "GO",
"score": 0.94,
"confidence": 0.97,
"recommended_partner": "Partner A",
"reasons": [
{ "code": "HEALTHY_PARTNER", "message": "All monitored execution signals are healthy." }
],
"reason_codes": ["HEALTHY_PARTNER"],
"request_id": "req_m3a7fzk2p1r4", // ← req_… format = demo only, not auditable (see note below)
"evaluated_at": "2026-07-15T11:30:47Z",
"decision_model": "v1.2",
"probe_count": 7,
"corridor": "GB-NG",
"currency": "GBP",
"corridor_supported": true,
"response_ms": 112,
"signals": {
"balance_sufficient": true,
"operation_valid": true,
"network_healthy": true,
"historical_failure_rate": 0.04,
"cop_overridden": false,
"fx_volatility": "low",
"fraud_flags": 0
},
"probe_details": {
"fraud_flags": { "signal": "CLEAR", "risk_value": 1.00 },
"network_healthy": { "signal": "CLEAR", "risk_value": 0.92 },
"balance_sufficient": { "signal": "CLEAR", "risk_value": 1.00 },
"fx_volatility": { "signal": "CLEAR", "risk_value": 0.88 },
"operation_valid": { "signal": "CLEAR", "risk_value": 0.95 },
"historical_failure_rate": { "signal": "CLEAR", "risk_value": 0.96 },
"cop_overridden": { "signal": "CLEAR", "risk_value": 1.00 }
},
"environment": "fca-sandbox-validated",
"api_version": "1.2"
}req_… IDs are demo-only and not auditable.
The request_id in this example uses the demo format. It is ephemeral — demo calls are never persisted, so passing this ID to
GET /api/v1/audit/:id returns a 404.
Auditable IDs use the format PAYKX-{year}-{id} and are only issued by the authenticated
POST /api/v1/verify endpoint.
Authentication & API Keys
Every call to a protected endpoint requires your API key, passed as a Bearer token. There are two key types — test keys for shadow mode and live keys for production.
Key types
| Type | Format | Environment | Behaviour |
|---|---|---|---|
| Test key | pk_test_xxxxx | Shadow / demo | Full scoring, no live rails. Responses are deterministic and safe to run repeatedly. |
| Live key | pk_live_xxxxx | Production | Real corridor data, live partner probes. Requires approved access. |
How to authenticate
Pass your key in the Authorization header as a Bearer token, or alternatively in the X-API-Key header:
curl https://api.paykx.co.uk/api/v1/verify \
-H "Authorization: Bearer pk_test_xxxxx" \
-H "Content-Type: application/json" \
-d '{"corridor": "GB-NG", "amount": 1500, "currency": "GBP"}'curl https://api.paykx.co.uk/api/v1/verify \
-H "X-API-Key: pk_test_xxxxx" \
-H "Content-Type: application/json" \
-d '{"corridor": "GB-NG", "amount": 1500, "currency": "GBP"}'const res = await fetch("https://api.paykx.co.uk/api/v1/verify", {
method: "POST",
headers: {
"Authorization": "Bearer pk_test_xxxxx",
"Content-Type": "application/json"
},
body: JSON.stringify({ corridor: "GB-NG", amount: 1500, currency: "GBP" })
});
const { decision, score, confidence, reason_codes } = await res.json();import requests
resp = requests.post(
"https://api.paykx.co.uk/api/v1/verify",
headers={"Authorization": "Bearer pk_test_xxxxx"},
json={"corridor": "GB-NG", "amount": 1500, "currency": "GBP"},
)
data = resp.json()
print(data["decision"], data["confidence"], data["reason_codes"])Key lifecycle
| Topic | Detail |
|---|---|
| Expiry | Test keys do not expire. Live keys expire after 90 days and are reissued on request. |
| Rotation | To rotate, submit a new access request noting you need a replacement. Old keys are revoked within 24 hours of issuing the new one. |
| Errors | A missing or invalid key returns 401 Unauthorized. There are no fallback or default keys — the API always fails closed. |
| Scope | Keys scope verification history — your key only reads results it created. Two callers cannot see each other's records. |
Onboarding flow
From zero to a verified, scoped API key in five steps:
Go to /request-access and fill in your name, email, and a one-line description of your use case. No credit card or contract needed.
A paykx-sandbox-… key is issued on submission. Use it to call any protected endpoint right now. Responses are identical in shape to production — you can build your full integration today.
Send real request shapes. The sandbox scoring engine runs the same seven-signal pipeline as production. Use GET /api/verifications (with your key) to retrieve your verification history for QA.
The PAYKX team reviews your request. On approval, a paykx-live-… key is issued directly to you. The raw key is shown once — copy it immediately and store it as an environment variable.
Swap the environment variable value from your sandbox key to your live key. The request and response shape is identical. Your integration keeps working without modification.
Permissions
| Permission | Sandbox key | Live key |
|---|---|---|
Call /api/v1/verify | ✓ | ✓ |
Call /api/v1/demo-verify | Not needed — public | Not needed — public |
Call /api/v1/demo-compare | Not needed — public | Not needed — public |
Call /api/v1/batch-assess | ✓ | ✓ |
| Read own verification history | ✓ | ✓ |
| Read other keys' verifications | ✗ | ✗ |
| Admin panel access | ✗ | ✗ |
Keys are scoped to the caller. Each key can only read the verification records it created — two API consumers are never able to see each other's data.
Sandbox / Demo
PAYKX currently runs in shadow mode. This is a safe test environment where every decision is computed with the real scoring engine, but no live payment rails are touched. It's a good way to integrate and test before you go live.
Public demo endpoint
Want to try PAYKX without a key? The demo endpoint needs no authentication and returns a full seven-signal decision for the GB-NG corridor.
curl -X POST https://api.paykx.co.uk/api/v1/demo-verify \
-H "Content-Type: application/json" \
-d '{"corridor": "GB-NG", "amount": 1500, "currency": "GBP"}'Profile comparison endpoint
Want to see how different risk profiles score the same payment? POST /api/v1/demo-compare needs no authentication and scores a payment against conservative, balanced, and speed-optimised profiles in one call. Use amount: 85000 to see profiles diverge.
curl -X POST https://api.paykx.co.uk/api/v1/demo-compare \
-H "Content-Type: application/json" \
-d '{
"payment": {"corridor": "GB-NG", "amount": 85000, "currency": "GBP"},
"profiles": ["conservative", "balanced", "optimise_speed"]
}'See the full endpoint reference in the API Reference section below, or explore it interactively in the API Explorer ↗.
API Reference
Full request and response detail for every endpoint. Expand a card to see parameters, an example response, and a live “Try it out” console.
Runs a deterministic risk assessment on the specified corridor using seven weighted signals:
fraud_flags (30%), network_healthy (25%), balance_sufficient (15%),
fx_volatility (10%), operation_valid (10%), historical_failure_rate (5%), cop_overridden (5%).
Score ≥ 0.80 → GO | 0.48–0.79 → DEGRADED | < 0.48 → NO-GO
| Name | In | Type | Description |
|---|---|---|---|
| mode | query | string | live (default) or shadow |
| Authorization* | header | string | Bearer <your-api-key> |
| Field | Type | Required | Description |
|---|---|---|---|
| corridor | string | YES | Payment corridor in FROM-TO format, e.g. GB-NG, NG-GB |
| amount | number | optional | Transfer amount in units of currency. Triggers policy cap for large transfers (> 50,000 → DEGRADED; > 500,000 → NO-GO) |
| currency | string | optional | ISO 4217 currency code, e.g. GBP, USD, NGN. Defaults to GBP |
| idempotency_key | string | optional | A unique identifier (UUID recommended) that makes retries safe. Repeated calls with the same key and body return the cached result. Mismatched body returns 409 |
"decision": "GO", "score": 0.87, "volatility": 0.09, "confidence": 0.87, "probe_count": 7, "corridor_supported": true, "validation_note": "Corridor fully validated with deep data.", "idempotency_hit": false, "signals": { "balance_sufficient": true, "operation_valid": true, "network_healthy": true, "historical_failure_rate": 0.04, "cop_overridden": false, "fx_volatility": "low", "fraud_flags": 0 }, "probe_details": { "balance_sufficient": { "signal": "CLEAR", "risk_value": 1.00 }, "operation_valid": { "signal": "CLEAR", "risk_value": 0.95 }, "network_healthy": { "signal": "CLEAR", "risk_value": 0.92 }, "historical_failure_rate": { "signal": "CLEAR", "risk_value": 0.96 }, "cop_overridden": { "signal": "CLEAR", "risk_value": 1.00 }, "fx_volatility": { "signal": "CLEAR", "risk_value": 0.88 }, "fraud_flags": { "signal": "CLEAR", "risk_value": 1.00 } }, "currency": "GBP", "environment": "fca-sandbox-validated"
"decision": "DEGRADED", "score": 0.61, "confidence": 0.88, "recommended_partner": "Partner B", "reasons": [ { "code": "HIGH_FX_VOLATILITY", "message": "Foreign exchange spread or rate volatility is elevated." }, { "code": "HIGH_FAILURE_RATE", "message": "Historical failure rate on this corridor is above acceptable threshold." } ], "reason_codes": ["HIGH_FX_VOLATILITY", "HIGH_FAILURE_RATE"], "request_id": "req_p9xk2m4r7nq1", "evaluated_at": "2026-07-15T11:31:22Z", "decision_model": "v1.2", "probe_count": 7, "corridor_supported": true, "signals": { "balance_sufficient": true, "operation_valid": true, "network_healthy": true, "historical_failure_rate": 0.28, "cop_overridden": false, "fx_volatility": "high", "fraud_flags": 0 }, "environment": "fca-sandbox-validated"
Returns the current health status of the PAYKX Rail API, including version and mode. No authentication required.
"status": "healthy", "version": "1.2", "message": "PAYKX Rail Verification API is running successfully", "mode": "shadow", "environment": "shadow-demo"
Returns the system-level maturity and confidence status of the Corridor Intelligence Platform (CIP). No authentication required. Use this endpoint to determine unambiguously whether CIP probe telemetry is influencing live scoring decisions.
confidence: "experimental" — telemetry only.The CIP is in Phase 0. Probe data is collected and stored but
decision_influence: false means
no CIP signal has any effect on POST /api/v1/verify responses.
This status will advance to "provisional" once the
quality gate defined in ADR-015 is passed.
| Field | Type | Description |
|---|---|---|
| phase | number | CIP implementation phase. 0 = telemetry collection only; 1 = sandbox coverage; 2 = production signals; 3 = full network. |
| state | string | "telemetry_only" — data collected, no scoring influence. "scoring_passive" — signals computed, blend_weight=0. "scoring_active" — blend_weight>0, influencing decisions. |
| decision_influence | boolean | false means no CIP observation has any effect on any verify decision. Only becomes true after the ADR-015 governance process is completed. |
| supported_for_decisioning | boolean | false means this telemetry layer is not cleared for production payment decisions. Explicit complement to decision_influence — removes all ambiguity that telemetry exists but is not yet trusted for scoring. |
| confidence | string | "experimental" — corpus <14 days, unvalidated. "provisional" — quality gate passed, blend_weight 0.05–0.10. "validated" — full production coverage, blend_weight ≥0.15. |
| last_updated | string (ISO 8601) | The date the CIP entered its current phase and confidence state. This is not the date of the last API request, last governance document edit, or last deployment — it changes only when a formal phase or confidence transition is committed through the ADR-015 governance process. |
| observations_collected | number | Total synthetic probe observations stored in cip.observations. 0 during Phase 0 before the probe pipeline is live. Use this to track corpus growth over time. |
| blend_weight | number | Proportion of the composite score drawn from CIP signals (0–1). Governed by ADR-015. Requires two-engineer PR approval and named decision-maker sign-off to increase. |
| escalation_enabled | boolean | false — anomaly alerts never auto-degrade a corridor regardless of severity or duration. Phase 0 constraint; enabled in Phase 1 after sustained-alert validation. |
| active_adapters | string[] | Adapter IDs currently providing telemetry data. Grows as additional data sources are confirmed permitted and integrated. |
| governed_by | string | ADR reference that controls changes to this status. All state transitions require the process defined there. |
{
"phase": 0,
"state": "telemetry_only",
"decision_influence": false,
"supported_for_decisioning": false,
"confidence": "experimental",
"last_updated": "2026-08-01T00:00:00.000Z",
"observations_collected": 0,
"blend_weight": 0,
"escalation_enabled": false,
"active_adapters": ["synthetic-baseline", "central-bank-ng"],
"governed_by": "ADR-015"
}
Runs the same seven-signal scoring as /api/v1/verify for the GB-NG corridor, with no API key required. Rate limited to 15 requests per 15 minutes per IP.
| Field | Type | Required | Description |
|---|---|---|---|
| corridor | string | optional | Defaults to GB-NG |
| amount | number | optional | Transfer amount (defaults to 1000) |
| currency | string | optional | Defaults to GBP |
Evaluates the same payment against up to three named risk profiles (conservative, balanced, optimise_speed) and returns a divergence summary. No API key required. Rate limited to 15 requests per 15 minutes per IP. For authenticated production use, see POST /api/v1/decision/compare.
| Field | Type | Required | Description |
|---|---|---|---|
| payment | object | YES | Payment details: corridor, amount, currency. Defaults to GB-NG / 1000 / GBP. |
| profiles | string[] | optional | Which profiles to compare: conservative, balanced, optimise_speed. Defaults to all three. |
curl -X POST https://api.paykx.co.uk/api/v1/demo-compare \
-H "Content-Type: application/json" \
-d '{
"payment": { "corridor": "GB-NG", "amount": 85000, "currency": "GBP" },
"profiles": ["conservative", "balanced", "optimise_speed"]
}'"corridor": "GB-NG", "amount": 85000, "profiles_compared": ["conservative", "balanced", "optimise_speed"], "results": { "conservative": { "decision": "DEGRADED", "score": 0.726, "go_threshold": 0.85 }, "balanced": { "decision": "DEGRADED", "score": 0.726, "go_threshold": 0.80 }, "optimise_speed": { "decision": "GO", "score": 0.726, "go_threshold": 0.70 } }, "divergence": { "profiles_agree": false, "summary": "Profiles diverge: conservative→DEGRADED, balanced→DEGRADED, optimise_speed→GO." }, "evaluated_at": "2026-07-29T12:00:00Z", "environment": "sandbox"
Submit a request for a production API key. This records your request and returns a confirmation; once reviewed and approved, the PAYKX team issues a live key. Rate limited to 5 requests per 15 minutes per IP and protected by reCAPTCHA. The easiest way to submit is the access request form, which handles the CAPTCHA for you.
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | YES | Your name |
| string | YES | Work email | |
| reason | string | YES | Why you need access |
| recaptcha_token | string | YES | reCAPTCHA token (supplied automatically by the form) |
Corridor Scope
PAYKX only returns a confident decision for corridors it has deep, validated data for. Everything else is deliberately returned as DEGRADED so you are never given false confidence.
| Corridor | Route | Status | Behaviour |
|---|---|---|---|
GB-NG | United Kingdom → Nigeria | Fully supported | Full seven-signal scoring, real GO / DEGRADED / NO-GO |
NG-GB | Nigeria → United Kingdom | Fully supported | Full seven-signal scoring, real GO / DEGRADED / NO-GO |
| Any other | e.g. US-GB, GB-US, US-NG | Limited | Returns corridor_supported: false and a forced DEGRADED decision |
corridor_supported flag on the response. When it is false, treat the result as “not enough data” rather than a genuine risk signal.Decisions
Every verification returns one of three decisions. For supported corridors the decision follows the composite score band below, so it's fully deterministic and explainable. Unsupported corridors are always returned as DEGRADED (see note).
| Decision | Score band | Meaning | What the operator should do |
|---|---|---|---|
| GO | ≥ 0.80 | Low risk | Proceed with the payment |
| DEGRADED | 0.48 – 0.79 | Moderate risk | Review manually or route with caution |
| NO-GO | < 0.48 | High risk | Block or investigate before sending |
GB-NG and NG-GB are fully validated with deep data. Any other corridor returns corridor_supported: false and an honest DEGRADED decision rather than a falsely confident result.Score vs Confidence — why both matter
The response returns two separate numeric fields that are easy to conflate but operationally distinct:
| Field | What it measures | Range |
|---|---|---|
score | The composite risk score — the weighted sum of all seven signal values. Drives the decision band directly. | 0.00 – 1.00 |
confidence | How certain the engine is about its own assessment given the signal quality and corridor data depth. High confidence means the decision is well-supported; low confidence means it is based on thinner data. | 0.00 – 1.00 |
Two DEGRADED decisions can look identical in the decision band but be operationally very different:
"decision": "DEGRADED", "score": 0.68, "confidence": 0.97, "recommended_partner": "Partner B", "reasons": [{ "code": "HIGH_FX_VOLATILITY", "message": "FX volatility elevated." }]
High confidence DEGRADED — strong signal, known cause, route or review with certainty.
"decision": "DEGRADED", "score": 0.68, "confidence": 0.51, "recommended_partner": null, "reasons": [{ "code": "CORRIDOR_UNSUPPORTED", "message": "Limited data." }]
Low confidence DEGRADED — thin data, treat as “not enough signal” and apply more caution.
Code Examples
Call the verify endpoint from any stack. Replace YOUR_API_KEY with your issued key.
curl https://api.paykx.co.uk/api/v1/verify \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"corridor": "GB-NG", "amount": 1500, "currency": "GBP"}'import requests
resp = requests.post(
"https://api.paykx.co.uk/api/v1/verify",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={"corridor": "GB-NG", "amount": 1500, "currency": "GBP"},
)
data = resp.json()
print(data["decision"], data["score"])const res = await fetch("https://api.paykx.co.uk/api/v1/verify", {
method: "POST",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json"
},
body: JSON.stringify({ corridor: "GB-NG", amount: 1500, currency: "GBP" })
});
const data = await res.json();
console.log(data.decision, data.score);SDKs
The official @paykx/sdk package is in private beta. Copy the wrapper below into your project now — it has the same interface the published package will use, so your code will not need to change.
npm install @paykx/sdk
import { verify } from '@paykx/sdk';
const result = await verify({
apiKey: process.env.PAYKX_API_KEY,
corridor: 'GB-NG',
amount: 1500,
currency: 'GBP',
});
// result.decision | result.confidence | result.reason_codes
Request early access: taseenrayed@paykx.co.uk
Use today — copy this wrapper
Same interface, no dependency, works in any Node.js or browser environment:
// paykx.js — drop-in client, same interface as @paykx/sdk
const BASE = "https://api.paykx.co.uk";
export async function verify({ apiKey, corridor, amount, currency = "GBP", idempotencyKey } = {}) {
const res = await fetch(`${BASE}/api/v1/verify`, {
method: "POST",
headers: {
"Authorization": `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ corridor, amount, currency, idempotency_key: idempotencyKey }),
});
if (res.status === 429) {
const retryAfter = res.headers.get("Retry-After");
throw Object.assign(new Error("Rate limited"), { status: 429, retryAfter });
}
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw Object.assign(new Error(err.message ?? `PAYKX error ${res.status}`), { status: res.status });
}
return res.json(); // { decision, score, confidence, reason_codes, signals, probe_details, … }
}
// usage
const { decision, confidence, reason_codes } = await verify({
apiKey: process.env.PAYKX_API_KEY,
corridor: "GB-NG",
amount: 1500,
});
console.log(decision, confidence, reason_codes);# paykx.py — drop-in client, same interface as paykx-sdk (PyPI, coming soon)
import os, requests
BASE = "https://api.paykx.co.uk"
def verify(*, api_key=None, corridor, amount, currency="GBP", idempotency_key=None):
api_key = api_key or os.environ["PAYKX_API_KEY"]
resp = requests.post(
f"{BASE}/api/v1/verify",
headers={"Authorization": f"Bearer {api_key}"},
json={"corridor": corridor, "amount": amount, "currency": currency,
**({"idempotency_key": idempotency_key} if idempotency_key else {})},
timeout=10,
)
resp.raise_for_status()
return resp.json() # dict with decision, score, confidence, reason_codes, …
# usage
result = verify(corridor="GB-NG", amount=1500)
print(result["decision"], result["confidence"], result["reason_codes"])// paykx.cjs — CommonJS version
const BASE = "https://api.paykx.co.uk";
async function verify({ apiKey, corridor, amount, currency = "GBP", idempotencyKey } = {}) {
const res = await fetch(`${BASE}/api/v1/verify`, {
method: "POST",
headers: { "Authorization": `Bearer ${apiKey}`, "Content-Type": "application/json" },
body: JSON.stringify({ corridor, amount, currency, idempotency_key: idempotencyKey }),
});
if (!res.ok) throw new Error(`PAYKX ${res.status}`);
return res.json();
}
module.exports = { verify };package paykx
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
const base = "https://api.paykx.co.uk"
type VerifyRequest struct {
Corridor string `json:"corridor"`
Amount float64 `json:"amount,omitempty"`
Currency string `json:"currency,omitempty"`
IdempotencyKey string `json:"idempotency_key,omitempty"`
}
type VerifyResponse struct {
Decision string `json:"decision"`
Score float64 `json:"score"`
Confidence float64 `json:"confidence"`
ReasonCodes []string `json:"reason_codes"`
RequestID string `json:"request_id"`
EvaluatedAt string `json:"evaluated_at"`
}
func Verify(apiKey string, req VerifyRequest) (*VerifyResponse, error) {
body, _ := json.Marshal(req)
r, err := http.NewRequest("POST", base+"/api/v1/verify", bytes.NewReader(body))
if err != nil {
return nil, err
}
r.Header.Set("Authorization", "Bearer "+apiKey)
r.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(r)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("paykx: %s", resp.Status)
}
var result VerifyResponse
json.NewDecoder(resp.Body).Decode(&result)
return &result, nil
}
// usage
// result, err := paykx.Verify(os.Getenv("PAYKX_API_KEY"), paykx.VerifyRequest{
// Corridor: "GB-NG", Amount: 1500, Currency: "GBP",
// })# Verify corridor — replace YOUR_KEY with your API key
curl https://api.paykx.co.uk/api/v1/verify \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"corridor":"GB-NG","amount":1500,"currency":"GBP"}'
# Demo endpoint — no key required
curl -X POST https://api.paykx.co.uk/api/v1/demo-verify \
-H "Content-Type: application/json" \
-d '{"corridor":"GB-NG","amount":1500,"currency":"GBP"}'
# Profile comparison — no key required
# Returns the same payment scored against conservative / balanced / speed-optimised profiles
curl -X POST https://api.paykx.co.uk/api/v1/demo-compare \
-H "Content-Type: application/json" \
-d '{"payment":{"corridor":"GB-NG","amount":85000,"currency":"GBP"},"profiles":["conservative","balanced","optimise_speed"]}'
# Health check
curl https://api.paykx.co.uk/api/health
# With idempotency (safe retries)
curl https://api.paykx.co.uk/api/v1/verify \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"corridor":"GB-NG","amount":1500,"currency":"GBP","idempotency_key":"uuid-goes-here"}'Error Codes
PAYKX uses standard HTTP status codes. Every error response includes a JSON body with a message field.
| Code | Meaning | When it happens |
|---|---|---|
| 200 | OK | Decision returned successfully |
| 400 | Bad Request | Request body is missing required fields or is malformed |
| 401 | Unauthorized | Missing or invalid API key |
| 409 | Conflict | An idempotency key was reused with a different request body |
| 422 | Unprocessable | Request parsed but semantically invalid (e.g. unsupported corridor) |
| 429 | Too Many Requests | Rate limit exceeded, slow down and retry later |
| 500 | Server Error | Unexpected error — safe to retry with exponential backoff |
| 503 | Service Unavailable | Decision service temporarily unavailable — retry later |
JSON response bodies
Every non-200 response returns the same JSON envelope. Use message for display and code (when present) for programmatic logic.
{ "message": "corridor is required" }
{ "message": "Invalid API key." }
{ "message": "Idempotency key reused with a different request body." }
HTTP/1.1 429 Too Many Requests
Retry-After: 37
{ "message": "Rate limit exceeded. Try again in 37 seconds." }
{ "message": "Internal Server Error" }
{ "message": "Service temporarily unavailable." }
Idempotency
Pass an idempotency_key (for example a UUID) in the request body to make retries safe. The first request with a given key is processed and cached; reusing the same key with the same body returns the identical cached result with idempotency_hit: true. Reusing it with a different body returns 409 Conflict.
Error Handling Guide
Practical guidance for each error state — what caused it, how to detect it in code, and the correct recovery action for your integration.
Cause: The request body is missing a required field, has an invalid value, or the JSON is malformed. Common triggers: missing corridor or amount, a corridor string in the wrong format (e.g. "UK-NG" instead of "GB-NG"), or a negative amount.
HTTP/1.1 400 Bad Request
{
"message": "corridor is required",
"code": "INVALID_REQUEST"
}
Recommended action:
- Read the
messagefield — it identifies the exact field or constraint that failed. - Do not retry automatically. Fix the request payload before sending again.
- Log the full request body alongside the error so your team can diagnose integration issues quickly.
- Validate
corridoragainst the supported corridor list before sending.
Cause: The Authorization header is absent, the token is malformed, or the key has been revoked or has not yet been approved.
HTTP/1.1 401 Unauthorized
{
"message": "Invalid API key."
}
Recommended action:
- Confirm your key is being sent as
Authorization: Bearer <key>— not in the query string or body. - Check for whitespace or line breaks in the stored key value (common in copy-paste from email).
- Do not retry with the same key — repeated 401s against a production key may trigger abuse alerts.
- If the key was just issued, allow up to 60 seconds for propagation before retrying.
- To rotate a compromised key, contact access@paykx.co.uk immediately.
Cause: Your API key (or source IP for public endpoints) has exceeded the per-minute or per-hour request allowance. See the Rate Limits table for the exact thresholds.
HTTP/1.1 429 Too Many Requests
Retry-After: 37
{
"message": "Rate limit exceeded. Try again in 37 seconds."
}
Recommended action:
- Read the
Retry-Afterresponse header — it tells you the exact number of seconds to wait. - Implement exponential backoff with jitter if you do not read
Retry-After: start at 1 s, double on each retry up to 60 s, add ±10% random jitter. - If you regularly hit rate limits, batch payment intents server-side before calling PAYKX, or request a higher tier — contact access@paykx.co.uk.
- Do not fan out multiple identical requests to work around rate limits — use
idempotency_keyfor safe retries instead.
Cause: One or more upstream data signals (FX feed, network health probe, fraud flag provider) are temporarily unreachable. PAYKX cannot produce a reliable decision and will not guess. This is a transient condition — typically resolves within 2 minutes.
HTTP/1.1 503 Service Unavailable
Retry-After: 120
{
"message": "Signal provider temporarily unavailable. Retry after 2 minutes.",
"unavailable_signals": ["fx_volatility", "network_healthy"]
}
Recommended action:
- This is safe to retry. Wait at least the
Retry-Afterseconds, then resubmit with the sameidempotency_key. - In your internal logic, treat a 503 as "DEGRADED — hold payment" rather than as a permanent failure.
- Subscribe to the status page for incident alerts so your on-call team is notified automatically.
- If you receive 503 for more than 5 minutes continuously, escalate to access@paykx.co.uk.
- Do not fall back to allowing payments unconditionally during a PAYKX outage — your downstream compliance policy should define what to do when intelligence is unavailable.
Rate Limits
Public endpoints are rate limited per IP. Authenticated endpoints are gated per API key. Exceeding any limit returns 429 Too Many Requests with a Retry-After header telling you when to retry.
| Endpoint | Auth | Limit | Window | Scope |
|---|---|---|---|---|
/api/v1/verify | Required | 100 requests | 1 minute | Per API key |
/api/v1/demo-verify | None | 15 requests | 15 minutes | Per IP |
/api/v1/demo-compare | None | 15 requests | 15 minutes | Per IP |
/api/v1/request-access | None | 5 requests | 15 minutes | Per IP |
/api/health | None | Unlimited | — | — |
Retry-After header
When a 429 is returned, the response includes a Retry-After header with the number of seconds until your limit resets. Implement exponential backoff starting from that value:
HTTP/1.1 429 Too Many Requests
Retry-After: 37
Content-Type: application/json
{ "message": "Rate limit exceeded. Try again in 37 seconds." }Burst behaviour
Requests are counted on a rolling window, not a fixed clock boundary. A burst of requests within the same second counts against the per-minute limit. If you need higher throughput, contact taseenrayed@paykx.co.uk to discuss a higher-rate tier.
Retry Guidance
Not every error is worth retrying. Use the table below to decide whether to retry, fix credentials, or escalate — and how quickly.
| Status | Should you retry? | Strategy |
|---|---|---|
| 400 | No | Fix the request — required field missing or corridor invalid |
| 401 | No | Fix credentials — API key is missing or has been revoked |
| 403 | No | Access denied — contact PAYKX to check your key permissions |
| 409 | No | Idempotency conflict — reuse key with same body to get the cached result |
| 429 | Yes | Retry after the Retry-After header value (seconds) |
| 500 | Yes | Exponential backoff: 1 s → 2 s → 4 s → 8 s, then escalate |
| 503 | Yes | Retry after a short delay — service is recovering |
Exponential backoff pattern
async function verifyWithRetry(payload, apiKey, maxAttempts = 4) {
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const res = await fetch("https://api.paykx.co.uk/api/v1/verify", {
method: "POST",
headers: { "Authorization": "Bearer " + apiKey, "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (res.ok) return res.json();
if (res.status === 429) {
const wait = parseInt(res.headers.get("Retry-After") ?? "5", 10) * 1000;
await new Promise(r => setTimeout(r, wait));
} else if (res.status >= 500) {
await new Promise(r => setTimeout(r, Math.pow(2, attempt) * 1000));
} else {
throw Object.assign(new Error("PAYKX error " + res.status), { status: res.status });
}
}
throw new Error("Max retries exceeded");
}Request Traceability
Every PAYKX response includes fields and headers that let you correlate a decision back to the exact request that produced it — in your logs, in your UI, and in support escalations. Traceability is part of the Execution Assurance contract: if you acted on a PAYKX decision, you can always prove what the engine said and when.
Response fields
| Field | Type | Example | Purpose |
|---|---|---|---|
request_id | string | "paykx-req-a3f9c2" | Unique identifier for this verification. Log it alongside every payment operation — it's the primary key for support and audit queries. |
evaluated_at | ISO 8601 | "2026-07-19T11:42:00.000Z" | Exact timestamp the scoring engine produced the decision. Use this for audit trails and time-bounded replays. |
api_version | string | "1.2" | The scoring model version that produced this decision. If you later compare two decisions made under different model versions, this tells you which rules applied. |
decision_model | string | "v1.2" | Alias for the decision logic version — included for human-readable audit logs alongside the numeric api_version. |
correlation_id | string (optional) | "my-payment-ref-123" | Echoed back if you pass it on the request. Use it to tie a PAYKX decision to your own internal payment reference — no UUID required on your side. |
Response headers
| Header | Value | Purpose |
|---|---|---|
X-Request-ID | Same as request_id | Available in the HTTP response headers so you can correlate without parsing the body — useful for logging middleware. |
Passing a correlation ID
Include correlation_id in your request body to bind the PAYKX decision to your own transaction reference. It is echoed back on the response and stored with the verification record.
{
"corridor": "GB-NG",
"amount": 2500,
"currency": "GBP",
"correlation_id": "your-internal-payment-ref-abc123"
}
// Response includes:
{
"request_id": "paykx-req-a3f9c2",
"correlation_id": "your-internal-payment-ref-abc123",
"evaluated_at": "2026-07-19T11:42:00.000Z",
"api_version": "1.2",
"decision": "GO",
...
}Retrieving verification history
Every verification you run is stored and retrievable. Use your API key to query your own record set — cross-key access is not permitted.
GET /api/verifications Authorization: Bearer pk_test_xxxxx // Returns an array of your verification records including: // request_id, corridor, decision, score, confidence, evaluated_at, correlation_id
request_id, correlation_id, decision, score, reason_codes, and evaluated_at to your own ledger at the point of each PAYKX call. If a payment is later disputed, you have a timestamped, versioned record of the pre-execution assurance check that was performed.Decision Model
PAYKX evaluates seven independent signals across five risk dimensions. Every signal is weighted, the weights produce a composite score, and the score determines the decision band. There is no black-box model — every decision is auditable.
Seven signals — what each one measures
PAYKX does not expose proprietary scoring weights or raw data sources, but the category of inputs feeding each signal is documented here so you can reason about what a flag means and what to do about it.
fraud_flags
Weight: 30%
AML and fraud indicator scan
Evaluates transaction-level AML indicators, amount-pattern anomalies, counterparty risk typologies, and known fraud profile matches for the corridor. The signal is binary: zero flags (clear) or one or more flags detected.
When flagged: The reason code HIGH_FRAUD_SIGNAL is emitted. Block the transaction and escalate — do not retry the same payload.
network_healthy
Weight: 25%
Live rail and settlement network probe
A real-time probe of the settlement rail health for the requested corridor — network latency, congestion indicators, and partner settlement endpoint availability. Unhealthy state means a payment that might technically send could silently fail to settle.
When flagged: NETWORK_DEGRADED. Retry after a delay (see Retry Guidance) or reroute to an alternate rail.
balance_sufficient
Weight: 15%
Partner liquidity and balance check
Checks whether the selected corridor has sufficient liquidity headroom for the requested amount — based on partner reserve levels and recent settlement volumes. A transfer that enters a low-liquidity corridor may be delayed, reversed, or held pending rebalancing.
When flagged: INSUFFICIENT_BALANCE. Route to a different partner or retry later when reserves recover.
fx_volatility
Weight: 10%
Foreign exchange spread and rate volatility
Measures the current bid-ask spread and rate-of-change for the currency pair. Elevated volatility increases the gap between the rate quoted at initiation and the rate at settlement — widening slippage risk and increasing the probability of a failed payout if the rate moves outside corridor bounds.
When flagged: HIGH_FX_VOLATILITY. Proceed with caution or wait for a stable window; show the user an indicative rate with a tight expiry.
operation_valid
Weight: 10%
Transaction type and operation validity
Validates that the transaction type, operation category, and currency combination is supported and correctly structured for the target corridor. Some operation types are restricted for specific corridors — this signal surfaces that mismatch before any funds move.
When flagged: OPERATION_INVALID. Check the operation type field and resubmit — this is a data error, not a risk signal.
historical_failure_rate
Weight: 5%
Partner and corridor failure rate lookback
A rolling lookback of settlement outcomes for this partner and corridor combination. A rising or sustained failure rate is a leading indicator of coming execution problems — systems are often degrading before they visibly fail. This signal catches the trend early.
When flagged: HIGH_FAILURE_RATE. Consider an alternate route or a later time window.
cop_overridden
Weight: 5%
Confirmation of Payee and CoP override detection
Detects whether Confirmation of Payee (CoP) was bypassed in the payment initiation chain upstream of this call. CoP bypass increases the probability of account name mismatch, authorised push payment (APP) fraud, and regulatory non-compliance. This signal fires as a compliance flag, not a network signal.
When flagged: COP_OVERRIDE_ACTIVE. Require manual CoP confirmation before releasing the transfer.
Execution Uncertainty Score
The Execution Uncertainty Score (EUS) is a derived operator metric surfaced in Replay Analysis reports. It provides a single headline number summarising the concentration of DEGRADED and NO-GO decisions across an analysed transaction portfolio — giving operators an immediate orientation before reading the full report.
How it is calculated
EUS is computed directly from the replay results — it is not a separate model. The formula is:
EUS = round( min(100, no_go_pct × 3.5 + degraded_pct × 1.0) )
Where no_go_pct and degraded_pct are the percentage of transactions in each decision band from the replay. Both inputs come from the same PAYKX decisions already present in the report — EUS adds no new scoring logic.
Why NO-GO is weighted higher than DEGRADED
NO-GO decisions represent execution conditions severe enough for PAYKX to block a payment entirely. DEGRADED decisions indicate conditions that may warrant review but do not necessarily prevent execution. The 3.5× weight on NO-GO reflects that asymmetry — a portfolio with 10% NO-GO carries meaningfully more operational risk than one with 10% DEGRADED, and the score should reflect that difference.
Score levels and thresholds
| Band | Score range | Interpretation |
|---|---|---|
| Low | 0 – 20 | Relatively stable execution conditions. Most transactions are receiving GO decisions. |
| Moderate | 21 – 35 | Noticeable proportion of flagged decisions. Review DEGRADED transactions before release. |
| High | 36 – 60 | Significant execution uncertainty. A material share of the portfolio received DEGRADED or NO-GO decisions. |
| Critical | 61 – 100 | Severe execution uncertainty. Immediate operational review of corridor conditions is recommended. |
These thresholds are PAYKX guidance derived from the scoring formula. They will be calibrated against real partner replay data as design partners share historical transaction outcomes. Thresholds may shift as calibration data is incorporated.
What EUS is not
EUS is a summary statistic, not a predictive model output. It does not claim to predict future failure rates, guarantee operational outcomes, or replace analysis of the full report. Use it as an orientation metric — the opening number in a Shadow Diagnostic conversation — before reviewing decision distribution, corridor health, and top risk signals in detail.
Accessing EUS via API
EUS is returned automatically in every POST /api/v1/batch-assess response under execution_uncertainty_score. A standalone methodology reference is also available:
GET /api/v1/methodology
Returns the formula, band definitions, weighting rationale, and calibration status as structured JSON — suitable for embedding in operator dashboards or internal documentation.
Reason Codes
Every response includes a reasons array of objects (each with a code and a message) and a flat reason_codes array for backward-compatible consumers. Use these codes to drive downstream logic — retry routing, manual review queues, or user messaging. These codes are part of the API contract and will not be removed without a deprecation notice.
| Reason Code | Signal | What it means | Suggested action |
|---|---|---|---|
HEALTHY_PARTNER | — | All signals are clear; partner operating normally | Proceed — no elevated risk detected |
HIGH_FRAUD_SIGNAL | fraud_flags | AML or fraud indicators detected on this transaction | Block and escalate for manual review |
NETWORK_DEGRADED | network_healthy | Settlement rail or network health is below threshold | Retry after a delay or reroute to alternate rail |
INSUFFICIENT_BALANCE | balance_sufficient | Partner or corridor liquidity is too low | Route to a different partner or retry later |
HIGH_FX_VOLATILITY | fx_volatility | FX spread or rate volatility is elevated | Proceed with caution or wait for stable window |
OPERATION_INVALID | operation_valid | Transaction type not valid for this corridor | Check operation type and resubmit |
HIGH_FAILURE_RATE | historical_failure_rate | Recent failure rate on this corridor is elevated | Consider an alternate route or timing |
COP_OVERRIDE_ACTIVE | cop_overridden | Confirmation of Payee was bypassed | Require manual CoP confirmation before release |
CORRIDOR_UNSUPPORTED | — | Corridor is not yet in the validated set (GB-NG, NG-GB) | Treat as “no data” — do not use as a risk signal |
POLICY_CAP_APPLIED | — | Transfer amount exceeds an autonomous execution threshold | Route to manual settlement approval |
Transactions above £50,000 are subject to a policy cap that floors the verdict at DEGRADED. This reflects conservative execution policy for high-value transfers pending corridor-specific calibration.
Using reason codes in your application
Read reason_codes (flat array) or reasons (array of objects with code and message) to drive consistent user experiences. Build your routing logic around the code strings — they are stable API contract values and will not change without a deprecation notice.
const data = await res.json();
switch (data.decision) {
case "GO":
// Safe to execute — all signals clear
await executePayment(payload);
break;
case "DEGRADED":
if (data.reason_codes.includes("POLICY_CAP_APPLIED")) {
// High-value transfer needs manual approval
await queueForManualReview(payload, data.request_id);
} else if (data.reason_codes.includes("NETWORK_DEGRADED")) {
// Rail issue — retry in 30–60s
scheduleRetry(payload, 45_000);
} else if (data.reason_codes.includes("HIGH_FX_VOLATILITY")) {
// Show user a warning and ask to confirm at new rate
showFxWarning(data.reasons.find(r => r.code === "HIGH_FX_VOLATILITY").message);
} else {
await routeToPartnerB(payload);
}
break;
case "NO-GO":
if (data.reason_codes.includes("HIGH_FRAUD_SIGNAL")) {
// Hard block — do not allow retry
blockAndEscalate(payload, data.request_id);
} else {
// All other NO-GO — inform user and stop
showUserError("This transfer cannot proceed right now.");
}
break;
}data = resp.json()
decision = data["decision"]
codes = data["reason_codes"]
if decision == "GO":
execute_payment(payload)
elif decision == "DEGRADED":
if "POLICY_CAP_APPLIED" in codes:
queue_for_manual_review(payload, data["request_id"])
elif "NETWORK_DEGRADED" in codes:
schedule_retry(payload, delay_seconds=45)
elif "HIGH_FX_VOLATILITY" in codes:
show_fx_warning(next(r["message"] for r in data["reasons"]
if r["code"] == "HIGH_FX_VOLATILITY"))
else:
route_to_partner_b(payload)
elif decision == "NO-GO":
if "HIGH_FRAUD_SIGNAL" in codes:
block_and_escalate(payload, data["request_id"])
else:
show_user_error("This transfer cannot proceed right now.")decision. Reading reason_codes tells you why the decision was made — and therefore which action is correct. A DEGRADED because of HIGH_FX_VOLATILITY needs a different response than one caused by NETWORK_DEGRADED. The reason catalogue is the machine-readable explanation layer.Architecture
PAYKX sits in front of your existing payment flow as an independent decision step. It never touches funds. It only returns a decision you act on.
Request flow
A single POST /api/v1/verify call walks through the following steps inside PAYKX Rail before a decision is returned:
Bearer token extracted, hashed with SHA-256, and matched against approved key hashes. Simultaneously, the request body is validated against the typed Zod schema. Both must pass — a missing field or invalid key returns immediately without touching the scoring engine.
If an Idempotency-Key header is present, PAYKX checks whether that key has been seen before. If yes and the fingerprint matches, the cached decision is returned immediately — the scoring engine is not run again. A fingerprint mismatch returns 409 Conflict.
The requested corridor is normalised to a canonical form (e.g. GB-NG) and checked against the supported-corridor allowlist. If this corridor recently returned NO-GO, the 90-second cooldown window applies and the cached decision is returned without a recompute.
All seven signals are evaluated in parallel where dependencies permit. Each probe writes a numeric value (0.0–1.0) and a binary pass/fail into the probe details object.
Weighted signal values are summed into a composite score. If 3 or more signals are flagged, a 25% penalty is applied. The policy gate then checks the transfer amount against thresholds — amounts above £50,000 floor at DEGRADED; above £500,000 force NO-GO.
Score is mapped to GO (≥0.80), DEGRADED (0.48–0.79), or NO-GO (<0.48). Flagged signals are translated into reason codes, a recommended_action is derived for DEGRADED outcomes, and a recommended_partner is selected if applicable.
The verification record is written to the database with a unique request_id. The response is serialised and returned, with X-Request-ID set in the response headers. Total wall time: typically 60–150 ms.
Decision pipeline
Request body
↓ Zod schema validation
↓ Auth: SHA-256(token) vs approved hash set
↓ Idempotency: key seen? → return cached result
↓ Corridor: normalise → cooldown check
↓ Signal probes (7 parallel evaluations)
fraud_flags network_healthy balance_sufficient
fx_volatility operation_valid historical_failure_rate cop_overridden
↓ Weighted sum → composite score [0.00 – 1.00]
↓ Multi-flag penalty (3+ flagged → −25%)
↓ Policy gate (amount thresholds)
↓ Decision band: ≥0.80 GO | 0.48–0.79 DEGRADED | <0.48 NO-GO
↓ Reason codes derived from flagged signals
↓ recommended_action (DEGRADED only)
↓ Write verification record → return responseLatency
| Percentile | Response time | Notes |
|---|---|---|
| Median (p50) | ~80 ms | Full seven-signal evaluation with all signals returning cached or fast values |
| p95 | ~150 ms | Includes live network probe with moderate latency |
| p99 | <250 ms | Worst-case with live partner probe and elevated network latency |
| Idempotent hit | <20 ms | Cached result returned — scoring engine not invoked |
| Cooldown hit | <10 ms | Corridor in NO-GO cooldown — cached decision returned from DB |
The response_ms field on every response gives you the actual server-side processing time for that specific call. Use it to monitor your own p95 and p99 over time.
Audit trail
Every PAYKX response is an immutable, timestamped record. The following fields together form a complete audit entry for any pre-execution assurance check:
| Field | What it records |
|---|---|
request_id | Unique identifier for this specific verification call |
evaluated_at | ISO 8601 timestamp — exact moment the scoring engine produced the decision |
api_version | Scoring model version — tells you which rules were in effect |
decision | The engine's verdict: GO, DEGRADED, or NO-GO |
score | The composite score that produced the decision (0.00–1.00) |
confidence | How well-supported the decision is given signal quality |
reason_codes | Which signals were flagged — the explainable basis for the decision |
probe_details | Per-signal raw values — full transparency, signal by signal |
correlation_id | Your reference (if passed) — ties the PAYKX record to your payment ledger |
mode | shadow or live — which environment produced the decision |
Enterprise Integration Guide
A production integration follows five deterministic steps. Each step maps to a PAYKX API call or an action in your own system.
Request a production key via the access form. Sandbox keys are issued instantly for integration testing. Production keys are reviewed manually and issued within 1 business day.
POST /api/v1/request-access { "name": "Acme Payments", "email": "eng@acme.com", "reason": "..." } → Your key is sent by email. Store it in your secrets manager — never in code.
Before initiating any transfer, call /api/v1/verify with the corridor, amount, and currency. Pass an Idempotency-Key so safe retries never re-run the scoring engine.
POST /api/v1/verify
Authorization: Bearer <your-key>
Idempotency-Key: <your-payment-uuid>
{ "corridor": "GB-NG", "amount": 5000, "currency": "GBP" }The response arrives in under 200 ms and contains the decision, composite score, confidence, signal breakdown, and reason codes. Map it to your payment flow logic.
{ "decision": "GO", "score": 0.87, "confidence": 0.94, "request_id": "req_...", "reason_codes": [] } { "decision": "DEGRADED", "score": 0.61, "confidence": 0.72, "reason_codes": ["HIGH_FX_VOLATILITY"] } { "decision": "NO-GO", "score": 0.31, "confidence": 0.88, "reason_codes": ["FRAUD_FLAG_ACTIVE", "NETWORK_DEGRADED"] }
Your system acts on the decision. If a compliance officer needs to override a DEGRADED or NO-GO result, record the override through the API — this creates a permanent, auditable link between the original decision and the human action.
// GO: proceed immediately if (decision === 'GO') initiateTransfer(payment); // DEGRADED: queue for review, or override if approved if (decision === 'DEGRADED') holdForReview(payment); // Record a compliance override if approved manually POST /api/v1/override { "request_id": "PAYKX-2026-622", "action": "EXECUTE", "reason_code": "COMPLIANCE_APPROVED", "justification": "..." } // NO-GO: block and do not proceed if (decision === 'NO-GO') preventExecution(payment);
Retrieve the full audit trail — original decision, all 7 signal readings, and any override — with a single call. Store or surface this to your compliance team as evidence of pre-execution due diligence.
GET /api/v1/audit/PAYKX-2026-{id}
Authorization: Bearer <your-key>
→ Returns decision + all signals + override (if any) in one response.
→ Immutable. Write-once. Cannot be modified after creation.PAYKX-{year}-{id} IDs are auditable.
The req_… format returned by /api/v1/demo-verify and /api/v1/demo-compare is ephemeral — those calls are never persisted and will return 404 here.
POST /api/v1/webhooks) to receive real-time push events for payment.decision.created and payment.override.created — so your system reacts without polling.
Decision Lifecycle
Every payment intent flows through a fixed lifecycle. PAYKX produces the decision — your system acts on it — and the audit record closes the loop.
Reading the diagram
Score ≥ 0.80. All critical signals clear. Proceed immediately. No hold required.
Score 0.48–0.79. One or more signals elevated. Queue for review or route via a lower-risk rail. A compliance officer may issue an override to proceed.
Score < 0.48. Multiple signals flagged or policy cap breached. Do not execute. Investigate the flagged signals before retrying.
POST /api/v1/override stores the justification and links it permanently to the original decision. The audit record returned by GET /api/v1/audit/… then contains both the original PAYKX decision and the human override — a complete, tamper-evident chain of custody.
Webhook Retry Policy
PAYKX guarantees at-least-once delivery for every webhook event. If your endpoint does not return a 2xx response, the platform retries automatically on a fixed backoff schedule.
Retry schedule
| Attempt | Delay after previous failure | Cumulative wait |
|---|---|---|
1 | Immediate | — |
2 | 1 minute | 1 min |
3 | 5 minutes | 6 min |
4 | 15 minutes | 21 min |
5 | 30 minutes | 51 min |
After 5 failed attempts the event is dropped. Events are not replayed beyond the retry window.
Event envelope fields
Every delivery includes metadata fields to support idempotent processing:
{
"event": "payment.override.created",
"event_id": "evt_3f8a2c1d9e7b4051",
"created_at": "2026-07-20T14:23:01.000Z",
"attempt": 1,
"data": { ... }
}
- event_id — stable across all retry attempts for the same event. Use this to deduplicate on your side.
- attempt — integer starting at
1. Increments with each retry so you can log delivery history. - X-PAYKX-Attempt — also sent as an HTTP header on every delivery request.
- X-PAYKX-Signature — HMAC-SHA256 of
secret + body. Recompute this on every attempt to verify authenticity before processing.
Responding correctly
Your endpoint must return HTTP 2xx within 5 seconds. Return any other status code — or let the connection time out — and PAYKX will schedule a retry.
event_id, your handler should check whether an event has already been processed before taking action — for example, by storing processed event IDs in a database.
Response headers on all enterprise endpoints
Every enterprise API endpoint also returns two environment headers for integration diagnostics:
X-PAYKX-Version: v1 X-PAYKX-Environment: sandbox
API Key Permissions
Today every API key grants full access to all authenticated endpoints. The permissions model below is designed for enterprise buyers who need fine-grained role separation — for example, allowing a treasury team to read decisions but not submit overrides.
Planned permission scopes
| Scope | Grants access to | Typical role |
|---|---|---|
decision:read |
POST /api/v1/verify, GET /api/v1/decision/:id/explanation |
Treasury analyst, risk monitor |
audit:read |
GET /api/v1/audit/:id, GET /api/v1/audit/export/:id |
Compliance officer, auditor |
override:create |
POST /api/v1/override |
Treasury manager, compliance lead |
webhook:manage |
POST /api/v1/webhooks, GET /api/v1/webhooks, DELETE /api/v1/webhooks/:id |
Platform engineer, integration owner |
Future API key schema
When permissions are introduced, API key metadata returned from the access-request flow will include a permissions array:
{
"api_key": "pk_live_...",
"issued_at": "2026-07-20T00:00:00Z",
"permissions": [
"decision:read",
"audit:read",
"override:create",
"webhook:manage"
]
}
Requests made with a key that lacks the required scope will receive a 403 Forbidden response with a required_scope field identifying which permission is missing.
PAYKX vs Payment Orchestration
The single most common question from bank and fintech buyers. PAYKX and payment orchestration layers solve adjacent problems — they are designed to work together, not replace each other.
Orchestration layers (e.g. Spreedly, Paydock, Volt) route a confirmed payment intent to the cheapest, fastest, or most available rail — SWIFT, SEPA, CHAPS, Faster Payments, local schemes. They assume the payment should go ahead and optimise execution.
- Rail selection & fallback routing
- Fee optimisation across providers
- Retry on rail failure
- Provider SLA monitoring
- Conversion rate uplift
PAYKX is a pre-execution intelligence layer. Before any rail is selected or any funds move, PAYKX evaluates corridor health, FX volatility, fraud signals, and network conditions to return a GO / DEGRADED / NO-GO decision with a confidence score.
- Corridor risk scoring (7 signals)
- GO / DEGRADED / NO-GO decision
- FX volatility & liquidity gate
- Fraud & AML signal aggregation
- Compliance-grade audit trail
How they fit together in a payment flow
Capability comparison
| Capability | Payment Orchestration | PAYKX |
|---|---|---|
| Rail selection & routing | ✓ | — |
| Provider failover | ✓ | — |
| Corridor risk scoring | — | ✓ |
| FX volatility gate | — | ✓ |
| Fraud / AML signal aggregation | — | ✓ |
| GO / NO-GO pre-execution gate | — | ✓ |
| Compliance override & audit chain | — | ✓ |
| Works with any orchestration layer | — | ✓ |
PAYKX and Fraud, AML & Compliance Layers
PAYKX does not evaluate whether a customer is legitimate. It evaluates whether the payment route is ready to execute.
Fraud detection, AML, and KYC systems analyse customer behaviour, identity, and transaction risk. They answer:
"Should this transaction from this customer be approved?"
PAYKX analyses payment execution conditions before funds are committed. It answers:
"Is this corridor and payment partner reliable enough to execute through right now?"
These layers are complementary.
PAYKX does not replace:
- KYC
- AML screening
- fraud detection
- transaction monitoring
PAYKX operates upstream as an execution assurance layer.
Security & Compliance
A complete reference for the security controls, data handling practices, and compliance assurances that enterprise and regulated buyers need to evaluate PAYKX for production use.
🔒 Authentication
All authenticated API endpoints require a Bearer token in the Authorization header. Tokens are long random strings (minimum 128 bits of entropy) issued through the access request flow.
Authorization: Bearer pk_live_<your-key>
- Keys are hashed (SHA-256) before storage — the plaintext key is never persisted.
- The plaintext key is returned once only at issuance. Store it in a secrets manager (e.g. AWS Secrets Manager, HashiCorp Vault, GCP Secret Manager).
- Keys have no expiry by default in sandbox. Production keys will support configurable TTL and rotation policies.
- Admin operations (key issuance, access approval) require a separate
X-Admin-Keyheader with a distinct secret — no API key can perform admin actions.
🔐 Encryption
All API traffic is TLS 1.2+ encrypted end-to-end. HTTP connections are not accepted in production — all requests are redirected to HTTPS. Certificate management is handled by the platform (auto-renewed).
The PostgreSQL database is encrypted at rest using AES-256. API key secrets are SHA-256 hashed — PAYKX cannot recover a key once issued. No payment instrument data (card numbers, IBANs) is stored at any point.
📄 Audit Retention
Every verification decision is stored as a write-once immutable record. Records are never modified or deleted in response to API calls.
| Record type | Retained for | Accessible via |
|---|---|---|
| Verification decisions | 7 years (planned) | GET /api/v1/audit/:id |
| Compliance overrides | 7 years (planned) | GET /api/v1/audit/:id → override |
| Compliance export reports | Generated on demand | GET /api/v1/audit/export/:id |
| Access request submissions | Duration of relationship | Admin panel only |
| Webhook delivery logs | 30 days (planned) | Admin panel (roadmap) |
Retention periods are set to align with FCA and EBA record-keeping requirements for payment service providers. Contact us if your jurisdiction requires a different retention window.
🔑 API Key Management
- Never embed keys in frontend code. API keys must only be used from server-side code where they cannot be extracted by browser inspection or public repos.
- Use environment variables or a secrets manager. Do not hardcode keys in source files. Treat them like database passwords.
- Rotate keys if compromised. Email access@paykx.co.uk to revoke a key immediately. Revocation takes effect within 60 seconds.
- Use one key per environment. Maintain separate keys for development, staging, and production. Never use a production key in test environments.
- Scope keys by role (roadmap). See the API Key Permissions section for the planned granular permission model.
🔗 Webhook Security
Every webhook delivery is signed with HMAC-SHA256. Your endpoint should verify the signature before processing the payload — this prevents spoofed events from triggering actions in your system.
// Node.js — verify incoming webhook signature
import { createHash } from "crypto";
function verifyWebhook(secret: string, rawBody: string, signature: string): boolean {
const expected = "sha256=" + createHash("sha256").update(secret + rawBody).digest("hex");
return expected === signature;
}
// In your handler:
const sig = req.headers["x-paykx-signature"];
const body = req.body; // raw string — do not parse before verifying
if (!verifyWebhook(process.env.PAYKX_WEBHOOK_SECRET, body, sig)) {
return res.status(401).send("Invalid signature");
}
- The
X-PAYKX-Signatureheader is present on every delivery attempt, including retries. - Use a constant-time comparison (not
===) to prevent timing attacks — most crypto libraries provide this. - Store the webhook secret in your secrets manager alongside your API key.
- Acknowledge with
200 OKimmediately — do heavy processing asynchronously to avoid timeouts triggering retries. - See the Webhook Retry Policy for the full retry schedule and deduplication guidance.
👤 Data Handling
- Corridor identifier (e.g. GB-NG)
- Transfer amount & currency
- Decision outcome & score
- Signal risk values (no raw signal data)
- Override records with actor identity
- API key hash (not plaintext)
- Sender or recipient personal details
- IBANs, account numbers, sort codes
- Card numbers or payment instruments
- IP addresses in decision records
- Plaintext API keys
- Any biometric or KYC data
Use Cases
Where a deterministic pre-transaction decision adds the most value.
Remittance
Screen cross-border consumer transfers for corridor risk before release, cutting failed and reversed payments.
Treasury
Add a corridor-health gate to bulk settlement runs so large transfers escalate for review automatically.
Agentic Payments
Give autonomous agents a safe, idempotent GO/NO-GO check before they move money on a user's behalf.
Status
Live health of the PAYKX API, checked from your browser right now.
Corridors fully validated: GB-NG, NG-GB.
How PAYKX helps payment teams scale
This section is for payment operations, treasury, and compliance leads — not engineers. It explains what changes when PAYKX is in the stack.
Before PAYKX
Payment teams managing cross-border execution typically rely on:
- Provider dashboards — checked manually, updated on delay, one per partner
- Failure queues — discovered after settlement, not before execution
- Spreadsheets — corridor performance tracked offline, not in the payment path
- Escalations — when a payment fails, the first signal is a customer complaint
The result: execution decisions are made without real-time signal data. Risk is discovered retrospectively.
After PAYKX
One API call before each payment returns:
- Execution confidence — a score reflecting current corridor and partner health, not historical averages
- Reason codes — machine-readable flags with plain-English business explanations, readable by compliance and treasury teams
- Audit evidence — every decision is logged with probe-level detail, exportable for regulatory review
- Operational recommendations — the API tells the operator what to do, not just what the signals say: proceed, reroute, request KYC, or hold
What this replaces
| Without PAYKX | With PAYKX |
|---|---|
| Check provider dashboard before sending | POST /api/v1/verify — live seven-signal response in <300ms |
| Discover failure after settlement | NO-GO decision before funds are committed |
| Manual rerouting investigation | suggested_path and recommended_action: REROUTE returned inline |
| Offline spreadsheet corridor analysis | POST /api/v1/replay/analyse — historical transaction dataset scored against current model |
| Audit trail assembled after the fact | Every decision stored with probe-level evidence via GET /api/v1/decisions/:id/audit |
Audience reference
- Payments operations — use
/verifyto gate execution; userecommended_actionto automate rerouting decisions - Treasury — use
operational_implicationsto understand settlement and liquidity exposure before funds move - Compliance — use
/auditand/explanationto produce decision evidence for regulatory review - Engineering — integrate via REST; see Quickstart and API Reference
Design Partner Data Flow
Bring PAYKX a payment execution question. We replay your historical evidence to show what PAYKX would have flagged and compare those signals with observed outcomes — each stage is discrete, reversible, and does not affect live payments.
What question can a replay answer?
Start with a payment execution question, such as “where did manual intervention start?”, “which payments later failed?”, or “what risks were visible before a delay?”. Then provide the historical evidence needed to test it:
- Corridor — source and destination country (e.g.
GB-NG) - Amount and currency — transaction value
- Outcome (optional but useful) — whether the payment succeeded or failed, so alignment can be measured
Customer names, account numbers, customer IDs, and other personal data are not required and must be removed. After your request, PAYKX provides a CSV template and secure upload instructions — do not send raw payment files by email.
How long does each stage take?
| Stage | Typical timeline | What you see |
|---|---|---|
| Replay analysis | Timeline agreed after dataset acceptance and capacity confirmation | Decision intelligence report |
| Shadow evaluation | 2–4 weeks | Live alignment data; no operational change |
| Production readiness | Operator decision | Active gating; overrides available |
Each stage has a clear exit condition. You can stop at any point — nothing is irreversible until you choose to move to active gating.
Start here
Request a limited shadow replay → — tell us your intended corridor and sample size. We'll send a CSV template and secure upload instructions within one business day; the delivery timeline is agreed only after PAYKX confirms the dataset is usable and assigns capacity.
Changelog
Notable changes to the PAYKX API and developer docs.
- Added ISO 20022 payment message parsing — decisions now reference ISO message type in audit record
- Added decision confidence endpoint —
/api/v1/decisions/searchwith score band filtering - Added webhook signing and replay tools — HMAC-SHA256 signatures,
/api/v1/webhooks/replay/:id - Health endpoint extended with
signals,execution_mode,capabilities, andlimitationsblocks - Navigation unified across all pages; Design Partner programme page added
- Added audit retrieval — full probe-level evidence per decision via
/api/v1/decisions/:id/audit - Added sandbox scenarios — 12 pre-built corridor test cases with expected outcomes
- Added model versioning —
model_versionfield on every response;/api/v1/model/changelogendpoint - Security hardening: CAPTCHA fail-closed, API keys stored as SHA-256 hashes, 128-bit entropy
- Developer hub: Go + cURL SDK examples, OpenAPI YAML download, Status page
- Seven-signal scoring engine with deterministic GO / DEGRADED / NO-GO decision bands
- Corridor cooldowns and honest DEGRADED handling for unsupported corridors
- Idempotency key support — safe retries with fingerprint binding
- Public demo endpoint with no authentication required