> For the complete index of the 402pay docs, see [llms.txt](https://developer.402pay.co/llms.txt).

# Accept a payment

Create a payment on your server, send the customer to checkout, and fulfill on the webhook.

Your server and hosted checkout

Recommended

- **Code needed**: One API call and a webhook handler
- **Time**: An afternoon
- **Customization**: Amount, reference, metadata, redirects and expiry on every payment
- **Best for**: Stores and apps with their own orders

Create the payment from your server when the customer is ready to pay, then send them to its `url`. Checkout takes it from there, and a webhook tells you when it's paid.

The customer's coins go straight to your wallet. The webhook is how your server hears about it.

- Your server (POST /payments) → Hosted checkout (the payment's url)
- Hosted checkout (the payment's url) → Customer's wallet (sends the coin)
- Customer's wallet (sends the coin) → Your wallet (funds land here)
- Your wallet (funds land here) → 402pay (sees it on chain)
- 402pay (sees it on chain) → Your server (POST /payments): payment.succeeded

## Create the payment

server.js, Node.js:

```js
// When the customer checks out, create the payment and send them to it.
app.post("/checkout", async (req, res) => {
  const order = await orders.create(req.body);

  const response = await fetch("https://dash.402pay.co/api/v1/payments", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.PAY402_SECRET_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      amount: order.totalCents,
      currency: "USD",
      reference: order.id,
      description: order.summary,
      success_url: `https://example.com/orders/${order.id}`,
      metadata: { order_id: order.id },
    }),
  });

  const { data: payment } = await response.json();
  res.redirect(303, payment.url);
});
```

- `amount` is in minor units of `currency`: `4900` with `USD` is $49.00.
- `reference` is your own ID. It names one live payment, so a retried create with the same reference, amount and currency returns the payment that already exists, with 200. The same reference with a different amount or currency returns 409 `reference_in_use`. Without a `reference`, every create makes a new payment.
- `expires_at` sets how long the payment can be paid, from 15 minutes to 30 days. It defaults to 24 hours.
- `success_url` is where checkout sends the customer after paying. Without it, they go to the redirect in your checkout settings, or see their receipt when there's none. Both it and `cancel_url` must use `https://`, except on `localhost` and `127.0.0.1`.

## Fulfill the order

Fulfill on the `payment.succeeded` webhook, not the redirect: a customer can close the tab before your success page loads. Answer quickly, then do the work.

webhooks.js, Node.js:

```js
// Fulfill once, when the payment succeeds.
app.post("/webhooks/402pay", express.raw({ type: "application/json" }), async (req, res) => {
  if (!verifyWebhook(process.env.PAY402_WEBHOOK_SECRET, req.headers, req.body)) {
    return res.sendStatus(400);
  }
  res.sendStatus(200);

  const event = JSON.parse(req.body);
  // A test delivery names a made-up payment, and its data holds only that id.
  if (event.test) return;
  if (event.type === "payment.succeeded") {
    await orders.markPaid(event.data.reference, event.data.id);
  }
});
```

## Payment statuses

| Status | Meaning |
| --- | --- |
| `pending` | Waiting for the customer, or for funds to arrive. |
| `succeeded` | Paid in full, or accepted: the funds landed in your wallet, or paid fees you owed. Fulfill the order. |
| `underpaid` | Less than the amount due arrived. Checkout asks for the rest, or you can accept it. |
| `needs_review` | Funds arrived in a way that needs your decision, such as after the checkout expired. |
| `failed` | A payment was canceled, by the business or by the customer at a link's checkout, or a declined card had no route left or wasn't retried within 30 minutes. No funds landed in the wallet. It isn't always final: an API payment you didn't cancel can still be paid, since the customer's next attempt makes it `pending` again. It's over once `canceled_at` is set. |
| `expired` | Nothing arrived before the payment, or a link's quote, expired. |

## Cancel or accept

Cancel a payment that hasn't received funds with [`POST /payments/{id}/cancel`](https://developer.402pay.co/api/payments/cancel.md). Count an underpaid payment, or one that needs review, as paid with [`POST /payments/{id}/accept`](https://developer.402pay.co/api/payments/accept.md).

## Next steps

- [Webhooks](https://developer.402pay.co/guides/webhooks.md): Signed events for every change, retried until your server answers.
- [Underpayments and overpayments](https://developer.402pay.co/guides/underpayments.md): What happens when a customer sends too little, too much, too late or on the wrong network.
