LIVE · v04.2.1 routing.engine = ✓ 18/18 PSPs p95 = 240ms · auth = 98.7% EUR · UTC · cluster eu-west-3
~/console $ open route

DEVELOPERS · REST API · webhooks · 8 SDKs

One API replaces 18. Drop-in SDK in your stack.

If you've integrated Stripe, you can integrate Whaliepay in an afternoon — the API shape is intentionally familiar. The difference is what happens after the POST: cascading retries across PSPs, BIN-level routing, FX optimisation, all behind a single endpoint.

v1stable API since 2022
8official SDKs
15webhook event types
60 req/sdefault rate limit
// quick start

POST your first routed transaction in 4 minutes

Grab a sandbox API key from the console (free, no card required), then run the snippet in your language of choice. The routing engine sandbox replies exactly like production — same fields, same headers, same webhooks.

POST /v1/payments · cURL
# 1. Sandbox key from the console export WH_KEY="sk_test_4a91f3c2b7e8d9..." # 2. POST your first payment curl https://api.whaliepay.com/v1/payments \ -H "Authorization: Bearer $WH_KEY" \ -H "Content-Type: application/json" \ -d '{ "amount": 34200, "currency": "EUR", "payment_method": "pm_card_test_4242", "customer": "cus_test_4a91", "routing": { "strategy": "smart", "retry_depth": 3, "exclude": [] }, "metadata": { "order_id": "order_7B2A", "merchant_ref": "halcyon-12340" } }'
POST /v1/payments · Node.js
// npm install whaliepay import Whaliepay from "whaliepay"; const wh = new Whaliepay(process.env.WH_KEY); const payment = await wh.payments.create({ amount: 34200, currency: "EUR", payment_method: "pm_card_test_4242", customer: "cus_test_4a91", routing: { strategy: "smart", retry_depth: 3, exclude: [] }, metadata: { order_id: "order_7B2A" } }); console.log(payment.id, payment.status, payment.routed_via); // → pay_2a91f3 · captured · stripe-eu
POST /v1/payments · Python
# pip install whaliepay import os from whaliepay import Whaliepay wh = Whaliepay(os.environ["WH_KEY"]) payment = wh.payments.create( amount=34200, currency="EUR", payment_method="pm_card_test_4242", customer="cus_test_4a91", routing={ "strategy": "smart", "retry_depth": 3, }, metadata={"order_id": "order_7B2A"}, ) print(payment.id, payment.status, payment.routed_via) # → pay_2a91f3 captured stripe-eu
POST /v1/payments · Go
// go get github.com/whaliepay/whaliepay-go package main import ( "context" "fmt" "os" whaliepay "github.com/whaliepay/whaliepay-go" ) func main() { wh := whaliepay.New(os.Getenv("WH_KEY")) pay, _ := wh.Payments.Create(context.Background(), &whaliepay.PaymentCreate{ Amount: 34200, Currency: "EUR", PaymentMethod: "pm_card_test_4242", Customer: "cus_test_4a91", Routing: &whaliepay.Routing{Strategy: "smart", RetryDepth: 3}, }) fmt.Println(pay.ID, pay.Status, pay.RoutedVia) }

The sandbox returns the same response shape as production. Test card 4242 4242 4242 4242 always authorises, 4000 0000 0000 0002 always declines, 4000 0000 0000 9995 triggers a 3DS challenge. Full list of triggers in /developers#sandbox.

// REST API

Core endpoints — payments, refunds, payouts, customers

All endpoints accept and return JSON. Authentication is via bearer token (sk_live_… or sk_test_…). Idempotency is supported on every POST via the Idempotency-Key header. OpenAPI spec at api.whaliepay.com/openapi.json.

# Payments

POST/v1/payments

Create a routed payment. The engine evaluates rules and signals, forwards to the best rail. Returns the routed_via PSP, the auth status, and a payment.id for subsequent operations.

GET/v1/payments/{id}

Retrieve a payment by ID. Returns the latest state, the rail used, attempt history, decline reasons (if any), and the unified ledger entry id.

GET/v1/payments?limit=&cursor=&status=

List payments with cursor-based pagination. Filter by status, customer, date range, currency, or routed_via PSP. Default limit 25, max 100 per page.

POST/v1/payments/{id}/capture

Capture a previously authorised payment. Partial capture is supported by passing the amount field — useful for ship-and-bill workflows.

POST/v1/payments/{id}/cancel

Cancel an uncaptured authorisation. Forwards the void to the routing PSP, releases the hold on the card.

# Refunds

POST/v1/refunds

Refund a captured payment. Full or partial. Always routed back through the original PSP — the routing engine does not split refunds.

GET/v1/refunds/{id}

Retrieve a refund by ID. Returns processing status, expected settlement date, and the associated ledger entry.

# Payouts

POST/v1/payouts

Trigger an out-of-cycle payout to your settlement bank. Optional currency and amount; defaults to the full balance.

GET/v1/payouts?status=

List payouts. Filter by status (pending, in_transit, paid, failed). Includes ETA per the PSP's settlement window.

# Customers & payment methods

POST/v1/customers

Create a customer. Stored once in our vault, addressable across all PSPs — no need to duplicate the record per provider.

GET/v1/customers/{id}

Retrieve a customer. Returns payment methods, default method, email, billing details, and the routing history per customer.

PUT/v1/customers/{id}

Update a customer. Propagates to the relevant PSPs that hold the customer record (Stripe, Adyen, etc.).

POST/v1/payment_methods

Vault a payment method. Returns a pm_… token usable on every connected PSP. Underlying card is never re-collected.

DELETE/v1/payment_methods/{id}

Delete a vaulted method. Revokes the cross-PSP token; the underlying network token is detached on the next sync.

# Webhooks

POST/v1/webhooks

Register a webhook endpoint. Pick the events you care about, set the signing secret in the console.

GET/v1/webhook_deliveries?event_id=

List webhook deliveries. See every retry, the response code from your endpoint, and the payload that was sent. 30-day retention.

// official SDKs

8 official SDKs · maintained, type-safe, semantically versioned

Each SDK is a thin, type-safe wrapper over the REST API. Generated from the OpenAPI spec, idiomatic in each language, semantically versioned. Open-source on GitHub under Apache 2.0.

JS

Node.js

Stable v3.2.1 · TypeScript first · zero runtime deps · works in Edge/Workers runtimes.

npm install whaliepay
PY

Python

Stable v3.2.0 · sync + async clients · Pydantic-typed responses · Python 3.10+.

pip install whaliepay
PHP

PHP

Stable v3.1.4 · PSR-7/PSR-18 · works with Symfony 6/7 and Laravel 10/11 out of the box.

composer require whaliepay/whaliepay
RB

Ruby

Stable v3.1.2 · works with Rails 7+ · ActiveSupport integration for hashes & times.

gem install whaliepay
GO

Go

Stable v3.0.8 · idiomatic · context-aware · Go 1.21+.

go get github.com/whaliepay/whaliepay-go
RS

Rust

Beta v0.9.2 · async via tokio · serde for serialisation · Rust 1.74+.

cargo add whaliepay
NET

.NET

Stable v3.1.0 · .NET 8+ · System.Text.Json · async-first.

dotnet add package Whaliepay
JV

Java

Stable v3.0.6 · Java 17+ · works with Spring 6 · Maven Central + Gradle plugin.

implementation "com.whaliepay:whaliepay:3.0.6"
// webhooks

15 event types, signed with HMAC-SHA-256, retried up to 24h

Every state change in the routing engine emits a webhook. Signed with HMAC-SHA-256, with timestamp anti-replay. Retried with exponential back-off for up to 24 hours if your endpoint returns non-2xx.

payment.created
A new payment was created in the engine, before routing.
payment.routed
The engine forwarded the payment to the selected PSP, with the route metadata.
payment.succeeded
Payment authorised + captured. Includes the routed_via, the auth code, the total latency.
payment.authorized
Payment authorised but not captured. Sent for two-step flows.
payment.captured
A previously authorised payment was captured.
payment.failed
All retries failed. Includes the decline taxonomy reason + the attempted PSPs.
payment.retried
A retry was triggered. Includes the previous PSP, the reason, and the next PSP.
payment.canceled
An uncaptured auth was canceled.
refund.created
A refund was initiated. Includes target PSP, amount, ledger entry id.
refund.succeeded
A refund was processed end-to-end by the original PSP.
dispute.opened
A chargeback / dispute was opened by the issuer. Whaliepay forwards it from the routed PSP.
dispute.evidence_submitted
You submitted evidence on a dispute. Sent when the PSP confirms receipt.
payout.created
A payout was initiated to your settlement bank.
payout.paid
A payout was settled to your bank account. Includes ledger close info.
psp.degraded
A connected PSP fell below our health threshold and was paused in routing.
payment.succeeded · payload sample
{ "id": "evt_3a91f3c2b7", "type": "payment.succeeded", "created_at": "2026-05-20T15:02:14Z", "livemode": true, "data": { "id": "pay_2a91f3", "amount": 34200, "currency": "EUR", "status": "succeeded", "routed_via": "stripe-eu", "latency_ms": 142, "attempts": 1, "auth_code": "7A2B91", "customer": "cus_4a91", "payment_method": "pm_card_xxx_4242", "ledger_entry": "le_5d3c9a", "metadata": { "order_id": "order_7B2A" } } }
// sandbox

Sandbox · isolated test environment + scenario triggers

Every Whaliepay account has a parallel test environment. Sandbox keys start with sk_test_, can never charge a real card, and behave like the live engine — same shape, same routing, same webhook signatures.

CARDS

Test card numbers

  • 4242 4242 4242 4242 — always authorises
  • 4000 0000 0000 0002 — always declines (hard)
  • 4000 0000 0000 9995 — triggers 3DS challenge
  • 4000 0000 0000 9987 — declines on first PSP, second authorises (retry test)
  • 4100 0000 0000 0019 — fraud-flag decline
  • 4000 0027 6000 3184 — funds insufficient (soft)
SCENE

Scenario triggers

  • Pass x-whaliepay-scenario: psp_degraded — simulate a PSP being paused mid-routing
  • Pass x-whaliepay-scenario: webhook_delayed — webhooks fire 30s later
  • Pass x-whaliepay-scenario: dispute_inbound — fires dispute.opened 5 minutes later
  • Pass x-whaliepay-scenario: rate_limit — 429 response immediately
// migration guides

Moving from a single PSP — three concrete guides

If you've integrated one of these PSPs, switching to Whaliepay is mostly a base-URL change + a rename or two. We've built a side-by-side migration guide for each.

Moving from Stripe

payment_intent_id → payment.id. pm_… → pm_… (same shape). webhook signatures are HMAC-SHA-256. Average migration: 1-2 engineer-days, validated in shadow mode for 48h before flipping.

Moving from Adyen

pspReference → routed_via. Adyen webhook XML → JSON. Risk profile mapping → our fraud taxonomy. Average migration: 3-5 engineer-days for full coverage.

Moving from Checkout.com

cko_… → pay_… IDs. Source vs. instrument → unified payment_method. cko Sessions → our 3DS-via-routing. Average migration: 2-3 engineer-days.

// policies

Idempotency, retries and rate limits

Three policies you'll touch the most. All published and unchanged except via versioned, deprecation-noticed releases.

IDM

Idempotency

Every POST accepts the Idempotency-Key header. Re-sending the same key within 24h returns the original response. Use a UUID per logical operation — order id, refund id, payout request.

RTY

SDK retry policy

Each official SDK retries idempotent requests up to 3 times with exponential back-off (200ms, 800ms, 2.4s). On 429, retries respect the Retry-After header. On 5xx, retries respect server hints.

LIM

Rate limits

Default 60 req/s per merchant on the production API, 10 req/s on sandbox. Enterprise customers can request a higher tier; we never silently throttle without warning.

Want to skim the API hands-on? Grab our Postman collection or the OpenAPI spec.

Postman collection OpenAPI 3.1 spec SDK changelogs

Build the integration once. Route through eighteen rails.

Sandbox keys are free. The first 100k routed transactions on a Growth plan are routed for free if you sign at the end of a routing audit.