Skip to main content
External payment adapters let you use a regional or specialist provider without waiting for a native Shoppex integration. Your adapter creates the provider checkout, receives the provider webhook, and reports the result back to the exact Shoppex payment attempt.
An adapter is merchant-operated. Its signature proves that the event came from your configured adapter, not that Shoppex independently observed settlement. Adapter payments are marked merchant-attested, are excluded from trusted GMV, and do not support automatic refunds. They do collect Shoppex platform fees, like payments through a natively supported provider — the completing event is signed, bound to one attempt, and checked against its amount. Manual payment methods and “mark as paid” stay fee-free.

When to use an adapter

Use an external adapter when the provider has:
  • an API that creates a hosted checkout session;
  • a signed server-to-server payment webhook; and
  • a stable provider payment or session reference.
Use a manual payment method instead when you only need instructions, a static redirect, or human confirmation.

How the flow works

Shoppex creates the attempt before the first request. This makes attempt_id the idempotency key and prevents a delayed Worker response from replacing a newer buyer attempt.

Start from the Hono Worker

The Shoppex repository includes a production-shaped starter at workers/external-payment-adapter-starter. From the Shoppex repository root, install the workspace dependencies:
If you extract the starter into another repository, replace the workspace:* dependency on @shoppex/contracts with the released package version that contains this contract. The starter has four small pieces:
  • POST /sessions verifies Shoppex and creates the provider checkout;
  • POST /provider/webhook verifies your provider and loads the attempt mapping from D1; and
  • a Queue consumer signs and retries the event delivery to Shoppex; and
  • POST /.well-known/shoppex-payment-adapter answers Shoppex’s signed, non-mutating conformance probes.
Hono is only the HTTP router. You can implement the same contract with Elysia, Fastify, Go, or another Worker-compatible runtime.

Create Cloudflare resources

Create one D1 database, one delivery Queue, and its dead-letter Queue:
Copy the returned D1 ID into wrangler.jsonc, then apply the included migration:
The D1 row stores only the binding required after the redirect: provider_reference, attempt_id, event_url, exact amount, and currency.

Adapt the provider calls

In src/index.ts, replace the example fetch(PROVIDER_SESSION_URL, ...) body with your provider’s create-session API. Keep these rules:
  1. Send attempt_id as the provider idempotency key or metadata.
  2. Store the provider’s stable reference with the Shoppex event URL.
  3. Return an HTTPS checkout_url.
  4. Verify the provider webhook over its raw request body before parsing it.
  5. Read the settled amount and currency from the verified provider event and require both to match the D1 session before notifying Shoppex. Never copy the expected D1 amount into the event as if the provider observed it.
  6. Map only final provider settlement to payment.succeeded. Do not treat a browser return URL as proof of payment.
The example provider response expected by the starter is:
The example webhook shape is:
Replace its x-provider-signature HMAC verifier with the provider’s official verification algorithm. Preserve the provider’s stable event ID: the Worker uses it as the Shoppex webhook-id, so provider redelivery and Queue retries deduplicate to the same payment decision. Do not weaken or remove verification in production. If delivery exhausts its ten retries, Cloudflare moves the message to shoppex-external-payment-events-dlq for investigation and replay; it is not silently discarded.

Deploy and configure secrets

Add the provider credentials first:
Copy the deployed session URL, for example: https://shoppex-external-payment-adapter.example.workers.dev/sessions. In Shoppex, open Settings → Payments → External, select Add adapter, and enter that URL. Shoppex shows a whsec_... shared secret once. Save it in the Worker:
After the secret is deployed, select Test on the adapter. Shoppex derives the conformance URL from the session URL, so you do not configure a second URL. Enable the adapter only after all six contract checks pass and you have tested the real provider in its sandbox.

Signed Shoppex contract

Both directions use the Standard Webhooks header format:
The signed string is:
The session request includes exact integer minor units:
For example, 4999 EUR means €49.99. Never convert this through a floating point amount in the Worker. Shoppex generates the real event_url; your adapter must store and call it unchanged.

Test before going live

In Settings → Payments → External, select Test next to the adapter. Shoppex sends signed challenges to /.well-known/shoppex-payment-adapter and checks:
  1. the versioned request and challenge response;
  2. rejection of a changed Shoppex signature;
  3. signatures and attempt binding on event samples;
  4. detection of a wrong amount;
  5. one stable event identity across a duplicate delivery; and
  6. Shoppex’s 500 ms timeout deadline.
This test is safe to run on a configured adapter: it does not create a provider checkout, call the provider, create a Shoppex payment attempt, or touch an invoice. A green result proves that the Worker speaks the Shoppex contract. It does not prove that the provider API mapping or provider webhook verifier is correct. Run the starter checks:
Then test these failure cases against a sandbox provider:
  1. changed Shoppex body with the original signature → 401;
  2. provider webhook with a wrong signature → 401;
  3. unknown provider reference → 404;
  4. duplicate successful webhook → one completed Shoppex order;
  5. Shoppex event endpoint temporarily returns 500 → Queue retries, then moves an exhausted delivery to the dead-letter Queue;
  6. buyer starts a second attempt before the first session returns → the late first response cannot become current; and
  7. wrong provider amount or currency → Worker returns 409, sends no event, and the invoice remains unpaid.

Restrict an adapter to specific currencies

Many regional providers settle in one currency only. In the adapter’s settings, Accepted Currencies defaults to All currencies; select individual codes to restrict it. A restricted adapter is hidden at checkout for any other currency, and createSession refuses it server-side with a 400. Your Worker therefore never receives a session request in a currency you excluded, and does not need its own currency gate for this — keep the amount and currency check against the provider response, which guards a different failure.

Session claims and quarantined attempts

The starter Worker claims each Shoppex attempt in D1 before it calls your provider, and only the request that wins that insert calls it. A repeat of the same signed request replays the stored session; a different payload for the same attempt is refused with 409. If the provider call ends without a usable answer — timeout, network error, 5xx, or a 2xx body the Worker cannot parse — the claim is not released. None of those outcomes proves the provider created nothing, and retrying could open a second checkout the buyer might also pay. The row is marked status = 'failed_unknown' and further requests for that attempt answer 409. Only an allowlisted validation rejection (400, 401, 403, 404, 422) releases the claim, because those happen before a session exists. Every other non-2xx — redirects, 408, 409, 425, 429 — is treated as an unknown outcome and quarantined too.

Recovering a quarantined attempt

Check your provider’s dashboard for a session bound to that shoppex_attempt_id before clearing anything. Deleting the row without checking is what lets a second provider session appear.
  • A session does exist: complete the row instead of deleting it, so the stored reference matches what the provider will send events for.
  • No session exists: delete the row. Shoppex’s next attempt then claims cleanly.
The buyer is never stuck meanwhile: Shoppex can start a new payment attempt, which carries a new attempt_id and therefore its own claim.

Version-one limits

  • one-time payments only;
  • no partial or overpayments;
  • no Shoppex-initiated refund, dispute, or subscription operations;
  • no shared-secret rotation overlap; create a new adapter for a planned key replacement; and
  • public HTTPS endpoints only, with no redirects to private networks.
These limits fail closed. Add a versioned contract extension when your provider needs a new lifecycle instead of inferring missing values.