Payments
Hosted Checkout
A secure, Muny-hosted payment page for every payment intent. No frontend work: redirect to checkout_url and Muny handles wallets, QR, status and confirmation.
URL structure#
| URL | Purpose |
|---|---|
/pay/{payment_intent_id} | Checkout for one payment intent (checkout_url). |
/pay/l/{slug} | A payment link. Each visit creates a fresh payment intent server-side, then shows checkout. |
IDs are random UUIDs, so checkout URLs are unguessable. The page only exposes what a payer needs: merchant name and logo, description, amount, destination, expiry and status — never your metadata.
What the customer sees#
- Your display name, logo and a restrained accent colour, with "Powered by Muny".
- The amount in MUNY and an indicative USD value (from the price provider; never used for settlement).
- Pay with wallet — Phantom, Solflare, Backpack and other wallets are detected through the Solana Wallet Standard. Muny builds the transaction server-side; the wallet only signs and sends it.
- Scan QR — a Solana Pay QR code, plus the destination address and amount for manual payment.
- A TEST MODE banner whenever the payment is on devnet.
States#
| Checkout state | Underlying status |
|---|---|
| Waiting for payment | requires_payment |
| Wallet connected · awaiting signature | pending |
| Transaction submitted · confirming | submitted |
| Payment complete ✓ | confirmed / finalized |
| Payment failed | failed |
| Payment expired | expired |
The page polls the public checkout API; the status only becomes complete after Muny's verification service has confirmed the transfer on chain. After a short pause the customer is redirected to your success_url.
Never trust the redirect#
success_url is not proof of payment
Anyone can open your success URL. Fulfil orders from thepayment.confirmed webhook, or retrieve the payment on your server when the customer lands back.// app/orders/[id]/page.tsx — the success_url page
const payment = await muny.paymentIntents.retrieve(order.munyPaymentId);
if (payment.status === "confirmed" || payment.status === "finalized") {
return <Thanks order={order} />;
}
return <StillProcessing />; // the webhook will fulfil the order shortlyIf the customer cancels, they are sent to cancel_url. The payment stays open until it expires, so they can come back and pay.
Branding#
In Payments → Settings set your display name, logo URL, support email, accent (gold, silver, emerald, sapphire or rose), default success/cancel URLs, payment expiry and the default receiving wallet. The same settings are available at PATCH /v1/merchant-settings. Customisation is deliberately restrained so checkout always feels trustworthy.
Public checkout API#
Hosted checkout and the future Muny mobile app use the same unauthenticated endpoints. None of them can mark a payment as paid.
GET /v1/checkout/:id— public view.POST /v1/checkout/:id/customer— email / name when collected.POST /v1/checkout/:id/transaction{ account }— returns the exact transfer for that wallet to sign.POST /v1/checkout/:id/submit{ signature }— a hint that speeds up detection. See Verify payments.