Seller — Shop A
Sells the goods, owns the customer relationship and the order. Shows the consent checkbox. Never sees card data: the form is an iframe from Shop B.
How it works
This page follows a single order from the moment a customer reaches the payment step in Shop A to the moment the transaction appears in your ledger — including what happens when something fails.
Sells the goods, owns the customer relationship and the order. Shows the consent checkbox. Never sees card data: the form is an iframe from Shop B.
Evaluates your rules and answers one question: which shop charges this order, with which processor. Signs the answer, stores the trace, keeps the ledger.
Serves the card form, charges with its own processor account, creates a mirror order for its own bookkeeping, owns refunds and disputes.
Shop B’s own Stripe account (or the built-in test processor). Its credentials stay in Shop B; we never hold them and never call it.
Step 1
As soon as the payment step renders, the plugin calls POST /api/v1/route
with the cart total, the currency, the billing country and the line items. The seller's shop key
identifies the shop and the environment, so the request carries no secrets beyond the bearer key.
If the total changes later — a coupon, a shipping method, a tax recalculation — the plugin re-routes on finalize and the session is updated with a fresh grant. A grant is always bound to one amount and currency, so a changed total can never be charged against an old decision.
Authorization: Bearer shk_test_…
Idempotency-Key: 9f1c…e2
{
"amount": 2499,
"currency": "EUR",
"country": "DE",
"shipping_country": "DE",
"customer": { "type": "guest", "locale": "de_DE" },
"items": [
{ "sku": "GWC-WIDGET", "qty": 1,
"amount": 2499, "categories": ["widgets"] }
],
"exclude_shops": [],
"mode_hint": "embed"
}
Step 2
Rules are an ordered list. The first rule whose conditions match wins; its candidate shops and processors are then filtered by what is actually possible right now — is the shop online, does the processor support this currency and country, is the daily or monthly cap still open, is the shop verified as a charger.
The answer includes a grant: a compact token signed with our
Ed25519 key that names the seller, the charger, the processor, the amount, the currency and an
expiry thirty minutes out. Shops verify it against our public key, published at
/.well-known/gatewaychanger.json.
{
"kid": "gwc-2026-09",
"jti": "dec_01J8YR4K…",
"iat": 1758000000, "exp": 1758001800,
"env": "test",
"seller": { "id": "shp_A…", "legal_name": "Shop A GmbH" },
"charger": { "id": "shp_B…", "legal_name": "Shop B Ltd" },
"processor": "stripe",
"amount": 2499, "currency": "EUR",
"capture": "auto",
"rule": { "name": "EU cards over €50" }
}
A charger refuses a grant that is expired, that names another shop, that belongs to another environment, or whose amount does not match the session.
Step 3
A customer must never be surprised by the name on their statement. The route response contains the consent sentence, with the real legal names and the real amount filled in. The plugin shows it as a required checkbox next to the card form.
When the box is ticked, the accepted text, its SHA-256 hash, the timestamp and the IP address are stored on the seller's order and again on the charger's mirror order. If anyone ever asks what the customer agreed to, the answer is on both sides of the transaction.
Consent and shop terms →Shop A checkout — payment step
I agree that my payment of €24.99 is processed by Shop B Ltd on behalf of Shop A GmbH. See terms.
Card form — iframe served by Shop B
Illustration of the checkout the plugin renders. The two shaded fields live inside the charger's iframe; the seller's page cannot read them.
Step 4
The seller opens a session on the charger with the grant attached, and the charger creates the processor state it needs — for Stripe, a PaymentIntent. The reply contains the embed URL for the card form, or a redirect URL for the hosted pay page.
Every request between shops is signed with the caller's Ed25519 key over the timestamp, a nonce, the method, the path and a hash of the exact body. Replays are rejected for ten minutes; clocks more than five minutes apart are rejected outright. Responses are signed too, so the seller can detect tampering in either direction.
GWC-Shop-Id: shp_A…
GWC-Timestamp: 1758000123
GWC-Nonce: s9Qk7…
GWC-Signature: v1=MEUCIQD…
// signing string
timestamp \n nonce \n POST \n /gwc/v1/sessions
\n sha256_hex(raw_body)
// 201 Created
{
"session_id": "ses_01J8…",
"embed_url": "https://shop-b.example/?gwc-embed=ses_…",
"expires_at": "2026-09-16T10:30:00Z"
}
Step 5
The card is confirmed inside the iframe — including any 3-D Secure challenge — and the charger mints a single-use payment token. The seller redeems it with the charge call, which is idempotent: a retried request replays the stored answer for 24 hours instead of charging twice.
The charger creates a mirror order in its own store — one line labelled with the seller and the seller's order number — so its accounting, its VAT reporting and its refunds all work the way that shop already works.
// 200 OK
{
"charge_id": "chg_01J8…",
"status": "captured",
"processor": "stripe",
"processor_ref": "pi_3Q1…",
"charger_order_ref": "#1234",
"card": { "brand": "visa", "last4": "4242" },
"amount": 2499, "captured_amount": 2499,
"transaction_id": "txn_01J8…"
}
brand and last4 are the only card
attributes in the whole response — and the only ones we store.
Step 6
The charger reports the outcome to your ledger, and every later event — capture, void, refund, dispute opened, dispute won or lost — is appended to the same transaction. Both order references travel with it, so a support agent in either shop can find the other side in one search.
Settlement reports roll the ledger up per charger, seller and currency for a period: gross, refunds, disputes, net and count. That is the number one company invoices the other — we never hold or move the money ourselves.
| Charger | Seller | Ccy | Gross | Refunds | Net |
|---|---|---|---|---|---|
| Shop B Ltd | Shop A GmbH | EUR | 48,210.00 | −1,240.00 | 46,970.00 |
| Shop B Ltd | Shop C SRL | EUR | 12,880.50 | −310.00 | 12,570.50 |
| Shop A GmbH | Shop C SRL | USD | 9,415.00 | −0.00 | 9,415.00 |
Illustrative figures.
The most important distinction in the whole system: a card decline is the customer's problem to solve with another card, and a technical failure is ours to route around. Confusing the two is how orchestration platforms double-charge people.
declined, insufficient_funds,
incorrect_cvc, expired_card: the iframe
resets, the customer sees a specific message and tries another card. Routing to a second shop would
just get the same answer from the same scheme.
Timeouts, 5xx, processor_error,
unavailable: the seller asks for a new decision with the failed shop
excluded and tries the next candidate — at most twice, with the whole trail written to the
decision.
The charger serves a minimal card page inside an iframe on the seller's checkout. The customer never leaves Shop A. Frame ancestors are locked to the seller origin and both sides verify every postMessage.
The customer is sent to the charger's hosted pay page and returns with a charger-signed result token, backed by a signed server-to-server callback. Works everywhere, including Shopify.
A rule can pick the seller itself as the charger, using its own processor accounts. Nothing special happens: the same session, the same grant, the same ledger entry.
Every organization has two environments. Shops, keys, rules, decisions and transactions belong to exactly one of them, and a test shop can only ever route to another test shop. The key prefix tells the plugin where it is, and the plugin shows a TEST MODE badge in the store admin so nobody has to guess.
Getting started →The sandbox is free, and the test processor needs no credentials at all.