Skip to content
GatewayChanger

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 keysshk_test_… / shk_live_…. One per shop, used by the plugins, scoped to that shop and its environment.
  • Organization keyssk_test_… / sk_live_…. For automation, scoped to one environment, with a read or write role.

The dashboard itself uses session authentication on /app/* and not this API, though it calls the same services.

Conventions

  • Amounts are integer minor units; currency is an upper-case ISO-4217 code.
  • Cursor pagination: ?limit=50&cursor=… returns {"data": […], "next_cursor": "…"}.
  • Every POST honours Idempotency-Key: the same key replays the same response for 24 hours, and the same key with a different body returns 409.
  • 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 429 and a Retry-After header.
  • 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.