> ## 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 via API, Payment Links, or your Storefront

## Overview

There are three ways to accept payments with Shoppex:

<CardGroup cols={3}>
  <Card title="Storefront" icon="store">
    Customers browse your shop and checkout 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>

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

***

## The Payment Flow

<Info>
  You don't need to integrate with Stripe or PayPal directly. Shoppex handles all gateway communication for you.
</Info>

## Important Integration Rule

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

The important thing: redirect the customer to the Shoppex checkout URL, listen for webhook events, and fulfill based on the final invoice status. Don't build your fulfillment logic around a single provider-side session ID.

***

## Method 1: Storefront (Hosted/Public)

Your storefront at `yourshop.shoppex.io` is ready out of the box:

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 - perfect for sharing on social media, emails, or anywhere.

### Create via Dashboard

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

### Create via 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/invoice to that URL. Store `webhook_secret` immediately; it is not returned 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 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 just want a hosted payment URL. You don't get Shoppex catalog line items or automatic product delivery.

Use **`POST /dev/v1/orders`** when you want to charge for real Shoppex products — with 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>

### Need to know which payment methods the shop currently accepts?

Use `GET /dev/v1/me/capabilities` for that.

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.

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

### 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 should pay 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 via 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 your connected crypto provider — and starts the crypto session
immediately. Without any connected crypto provider or 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 (derived from your wallet key, or
pre-created on your device for Solana), 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-managed white-label crypto checkout has been removed, 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. Requests that still send `white_label` are rejected
by schema validation.

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

<Info>
  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/cancelled state
</Info>

<Info>
  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
</Info>

### 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}`, 'utf8')
    .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 should use `X-Shoppex-Signature-V2`. See the [Webhooks Guide](/guides/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](/api-reference/webhooks/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**.

### Stripe (Recommended)

Supports: Credit cards, Apple Pay, Google Pay, SEPA, Klarna, and more.

<Steps>
  <Step title="Get API Keys">
    Log in to [Stripe Dashboard](https://dashboard.stripe.com) → **Developers → API Keys**
  </Step>

  <Step title="Add to Shoppex">
    Enter your Publishable Key and Secret Key in **Settings → Payments → Stripe**
  </Step>

  <Step title="Configure Webhook">
    In Stripe Dashboard, create a webhook pointing to:

    ```
    https://api.shoppex.io/v1/webhooks/stripe/{your_shop_id}
    ```
  </Step>
</Steps>

### PayPal

<Steps>
  <Step title="Create REST App">
    Go to [PayPal Developer](https://developer.paypal.com) → Create App
  </Step>

  <Step title="Add Credentials">
    Enter Client ID and Secret in **Settings → Payments → PayPal**
  </Step>
</Steps>

### Cryptocurrency

Two options available:

| Option                       | Setup                                       | Fees              |
| ---------------------------- | ------------------------------------------- | ----------------- |
| **Shoppex Wallet** (BTC/LTC) | Self-custodial wallet, created in-dashboard | Network fees only |
| **Cryptomus / Oxapay**       | API key only                                | Provider fees     |

***

## 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 Test Invoice">
    Create an invoice via dashboard or API
  </Step>

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

  <Step title="Verify 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

<AccordionGroup>
  <Accordion title="Sell a digital product">
    1. Create product with **File** delivery type
    2. Upload your file
    3. Share your storefront or payment link
    4. Customer pays → receives download automatically
  </Accordion>

  <Accordion title="Sell software licenses">
    1. Create product with **Serials** delivery type
    2. Add license keys (one per line)
    3. Customer pays → receives unique license key
  </Accordion>

  <Accordion title="Custom checkout in your app">
    1. Create payment via API with customer email
    2. Redirect customer to `data.url`
    3. Listen for `order:paid` webhook
    4. Fulfill order in your system
  </Accordion>

  <Accordion title="Custom manual gateway (local PSP)">
    1. Create a **Manual** gateway with redirect URL and `{{invoice_id}}` template vars
    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](/guides/manual-payment-completion) for a Cloudflare Worker example.
  </Accordion>

  <Accordion title="Recurring subscription">
    1. Create product with **Subscription** type
    2. Set billing interval (monthly, yearly, etc.)
    3. Customer pays → subscription created
    4. Renewals happen automatically

    See [Subscriptions Guide](/guides/subscriptions) for details.
  </Accordion>
</AccordionGroup>

***

## Next Steps

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

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

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

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