Skip to content
GatewayChanger

How it works

One checkout, two shops, one signed decision

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.

How a payment is routed: the seller asks GatewayChanger who charges the order, GatewayChanger answers with a charger shop, a processor and a signed grant, the seller talks directly to the charger shop, and the charger charges the card with its own processor. Customer checks out at Shop A Seller — Shop A asks: who charges this order? GatewayChanger answers: Shop B, Stripe, + signed grant · no card data Shop A → Shop B directly Charger — Shop B serves the card form in an iframe, charges with its own account Processor Shop B’s Stripe account Card data goes browser → Shop B → processor. It never reaches GatewayChanger.
One decision per order. The seller shop keeps the customer relationship; the charger shop keeps the processor relationship; GatewayChanger keeps the rules, the audit trail and the ledger.

The four participants

01

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.

02

GatewayChanger

Evaluates your rules and answers one question: which shop charges this order, with which processor. Signs the answer, stores the trace, keeps the ledger.

03

Charger — Shop B

Serves the card form, charges with its own processor account, creates a mirror order for its own bookkeeping, owns refunds and disputes.

04

Processor

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.

The walk-through

Step 1

The seller asks who should charge

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.

POST /api/v1/route
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

We decide, and we sign the decision

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.

  • Conditions: amount range, currency, billing and shipping country, item categories, tags, metadata
  • Candidates: an ordered list of shop + processor pairs, including “the seller charges itself”
  • Strategies: first available, round robin, or weighted split across candidates
  • Every evaluation is stored as a trace — which rule matched, which candidate was skipped and why
grant payload (decoded) Ed25519
{
  "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

The customer is told who charges them

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

Place order

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 shops talk directly — and prove who they are

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.

POST {charger}/gwc/v1/sessions
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 charge, and the mirror order

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.

POST {charger}/gwc/v1/sessions/{id}/charge
// 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

One ledger, and what one shop owes another

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.

Settlement — September 2026
Example settlement report between charger and seller shops
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.

When something goes wrong

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.

Declines — never failed over

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.

Technical failures — re-routed

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.

Failover between charger shops A matched rule lists three candidates in order. The first is skipped because the shop is offline, the second because its daily cap is reached, and the third — the seller charging itself — is available, so the decision and the signed grant are issued for it. RULE MATCHED — “EU cards over €50” 1 Shop B · Stripe Offline — no heartbeat for 18 minutes → skipped 2 Shop C · Stripe Daily cap reached (€20,000 of €20,000) → skipped 3 Shop A charges itself · test processor Online, within caps, supports EUR → chosen Decision issued dec_01J8YR… · grant valid 30 min bound to amount, currency, charger Full trace stored for every attempt A technical failure during the charge — timeout, 5xx, processor outage — lets the seller re-route with the failed shop excluded. A card decline never fails over.
Candidates are evaluated in the order your rule defines them. Availability comes from heartbeats, processor capabilities and caps — not from guesswork.

Three ways to take the payment

Embed

Default

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.

Redirect

Universal fallback

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.

Self

Same code path

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.

Test and live never touch

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 →
shk_test_…
Test shop key. Routes only to test shops. The built-in test processor gives deterministic outcomes per card number.
shk_live_…
Live shop key. Requires a verified charger shop and an accepted card-scheme attestation before it can charge.

Ready to route an order?

The sandbox is free, and the test processor needs no credentials at all.