API reference — v1
Base URL https://gatewaychanger.com/api/v1. JSON only. Authenticate with Authorization: Bearer <key>.
This reference mirrors the platform specification. The API is being implemented alongside these docs during early access; the changelog records what has landed.
Authentication
Two kinds of key:
- Shop keys —
shk_test_…/shk_live_…. One per shop, used by the plugins, scoped to that shop and its environment. - Organization keys —
sk_test_…/sk_live_…. For automation, scoped to one environment, with areadorwriterole.
The dashboard itself uses session authentication on /app/* and not this API, though it calls the same services.
Conventions
- Amounts are integer minor units;
currencyis an upper-case ISO-4217 code. - Cursor pagination:
?limit=50&cursor=…returns{"data": […], "next_cursor": "…"}. - Every
POSThonoursIdempotency-Key: the same key replays the same response for 24 hours, and the same key with a different body returns409. - Errors:
{
"error": {
"type": "validation|authentication|permission|not_found|conflict|rate_limit|no_route|server",
"code": "…",
"message": "…",
"param": "…"
}
}
- Rate limit: 600 requests per minute per key, answered with
429and aRetry-Afterheader. - Every response carries
X-Request-Id. - Public identifiers are prefixed:
org_,shp_,rul_,dec_,txn_,tev_(transaction event),whe_(webhook endpoint),key_,usr_.
Plugin-facing endpoints
These are called by the store plugins with a shop key.
POST /shops/self/activate
Registers the shop, its public key and its capabilities.
{
"platform": "woocommerce",
"plugin_version": "1.0.0",
"base_url": "https://shop-a.example",
"api_base": "https://shop-a.example/wp-json/gwc/v1",
"public_key": "<base64url 32 bytes>",
"previous_public_key": null,
"roles": ["seller", "charger"],
"capabilities": { "processors": ["test", "stripe"], "currencies": ["EUR", "USD"] },
"admin_email": "[email protected]"
}
Responds with {shop, environment, saas_public_keys, consent_template, heartbeat_interval} where
environment is test or live and heartbeat_interval is 300 seconds.
The platform then probes GET {api_base}/capabilities in a background job with a 10-second timeout and sets
verified_at when the returned shop_id and public_key match. Until then the shop is status: "pending_verification"
and cannot act as a charger.
POST /shops/self/heartbeat
{
"plugin_version": "1.0.0",
"roles": ["seller", "charger"],
"capabilities": { "processors": ["test"], "currencies": ["EUR"] },
"status": "online",
"metrics": { "pending_callbacks": 0, "last_error": null }
}
Responds {"ok": true, "status": "online", "next_heartbeat_in": 300}. A shop with no heartbeat for more than 15
minutes is marked offline, excluded from routing, and raises an alert.
GET /shops/self
The caller's own shop record, its environment, the organization name and information about the key in use.
GET /shops/{id}/public-key
{shop_id, public_key, previous_public_key?, api_base, base_url} — only for shops in the caller's organization and
environment. Used by a peer to verify a signature when it has no pinned key yet.
POST /route
The routing decision. The seller's key implies seller_shop_id.
{
"amount": 2499,
"currency": "USD",
"country": "DE",
"shipping_country": "DE",
"customer": { "type": "guest", "email_hash": "sha256:…", "locale": "de_DE" },
"items": [
{ "sku": "GWC-WIDGET", "name": "Test Widget", "qty": 1, "amount": 1999, "categories": ["widgets"], "tags": [] }
],
"metadata": { "wc_order_key": "…", "campaign": "summer" },
"exclude_shops": [],
"previous_decision_id": null,
"mode_hint": "embed"
}
200 OK:
{
"decision_id": "dec_…",
"environment": "test",
"rule": { "id": "rul_…", "name": "Default" },
"candidate": {
"shop": { "id": "shp_B", "name": "Shop B", "legal_name": "Shop B Ltd", "platform": "woocommerce",
"base_url": "…", "api_base": "…", "public_key": "…" },
"processor": { "id": "stripe", "label": "Stripe", "features": ["embed", "redirect"], "capture": "auto" }
},
"alternatives": [ { "shop": {}, "processor": {} } ],
"grant": "v1.…",
"grant_expires_at": "2026-09-16T10:30:00Z",
"consent": {
"text": "I agree that my payment of $24.99 is processed by Shop B Ltd on behalf of Shop A GmbH.",
"version": "2026-09-16",
"tos_url": "https://shop-a.example/terms"
},
"trace": [
{ "rule": "rul_1", "matched": false, "reason": "currency" },
{ "rule": "rul_2", "matched": true,
"candidates": [
{ "shop": "shp_B", "processor": "stripe", "available": true },
{ "shop": "shp_C", "processor": "test", "available": false, "reason": "offline" }
] }
]
}
When nothing is available the call returns 404 with {"error": {"type": "no_route", "message": "…", "trace": […]}}.
Decisions expire after 30 minutes. Their status moves issued → used when a charge is reported, and then to
completed, failed or expired.
POST /decisions/{id}/events
Telemetry from the seller: {"type": "session_created|consent_accepted|charge_requested|charge_failed|failed_over|abandoned", "data": {}}
→ 202.
POST /transactions
Reported by the charger, which owns the processor truth. Idempotent on the pair (charger shop, charge_id).
{
"decision_id": "dec_…",
"session_id": "ses_…",
"charge_id": "chg_…",
"seller_shop_id": "shp_A…",
"seller_order_ref": "1042",
"charger_order_ref": "#1234",
"processor": "stripe",
"processor_ref": "pi_3Q1…",
"amount": 2499,
"currency": "EUR",
"status": "captured",
"captured_amount": 2499,
"card": { "brand": "visa", "last4": "4242", "country": "DE", "funding": "credit" },
"customer": { "country": "DE", "email_hash": "sha256:…" },
"failure": null,
"occurred_at": "2026-09-16T10:05:12Z"
}
→ 201 {"transaction": {"id": "txn_…", …}}.
POST /transactions/{id}/events
{"type": "captured|voided|refunded|refund_failed|dispute_opened|dispute_won|dispute_lost|failed", "amount": 1000, "processor_ref": "…", "reason": "…", "occurred_at": "…"}
→ 201 {event, transaction} with the transaction's status and amounts recomputed.
GET /transactions/{id} · GET /transactions?decision_id=…
The transaction with its events, and lookup by decision for the seller side.
Automation endpoints
Called with an organization key; the dashboard mirrors all of them.
Shops
GET/POST /shops · GET/PATCH/DELETE /shops/{id} with the fields name, legal_name, platform, base_url,
roles, enabled, notes, attestation_accepted_at · POST /shops/{id}/keys/rotate (issues a new shop key; the old
one stays valid for 24 hours) · POST /shops/{id}/probe (re-run verification).
Rules
GET/POST /rules · GET/PUT/DELETE /rules/{id} · POST /rules/reorder {"ids": […]} ·
POST /rules/simulate — takes the same body as /route plus seller_shop_id and returns the same shape with
"simulated": true, persisting nothing.
Decisions and transactions
GET /decisions?status=&seller=&charger=&from=&to=&q= · GET /decisions/{id} ·
GET /transactions?…&q= where q matches references, the last four digits and order numbers ·
GET /transactions/export.csv?….
Settlements
GET /settlements?period=2026-09 returns, per charger, seller and currency: gross, refunds, disputes, net and count ·
GET /settlements/{period}/export.csv.
Webhooks, events and keys
GET/POST /webhooks · DELETE /webhooks/{id} · POST /webhooks/{id}/test · GET /webhooks/{id}/deliveries ·
GET /events?type=&from= for the audit and activity log · GET /keys · POST /keys · DELETE /keys/{id}.
Outgoing webhooks
Organization-level. Events:
decision.issued · decision.failed_over · transaction.created · transaction.captured · transaction.failed ·
transaction.refunded · transaction.disputed · shop.online · shop.offline · shop.verified · cap.reached
Payload:
{ "id": "evt_…", "type": "transaction.captured", "created_at": "…", "environment": "test", "data": { } }
Signature header:
GWC-Webhook-Signature: t=<unix ts>,v1=<hex hmac_sha256(secret, ts + "." + body)>
Deliveries are retried with backoff and are visible — with their responses — in the dashboard, where they can be replayed.