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

# Payments

> Accept payments through the API, payment links, or your storefront

There are three ways to accept payments with Shoppex.

<CardGroup cols={3}>
  <Card title="Storefront" icon="store">
    Customers browse your shop and check out directly
  </Card>

  <Card title="Payment Links" icon="link">
    Share a link that takes customers straight to checkout
  </Card>

  <Card title="API" icon="code">
    Create invoices programmatically from your app
  </Card>
</CardGroup>

<Note>
  This guide covers both hosted/public Shoppex flows (storefront, payment links) and Developer API flows (`POST /dev/v1/payments`, `POST /dev/v1/orders`).
</Note>

***

## The payment flow

<Note>
  You do not need to integrate with Stripe or PayPal directly. Shoppex handles all gateway communication for you.
</Note>

## Important integration rule

One Shoppex invoice can have more than one payment attempt over time. A customer can open checkout, try PayPal, abandon it, come back, and finish with Stripe. This surprises most people integrating for the first time.

Redirect the customer to the Shoppex checkout URL, listen for webhook events, and fulfill based on the final invoice status. Do not build your fulfillment logic around a single provider-side session ID.

***

## Method 1: Storefront (hosted, public)

Your storefront at `yourshop.shoppex.io` is ready by default:

1. Customer browses products
2. Adds to cart
3. Completes checkout
4. Receives product automatically

**Best for:** Digital products, subscriptions, simple e-commerce

***

## Method 2: Payment Links (hosted flow)

Create a link that goes directly to checkout. Share it on social media, in emails, or anywhere else.

### Create through the dashboard

1. Go to **Products → Your Product**
2. Click **Copy Payment Link**
3. Share the URL: `https://yourshop.shoppex.io/product/your-product`

### Create through the API

```typescript theme={"system"}
const response = await fetch('https://api.shoppex.io/dev/v1/payments', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer YOUR_API_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    title: 'Pro License',
    email: 'customer@example.com',
    value: 49.99,
    currency: 'USD',
    webhook: 'https://your-site.com/api/shoppex/payment-webhook'
  })
});

const { data } = await response.json();
console.log(data.url); // https://checkout.shoppex.io/invoice/4ea04c92-5cc3-4ea8-845c-cd3c7085796c
console.log(data.webhook_secret); // returned once when webhook is provided
```

If you pass `webhook`, Shoppex sends payment lifecycle events only for that created payment or invoice to that URL. Store `webhook_secret` immediately. Shoppex does not return it on later reads.

The `webhook` value must be your own HTTP(S) endpoint. Discord webhook URLs are not compatible with Shoppex developer-event payloads. Configure Discord notifications under **Notifications → Sales Alerts** instead.

**Best for:** Social media sales, email campaigns, one-off payments

***

## Method 3: API integration (Developer API)

Use the Developer API when you need your own checkout UI or a backend-driven payment flow.

### Payment vs order

`POST /dev/v1/payments` and `POST /dev/v1/orders` look similar, but they solve different problems.

Use **`POST /dev/v1/payments`** when you have your own checkout and want a hosted payment URL. You do not get Shoppex catalog line items or automatic product delivery.

Use **`POST /dev/v1/orders`** when you want to charge for real Shoppex products. It includes line items, variants, and automatic delivery (files, serials, subscriptions, or `DYNAMIC`).

The quick rule: `payments` = generic developer payment. `orders` = catalog-backed Shoppex order.

<Warning>
  **Subscriptions, serials, files, and DYNAMIC delivery always require `POST /dev/v1/orders`.**

  `POST /dev/v1/payments` creates a one-off developer invoice with a free-form title and amount. It does not link to catalog products, does not create subscription records, and does not appear in the **Subscriptions** dashboard tab.

  A common integration mistake: load a product from `GET /dev/v1/products`, then call `POST /dev/v1/payments` with that product's title and price. That disconnects checkout from the catalog. Pass the product `uniqid` in `POST /dev/v1/orders` instead.

  The API returns `422` when `POST /dev/v1/payments` receives catalog fields such as `product_id` or `items`.

  Recurring-billing custom fields such as `plan_type` still create the payment, but the `201` response includes a `warnings` array with code `subscription_checkout_mismatch` pointing you to `POST /dev/v1/orders`.
</Warning>

### Check which payment methods are enabled

Use `GET /dev/v1/me/capabilities` to see which payment methods the shop currently accepts.

A common mistake: `GET /dev/v1/payments` returns existing payment records, not the shop's payment configuration. If you want to know which gateways (Stripe, PayPal, Crypto) are enabled, use `GET /dev/v1/me/capabilities` instead.

<Note>
  Crypto payments require a connected crypto provider: your own OxaPay, Cryptomus, or NowPayments
  account (connected in the dashboard under Settings → Payments → Crypto), or a Native Crypto
  wallet. `GET /dev/v1/me/capabilities` shows the enabled payment methods and native crypto
  gateways for your shop.
</Note>

### Skip the crypto picker on hosted checkout

If you want to keep Shoppex hosted checkout but open a concrete coin directly, pass:

* `gateway`: the merchant crypto provider you want Shoppex to use
* `crypto_gateway`: the concrete coin or network the buyer pays with

This works for merchant crypto providers like `OXAPAY`, `CRYPTOMUS`, and `NOWPAYMENTS`.

```bash cURL theme={"system"}
curl -X POST https://api.shoppex.io/dev/v1/payments \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Discord Boost Package",
    "email": "customer@example.com",
    "value": 59.99,
    "currency": "USD",
    "gateway": "OXAPAY",
    "crypto_gateway": "TRON"
  }'
```

Example response shape:

```json theme={"system"}
{
  "data": {
    "uniqid": "4ea04c92-5cc3-4ea8-845c-cd3c7085796c",
    "crypto_gateway": "TRON",
    "url": "https://checkout.shoppex.io/invoice/4ea04c92-5cc3-4ea8-845c-cd3c7085796c?selected_gateway=TRON&auto_start_gateway=1&auto_start_crypto=1",
    "url_branded": "https://checkout.shoppex.io/invoice/4ea04c92-5cc3-4ea8-845c-cd3c7085796c?selected_gateway=TRON&auto_start_gateway=1&auto_start_crypto=1&shop=12345"
  }
}
```

Simple example:

* `gateway: "OXAPAY"` + `crypto_gateway: "TRON"` -> hosted checkout opens directly on TRON
* `gateway: "CRYPTOMUS"` + `crypto_gateway: "USDT_TRC20"` -> hosted checkout opens directly on USDT TRC20
* `gateway: "NOWPAYMENTS"` + `crypto_gateway: "BITCOIN"` -> hosted checkout opens directly on Bitcoin through your NowPayments account
* `gateway: "NATIVE_CRYPTO"` + `crypto_gateway: "LITECOIN"` -> Shoppex derives a non-custodial Litecoin receive address from your active native wallet

If you omit `crypto_gateway`, the buyer lands on the normal generic crypto payment selection first.
If you only send `crypto_gateway` without `gateway`, Shoppex uses your active native wallet when it
covers that coin. Otherwise, Shoppex uses your connected crypto provider. Either way, Shoppex starts
the crypto session immediately. If you have no connected crypto provider and no native wallet, the
request fails with a validation error.

### Native crypto payments (BTC, LTC, USDT-TRC20, SOL, USDT-SPL)

Use this flow when you want crypto paid directly into your own non-custodial wallet. Shoppex
assigns a fresh receive address from your active native wallet. The address is derived from your
wallet key, or pre-created on your device for Solana. Shoppex watches the chain and marks the
payment paid after the chain's confirmation policy. Shoppex does not create a provider payment
and does not hold keys or funds.

Check availability first:

```bash cURL theme={"system"}
curl https://api.shoppex.io/dev/v1/me/capabilities \
  -H "Authorization: Bearer YOUR_API_KEY"
```

Look for:

```json theme={"system"}
{
  "data": {
    "features": {
      "nativeCryptoCheckout": {
        "available": true,
        "enabled": true,
        "gateways": ["BITCOIN", "LITECOIN", "USDT_TRC20", "SOLANA", "USDT_SOL"]
      }
    }
  }
}
```

Create a native Litecoin payment:

```bash cURL theme={"system"}
curl -X POST https://api.shoppex.io/dev/v1/payments \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Discord Boost Package",
    "email": "customer@example.com",
    "value": 59.99,
    "currency": "USD",
    "gateway": "NATIVE_CRYPTO",
    "crypto_gateway": "LITECOIN"
  }'
```

Example response shape:

```json theme={"system"}
{
  "data": {
    "uniqid": "4ea04c92-5cc3-4ea8-845c-cd3c7085796c",
    "gateway": "NATIVE",
    "crypto_gateway": "LITECOIN",
    "crypto_address": "ltc1qexampleaddress",
    "crypto_amount": 0.42133742,
    "crypto_received": 0,
    "crypto_uri": "litecoin:ltc1qexampleaddress?amount=0.42133742",
    "crypto_confirmations_needed": 2,
    "status": "PENDING",
    "url": "https://checkout.shoppex.io/invoice/4ea04c92-5cc3-4ea8-845c-cd3c7085796c"
  }
}
```

If `gateway` is `NATIVE_CRYPTO`, Shoppex fails if no active native wallet exists for the requested
chain. If you only send `crypto_gateway` (for example `"BITCOIN"` or `"SOLANA"`), an active native
wallet is prioritized over external providers offering the same coin.

### White-label crypto checkout (removed)

Shoppex removed managed white-label crypto checkout, together with the `white_label`
request parameter. Crypto payments always use your own accounts now:

* Native Crypto for non-custodial receive addresses from your own wallet (BTC, LTC, USDT-TRC20,
  SOL, USDT-SPL)
* Your own OxaPay, Cryptomus, or NowPayments account for other coins

Historical white-label crypto payments can still be read through the Developer API and can still
complete through the normal payment lifecycle. Schema validation rejects requests that still send
`white_label`.

### Create a payment

<CodeGroup>
  ```typescript TypeScript theme={"system"}
  const response = await fetch('https://api.shoppex.io/dev/v1/payments', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer YOUR_API_KEY',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      title: 'Order #123',
      email: 'customer@example.com',
      value: 29.99,
      currency: 'USD',
      // Optional settings
      gateway: 'STRIPE',           // Force specific gateway
      return_url: 'https://yoursite.com/success',
      cancel_url: 'https://yoursite.com/cancelled'
    })
  });

  const { data } = await response.json();

  // Redirect customer to checkout
  window.location.href = data.url;
  ```

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

  response = requests.post(
      'https://api.shoppex.io/dev/v1/payments',
      headers={
          'Authorization': 'Bearer YOUR_API_KEY',
          'Content-Type': 'application/json'
      },
      json={
          'title': 'Order #123',
          'email': 'customer@example.com',
          'value': 29.99,
          'currency': 'USD'
      }
  )

  data = response.json()['data']
  print(f"Checkout URL: {data['url']}")
  ```

  ```bash cURL theme={"system"}
  curl -X POST https://api.shoppex.io/dev/v1/payments \
    -H "Authorization: Bearer YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "title": "Order #123",
      "email": "customer@example.com",
      "value": 29.99,
      "currency": "USD"
    }'
  ```
</CodeGroup>

<Note>
  If you create the payment with `gateway: 'PANDABASE'`, Shoppex also returns `checkout_url` and `session_id`.

  * Use `data.checkout_url` if you want the direct Pandabase checkout session URL.
  * Keep listening for Shoppex webhooks for the final paid or cancelled state.
</Note>

<Note>
  Server-side completion is also different:

  * `POST /dev/v1/payments/:id/complete` confirms a generic developer payment only
  * `POST /dev/v1/orders/:id/fulfill` completes and fulfills a catalog-backed order
  * `POST /dev/v1/orders/:id/complete` is the same completion pipeline, exposed as an alias for integrations that look for a `complete` endpoint
</Note>

### Handle the webhook

After payment, Shoppex sends a webhook to your server. Use the `webhook_secret` returned by `POST /dev/v1/payments` for a per-payment webhook. Use the endpoint secret from **Settings → Webhooks** only for global webhook endpoints.

```typescript theme={"system"}
import crypto from 'crypto';

function verifyWebhook(
  payload: string,
  signatureHeader: string,
  deliveryId: string,
  timestampHeader: string,
  secret: string,
): boolean {
  const segments = signatureHeader.split(',').map((part) => part.trim());
  const hasV1Marker = segments.includes('v1');
  const parts = Object.fromEntries(segments.filter((part) => part.includes('=')).map((part) => {
    const [key, value] = part.trim().split('=');
    return [key, value ?? ''];
  }));
  if (!deliveryId || !hasV1Marker || parts.t !== timestampHeader) {
    return false;
  }
  const timestamp = Number(parts.t);
  if (!Number.isFinite(timestamp)) return false;
  if (Math.abs(Math.floor(Date.now() / 1000) - timestamp) > 300) {
    return false;
  }
  if (!/^[0-9a-f]{64}$/i.test(parts.h ?? '')) return false;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${deliveryId}.${parts.t}.${payload}`)
    .digest('hex');
  if (parts.h.length !== expected.length) return false;

  return crypto.timingSafeEqual(
    Buffer.from(parts.h, 'hex'),
    Buffer.from(expected, 'hex'),
  );
}

app.post('/webhooks/shoppex', async (req, res) => {
  // Verify signature (important!)
  const signature = req.headers['x-shoppex-signature-v2'] as string;
  const deliveryId = req.headers['x-shoppex-delivery'] as string;
  const timestamp = req.headers['x-shoppex-timestamp'] as string;
  if (!verifyWebhook(req.rawBody, signature, deliveryId, timestamp, WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }

  const { event, data } = req.body;

  switch (event) {
    case 'order:paid':
      // Payment successful - fulfill the order
      await fulfillOrder(data.uniqid, data.customer_email);
      break;

    case 'order:cancelled':
      // Payment failed or expired
      await handleFailedPayment(data.uniqid);
      break;
  }

  res.status(200).send('OK');
});
```

<Warning>
  Always verify webhook signatures in production. New integrations must use `X-Shoppex-Signature-V2`. See [Webhooks](/developers/webhooks) for details.
</Warning>

## Dynamic product fulfillment

If you use products with `type: "DYNAMIC"`, there is one more callback contract to implement: `dynamic_webhook`.

This is separate from normal Shoppex event webhooks and uses the product's dynamic webhook signing secret, not the secret from **Settings → Webhooks**.

The `order:paid` webhook tells your app that the invoice is paid. The `dynamic_webhook` is a separate fulfillment callback that asks your server for the delivered token, key, or access data.

Use the dedicated contract docs here:

* [Dynamic product delivery](/developers/dynamic-delivery)

***

## Invoice vs payment link

| Feature               | Invoice (API)         | Payment Link    |
| --------------------- | --------------------- | --------------- |
| **Use case**          | Custom checkout flows | Quick sharing   |
| **Customer email**    | Required              | Optional        |
| **Multiple products** | Yes                   | Single product  |
| **Custom fields**     | Yes                   | Product default |
| **Expiration**        | Configurable          | No expiration   |
| **Tracking**          | Full invoice data     | Basic analytics |

**Rule of thumb:**

* Use **Payment Links** for simple, shareable checkouts
* Use **Invoices** when you need control over the checkout or customer data

***

## Payment gateways

Configure your payment providers in **Settings → Payments**. For gateway-specific setup, supported methods, and fees, see [Payment gateways](/payments/gateways).

For a developer integration, the enabled gateways decide two things: which `gateway` value you can pass to `POST /dev/v1/payments` or `POST /dev/v1/orders`, and which `crypto_gateway` values are valid. Use `GET /dev/v1/me/capabilities` to check what is enabled for the shop before you hardcode a gateway value.

***

## Testing payments

Enable **Test Mode** in Settings before going live.

### Test card numbers

| Card       | Number                | Result        |
| ---------- | --------------------- | ------------- |
| Visa       | `4242 4242 4242 4242` | Success       |
| Mastercard | `5555 5555 5555 4444` | Success       |
| Declined   | `4000 0000 0000 0002` | Declined      |
| 3D Secure  | `4000 0025 0000 3155` | Requires auth |

Use any future expiry date and any 3-digit CVC.

<Note>These numbers are Stripe-specific. PayPal and crypto gateways have their own sandbox modes.</Note>

### Test checklist

<Steps>
  <Step title="Create a test invoice">
    Create an invoice through the dashboard or the API.
  </Step>

  <Step title="Complete checkout">
    Pay with test card `4242 4242 4242 4242`.
  </Step>

  <Step title="Verify the webhook">
    Check that your webhook endpoint received `order:paid`.
  </Step>

  <Step title="Check fulfillment">
    Confirm the product was delivered (email, license, download).
  </Step>
</Steps>

<Tip>
  Use [ngrok](https://ngrok.com) to test webhooks locally:

  ```bash theme={"system"}
  ngrok http 3000
  # Use the generated URL as your webhook endpoint
  ```
</Tip>

***

## Common scenarios

### Sell a digital product

1. Create a product with **File** delivery type.
2. Upload your file.
3. Share your storefront or payment link.
4. The customer pays, then receives the download automatically.

### Sell software licenses

1. Create a product with **Serials** delivery type.
2. Add license keys, one per line.
3. The customer pays, then receives a unique license key.

### Custom checkout in your app

1. Create a payment through the API with the customer email.
2. Redirect the customer to `data.url`.
3. Listen for the `order:paid` webhook.
4. Fulfill the order in your system.

### Custom manual gateway (local PSP)

1. Create a **Manual** gateway with a redirect URL and `{{invoice_id}}` template variables.
2. Listen for `order:manual_payment_pending` on your shop webhook.
3. When your PSP confirms payment, call `POST /dev/v1/invoices/{uniqid}/complete`.
4. Shoppex delivers the product and sends `order:paid`.

See [Manual payment auto-completion](/developers/manual-payments) for a Cloudflare Worker example.

### Recurring subscription

1. Create a product with **Subscription** type.
2. Set the billing interval (monthly, yearly, and so on).
3. The customer pays, then Shoppex creates the subscription.
4. Renewals happen automatically.

See [Subscriptions](/developers/subscriptions) for details.

***

<CardGroup cols={2}>
  <Card title="Invoices" icon="file-invoice" href="/developers/invoices">
    Deep dive into invoice lifecycle and statuses
  </Card>

  <Card title="Webhooks" icon="webhook" href="/developers/webhooks">
    Set up real-time notifications
  </Card>

  <Card title="Subscriptions" icon="repeat" href="/developers/subscriptions">
    Set up recurring billing
  </Card>

  <Card title="API reference" icon="code" href="/developers/api-overview">
    Explore all endpoints
  </Card>
</CardGroup>
