> ## Documentation Index
> Fetch the complete documentation index at: https://docs.shoppex.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Complete or process manual-payment invoices

> Complete or process storefront manual-payment invoices after your external gateway confirms payment

## 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 is the API equivalent of the Dashboard’s **Process Invoice** action.

<Info>
  This pattern works for **storefront and payment-link invoices**, not only for `POST /dev/v1/payments`. Use `POST /dev/v1/invoices/{uniqid}/complete`.
</Info>

## End-to-end flow

```mermaid theme={"system"}
sequenceDiagram
  participant Buyer
  participant Shoppex
  participant YourGateway
  participant YourServer

  Buyer->>Shoppex: Select manual gateway + confirm sent
  Shoppex->>YourServer: order:manual_payment_pending (optional)
  Buyer->>YourGateway: Pays (bank, local PSP, etc.)
  YourGateway->>YourServer: Provider webhook
  YourServer->>Shoppex: POST /dev/v1/invoices/{uniqid}/complete
  Shoppex->>YourServer: order:paid + fulfillment
```

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`**).

<Warning>
  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.
</Warning>

***

## Prerequisites

| Requirement    | Details                                                                                                |
| -------------- | ------------------------------------------------------------------------------------------------------ |
| API key        | Dashboard → **Settings → Developer API**. Scope: `invoices.write` (or `*`).                            |
| Invoice status | `PENDING` or `PENDING_PAYMENT`                                                                         |
| Invoice ID     | UUID from checkout URL (`/invoice/{uniqid}`) or `order:manual_payment_pending` payload (`data.uniqid`) |

***

## Complete an invoice (minimal)

<CodeGroup>
  ```bash cURL theme={"system"}
  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
    }'
  ```

  ```typescript TypeScript (@shoppexio/sdk) theme={"system"}
  import { Shoppex } from '@shoppexio/sdk';

  const shoppex = new Shoppex({ apiKey: process.env.SHOPPEX_API_KEY! });

  await shoppex.invoices.complete(invoiceUniqid, {
    note: 'Paid via my local gateway',
  });
  ```

  ```python Python theme={"system"}
  import os
  import requests

  response = requests.post(
      f"https://api.shoppex.io/dev/v1/invoices/{invoice_uniqid}/complete",
      headers={
          "Authorization": f"Bearer {os.environ['SHOPPEX_API_KEY']}",
          "Content-Type": "application/json",
          "Idempotency-Key": f"complete-{invoice_uniqid}",
      },
      json={"note": "Paid via my local gateway"},
      timeout=30,
  )
  response.raise_for_status()
  ```
</CodeGroup>

**Success:** `200` with a stable completion result:

```json theme={"system"}
{
  "data": {
    "uniqid": "33333333-3333-4333-8333-333333333333",
    "status": "COMPLETED",
    "message": "Invoice completed successfully."
  }
}
```

Call `GET /dev/v1/invoices/{uniqid}` afterwards if you need the current full invoice representation.

**Common errors:**

| HTTP  | Meaning                                                   |
| ----- | --------------------------------------------------------- |
| `401` | Invalid or missing API key                                |
| `404` | Invoice not found for your shop                           |
| `422` | Invoice already `COMPLETED`, or status is not completable |

The **`Idempotency-Key`** header is required so retries after network drops do not double-apply side effects. The Shoppex SDK adds a deterministic key automatically when you do not pass one.

***

## 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](/guides/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):**

```json theme={"system"}
{
  "invoice_id": "4ea04c92-5cc3-4ea8-845c-cd3c7085796c",
  "status": "paid",
  "amount": "49.99",
  "currency": "USD"
}
```

```typescript worker.ts theme={"system"}
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.

<Tip>
  For redirect manual gateways, include `{{invoice_id}}` in the external checkout URL query string so your PSP webhook can return the same ID.
</Tip>

***

## 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.

***

## Related docs

<CardGroup cols={2}>
  <Card title="Payments guide" icon="credit-card" href="/guides/payments">
    Hosted checkout and Developer API overview
  </Card>

  <Card title="Webhooks" icon="webhook" href="/guides/webhooks">
    Verify `order:manual_payment_pending` and `order:paid`
  </Card>

  <Card title="Developer API" icon="code" href="/api-reference/introduction">
    Full API reference including `POST /dev/v1/invoices/{uniqid}/complete`
  </Card>

  <Card title="Payment gateways" icon="gear" href="/merchants/payments/gateways">
    Connect Stripe, PayPal, Cash App, and manual methods
  </Card>
</CardGroup>
