Skip to main content

Overview

Shoppex custom manual gateways (Settings → Payments → Manual) support payment instructions or buyer redirects with template variables such as {{amount}}, {{currency}}, and {{invoice_id}}. There is no built-in inbound IPN URL on manual gateways today. When your own payment provider confirms a transfer, your server should call the Developer API to complete the invoice.
This pattern works for storefront and payment-link invoices, not only for POST /dev/v1/payments. Use POST /dev/v1/invoices/{uniqid}/complete.

End-to-end flow

  1. Buyer starts manual checkout → invoice moves to PENDING_PAYMENT / awaiting merchant confirmation.
  2. Shoppex can notify you with order:manual_payment_pending (configure under Settings → Webhooks).
  3. Your external gateway sends its own webhook when money arrives.
  4. Your adapter calls POST /dev/v1/invoices/{uniqid}/complete with your Shoppex API key.
  5. Shoppex runs the normal completion pipeline (delivery, emails, order:paid).
Always verify the provider webhook on your server before calling Shoppex. Never expose your shx_* API key to the browser or to the payment provider’s client-side SDK.

Prerequisites

RequirementDetails
API keyDashboard → Settings → Developer API. Scope: invoices.write (or *).
Invoice statusPENDING or PENDING_PAYMENT
Invoice IDUUID from checkout URL (/invoice/{uniqid}) or order:manual_payment_pending payload (data.uniqid)

Complete an invoice (minimal)

curl -X POST "https://api.shoppex.io/dev/v1/invoices/INVOICE_UUID/complete" \
  -H "Authorization: Bearer shx_your_api_key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: complete-INVOICE_UUID" \
  -d '{
    "note": "Paid via my local gateway",
    "suppress_emails": false
  }'
Success: 200 with updated invoice data or { "message": "Invoice completed successfully." }. Common errors:
HTTPMeaning
401Invalid or missing API key
404Invoice not found for your shop
422Invoice already COMPLETED, or status is not completable
Use an Idempotency-Key header so retries after network drops do not double-apply side effects.

Listen for order:manual_payment_pending

Register a shop webhook (Settings → Webhooks) for order:manual_payment_pending. When a buyer submits manual payment instructions, Shoppex POSTs a signed payload to your URL. Extract the invoice UUID from data.uniqid and store it with your gateway’s pending-payment record if needed. Verify signatures with X-Shoppex-Signature-V2 — see Webhooks.

Cloudflare Worker adapter (copy-paste)

This worker receives a generic JSON webhook from your payment provider, optionally checks a shared secret, then completes the Shoppex invoice. Worker secrets (Wrangler):
  • SHOPPEX_API_KEY — your shx_* key with invoices.write
  • PROVIDER_WEBHOOK_SECRET — shared secret your gateway sends (header or HMAC); omit checks only in local dev
Expected provider JSON shape (customize to your PSP):
{
  "invoice_id": "4ea04c92-5cc3-4ea8-845c-cd3c7085796c",
  "status": "paid",
  "amount": "49.99",
  "currency": "USD"
}
worker.ts
interface ProviderWebhook {
  invoice_id?: string;
  status?: string;
  amount?: string | number;
  currency?: string;
}

const SHOPPEX_API = 'https://api.shoppex.io';

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    if (request.method !== 'POST') {
      return new Response('Method Not Allowed', { status: 405 });
    }

    const sharedSecret = request.headers.get('x-provider-secret');
    if (!env.PROVIDER_WEBHOOK_SECRET || sharedSecret !== env.PROVIDER_WEBHOOK_SECRET) {
      return new Response('Unauthorized', { status: 401 });
    }

    let payload: ProviderWebhook;
    try {
      payload = await request.json() as ProviderWebhook;
    } catch {
      return new Response('Bad Request', { status: 400 });
    }

    const invoiceUniqid = payload.invoice_id?.trim().toLowerCase();
    if (!invoiceUniqid) {
      return new Response('Missing invoice_id', { status: 400 });
    }

    if (payload.status?.toLowerCase() !== 'paid') {
      return new Response('Ignored', { status: 200 });
    }

    const completeResponse = await fetch(
      `${SHOPPEX_API}/dev/v1/invoices/${invoiceUniqid}/complete`,
      {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${env.SHOPPEX_API_KEY}`,
          'Content-Type': 'application/json',
          'Idempotency-Key': `complete-${invoiceUniqid}`,
        },
        body: JSON.stringify({
          note: `Auto-completed from provider webhook (amount=${payload.amount ?? 'n/a'} ${payload.currency ?? ''})`.trim(),
        }),
      },
    );

    if (completeResponse.status === 422) {
      const body = await completeResponse.text();
      if (/already completed/i.test(body)) {
        return new Response('Already completed', { status: 200 });
      }
      return new Response(body, { status: 422 });
    }

    if (!completeResponse.ok) {
      const body = await completeResponse.text();
      return new Response(body, { status: 502 });
    }

    return new Response('OK', { status: 200 });
  },
};

interface Env {
  SHOPPEX_API_KEY: string;
  PROVIDER_WEBHOOK_SECRET: string;
}
Deploy with Wrangler, then point your gateway’s webhook URL at the worker. Pass {{invoice_id}} in your manual gateway redirect URL or instructions so the provider echoes it back.
For redirect manual gateways, include {{invoice_id}} in the external checkout URL query string so your PSP webhook can return the same ID.

Redirect manual gateway example

Redirect URL in dashboard:
https://pay.example.com/checkout?amount={{amount}}&currency={{currency}}&reference={{invoice_id}}&email={{customer_email}}
When reference comes back on the provider webhook, map it to invoice_id in the worker above.

Security checklist

  • Verify provider signatures or shared secrets before calling Shoppex.
  • Optionally re-fetch the invoice with GET /dev/v1/invoices/{uniqid} and compare total / currency to the provider amount before completing.
  • Keep shx_* keys server-side only.
  • Use Idempotency-Key on every completion call.
  • Return 200 to your provider only after Shoppex accepts the completion (or the invoice is already completed).

Manual confirmation in the dashboard

If you do not automate completion, open the invoice in the dashboard when status is Awaiting Manual Confirmation, then Confirm Payment → Process Invoice. The Dev API complete call runs the same backend pipeline.

Payments guide

Hosted checkout and Developer API overview

Webhooks

Verify order:manual_payment_pending and order:paid

Developer API

Full API reference including POST /dev/v1/invoices/{uniqid}/complete

Payment gateways

Connect Stripe, PayPal, Cash App, and manual methods