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

# Custom Payment Methods

> Accept bank transfers, regional wallets or your own hosted payment page, and process the invoice yourself or from your own code.

A custom payment method covers anything SellAuth does not integrate with directly. You either tell the buyer how to pay, or send them to a page you control, and the invoice is completed once you confirm the money arrived.

There are two types, picked when you create the method:

| Type | The buyer sees | Payment is confirmed by |
| - | - | - |
| **Payment Instructions** | Text you write on the checkout page | You, in the dashboard |
| **Redirect to URL** | A redirect to a page you host | You, or your own code calling the API |

Requires a plan that includes custom payment methods. Add one under [**Payment Methods**](https://dash.sellauth.com/payment-methods).

## Payment instructions

Write what the buyer should do in the rich text editor, such as your bank details, a wallet address or a payment handle.

These variables are replaced per invoice:

| Variable | Value |
| - | - |
| `{id}` | Numeric invoice ID |
| `{unique_id}` | Public invoice reference, the one in the checkout URL |
| `{email}` | Buyer email |
| `{price}` | Invoice total |
| `{currency}` | Invoice currency |
| `{price_usd}` | Invoice total converted to USD |

Include `{unique_id}` and ask the buyer to use it as the payment reference. Without a reference, matching an incoming bank transfer to an order is difficult.

### Requiring proof of payment

Turn on **Require Proof of Payment** and the buyer must submit something before the order is put in front of you. Choose what they provide:

* **Text**, for a transaction reference or transfer ID
* **Image**, for a screenshot of the confirmation
* **Text or Image**, for either or both

**Proof of Payment Description** (up to 500 characters) is shown next to the field. Be specific about what you need, for example "Upload the confirmation screen showing the reference number and amount". Whatever the buyer submits appears on the invoice page for you to review.

## Approving a payment

An invoice on a custom method starts at **Pending**. When the buyer states they have paid, it moves to **Confirming**. That status is only a claim, and any buyer can trigger it without having paid, so treat it as a queue to verify rather than as evidence of payment.

<Steps>
  <Step title="Confirm the money arrived">
    Check your bank, wallet or account, and match both the amount and the reference.
  </Step>

  <Step title="Open the invoice and process it">
    From [**Invoices**](https://dash.sellauth.com/invoices). Processing delivers the items and sends the receipt exactly as an automatic payment would.
  </Step>

  <Step title="Decide whether to mark it as paid">
    **Mark as paid** sets the recorded paid amount to the invoice total. Use it when the money arrived outside SellAuth, which is the usual case. Leave it off if you are delivering without payment.
  </Step>
</Steps>

Invoices completed this way are labelled **Manually Completed**, which keeps them distinguishable from processor payments.

<Warning>
  Check the amount as well as the fact that a payment arrived. Underpayment is the most common abuse of custom methods, and nothing validates it automatically.
</Warning>

## Redirect to your own payment page

Set a **Redirect URL** and the buyer is sent there instead of reading instructions. The same variables work, so your endpoint receives everything it needs:

```
https://pay.yourdomain.com/start?invoice={id}&ref={unique_id}&amount={price}&currency={currency}&email={email}
```

This is how you connect a processor SellAuth does not support natively, or your own in-house payment flow. Your page takes the payment, and once your provider confirms it, your code processes the invoice through the SellAuth API.

## Processing an invoice from the API

One call completes the order:

```bash theme={null}
curl "https://api.sellauth.com/v1/shops/1/invoices/42/process?mark_as_paid=true" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

`42` is the `{id}` value your redirect URL received. `mark_as_paid=true` records the invoice total as paid, since the money was collected outside SellAuth. A success response means the items were delivered and the buyer was emailed. Full parameters are in [Process Invoice](/api-reference/invoices/process-invoice).

<Note>
  Create the API key under [**Account > Developers**](https://dash.sellauth.com/api). Scope it to this shop and to the permissions it needs rather than granting full access, and keep it on a server or worker where it is never exposed to a browser. See [Account Security](/guides/security#api-keys).
</Note>

## Worked example: Stripe through a Cloudflare Worker

This Worker has two endpoints. `/start` receives the buyer from your SellAuth checkout and opens a Stripe Checkout Session. `/webhook` receives Stripe's confirmation and processes the SellAuth invoice.

Set your custom method's redirect URL to `https://your-worker.workers.dev/start?invoice={id}&ref={unique_id}&amount={price}&currency={currency}&email={email}`.

```js theme={null}
import Stripe from 'stripe';

export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    const stripe = new Stripe(env.STRIPE_SECRET_KEY);

    if (url.pathname === '/start') {
      const invoiceId = url.searchParams.get('invoice');
      const ref = url.searchParams.get('ref');
      const amount = url.searchParams.get('amount');
      const currency = url.searchParams.get('currency');

      const session = await stripe.checkout.sessions.create({
        mode: 'payment',
        customer_email: url.searchParams.get('email'),
        line_items: [
          {
            quantity: 1,
            price_data: {
              currency: currency.toLowerCase(),
              unit_amount: Math.round(Number(amount) * 100),
              product_data: { name: `Order ${ref}` }
            }
          }
        ],
        // Carried through to the webhook so we know which invoice to process.
        metadata: { sellauth_invoice_id: invoiceId, sellauth_amount: amount },
        success_url: `https://yourshop.mysellauth.com/checkout/${ref}`,
        cancel_url: `https://yourshop.mysellauth.com/checkout/${ref}`
      });

      return Response.redirect(session.url, 302);
    }

    if (url.pathname === '/webhook' && request.method === 'POST') {
      let event;
      try {
        event = await stripe.webhooks.constructEventAsync(
          await request.text(),
          request.headers.get('stripe-signature'),
          env.STRIPE_WEBHOOK_SECRET,
          undefined,
          Stripe.createSubtleCryptoProvider()
        );
      } catch {
        return new Response('Invalid signature', { status: 400 });
      }

      if (event.type !== 'checkout.session.completed') {
        return new Response('Ignored', { status: 200 });
      }

      const session = event.data.object;
      const invoiceId = session.metadata.sellauth_invoice_id;
      const expected = Math.round(Number(session.metadata.sellauth_amount) * 100);

      if (session.payment_status !== 'paid' || session.amount_total < expected) {
        return new Response('Underpaid', { status: 400 });
      }

      const response = await fetch(
        `https://api.sellauth.com/v1/shops/${env.SELLAUTH_SHOP_ID}/invoices/${invoiceId}/process?mark_as_paid=true`,
        { headers: { Authorization: `Bearer ${env.SELLAUTH_API_KEY}` } }
      );

      // A non-2xx here means Stripe took the money but the order did not deliver.
      // Returning an error makes Stripe retry the webhook.
      if (!response.ok) {
        return new Response('Processing failed', { status: 500 });
      }

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

    return new Response('Not found', { status: 404 });
  }
};
```

Store `STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET`, `SELLAUTH_API_KEY` and `SELLAUTH_SHOP_ID` as Worker secrets, and point a Stripe webhook endpoint at `/webhook` subscribed to `checkout.session.completed`.

Three details in this example matter, and any replacement should keep them:

* **It verifies the webhook signature.** Without that check, anyone who learns your Worker URL can deliver free orders to themselves.
* **It carries the invoice ID in metadata** rather than trusting a value posted back to it.
* **It compares the amount paid against the amount owed** before processing.

The same structure works for any provider. Take their confirmation, verify it is genuine, then call the process endpoint.

## Next steps

<CardGroup cols={2}>
  <Card title="Supported payment methods" icon="credit-card" href="/guides/payment-methods">
    Everything SellAuth integrates with directly.
  </Card>

  <Card title="How checkout works" icon="cart-shopping" href="/guides/checkout">
    Where a custom method invoice sits before you approve it.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.