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#

URLPurpose
/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 stateUnderlying status
Waiting for paymentrequires_payment
Wallet connected · awaiting signaturepending
Transaction submitted · confirmingsubmitted
Payment complete ✓confirmed / finalized
Payment failedfailed
Payment expiredexpired

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 the payment.confirmed webhook, or retrieve the payment on your server when the customer lands back.
ts
// 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 shortly

If 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.