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.
- 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.jsNode.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);});amountis in minor units ofcurrency:4900withUSDis $49.00.referenceis 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 409reference_in_use. Without areference, every create makes a new payment.expires_atsets how long the payment can be paid, from 15 minutes to 30 days. It defaults to 24 hours.success_urlis 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 andcancel_urlmust usehttps://, except onlocalhostand127.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.jsNode.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. Count an underpaid payment, or one that needs review, as paid with POST /payments/{id}/accept.