Payments
Accept your first payment
Create a payment on your server, send the customer to Muny's hosted checkout, and fulfil the order when Muny tells you the payment is verified on chain. About five minutes.
How it fits together#
Muny payments are non-custodial: the customer pays straight from their Solana wallet into your settlement wallet. Muny builds the transaction, watches the chain, verifies the transfer and then notifies you. Your frontend never decides whether a payment succeeded.
Your server creates a payment intent
One API call with the amount, your order reference and the URLs to return to. You get back a
checkout_url.The customer pays on Muny checkout
They connect Phantom, Solflare or Backpack — or scan a Solana Pay QR code with any compatible wallet.
Muny verifies the transfer on chain
Network, token mint, recipient, exact amount, success and commitment are all checked. See Verify payments.
You receive payment.confirmed
Fulfil the order from the webhook (or by retrieving the payment). The customer is redirected to your success_url.
1. Create a payment#
Install the SDK with npm install @muny/sdk and create a payment with your secret API key. Amounts are decimal strings in whole MUNY. An idempotency key is required — the SDK generates one if you don't pass it, but deriving it from your order ID makes retries safe even across restarts.
// app/checkout/route.ts — Next.js App Router
import { redirect } from "next/navigation";
import { Muny } from "@muny/sdk";
const muny = new Muny({ apiKey: process.env.MUNY_API_KEY });
export async function POST(req: Request) {
const order = await createOrder(await req.formData()); // your own order record
const payment = await muny.paymentIntents.create(
{
amount: "49",
token: "MUNY",
reference: order.id,
description: "Pro plan · 1 month",
success_url: "https://shop.example/orders/" + order.id,
cancel_url: "https://shop.example/cart",
metadata: { order_id: order.id },
},
{ idempotencyKey: "order_" + order.id }, // retries never create a second payment
);
await saveMunyPaymentId(order.id, payment.id);
redirect(payment.checkout_url);
}{
"id": "3f1c9a52-7d0e-4c1b-9b8e-6a2f1d5c0e47",
"object": "payment_intent",
"livemode": false,
"status": "requires_payment",
"amount": "49",
"token": "MUNY",
"reference": "order_123",
"checkout_url": "https://muny.io/pay/3f1c9a52-7d0e-4c1b-9b8e-6a2f1d5c0e47",
"solana_pay_url": "solana:7hjn…GMz?amount=49&spl-token=…&reference=…",
"expires_at": "2026-10-01T15:42:00.000Z"
}Server-side only
API keys (mk_test_… / mk_live_…) are secrets. Create payments on your server; never in the browser or a mobile app.2. Redirect to checkout#
Send the customer to payment.checkout_url. The hosted page shows your business name and logo, the amount with an indicative USD value, a wallet button and a QR code. Prefer to render your own? Show payment.solana_pay_url as a QR — see QR payments.
3. Fulfil on the webhook#
Subscribe an endpoint to payment.confirmed in Developers → Webhooks, then fulfil from the event. The redirect to your success_url is a convenience for the customer, not proof of payment.
import { verifyWebhook } from "@muny/sdk";
// Use the RAW request body — verify before parsing JSON.
app.post("/webhooks/muny", express.raw({ type: "application/json" }), async (req, res) => {
let event;
try {
event = await verifyWebhook(req.body.toString("utf8"), req.header("muny-signature"), process.env.MUNY_WEBHOOK_SECRET!);
} catch {
return res.status(400).end();
}
if (event.type === "payment.confirmed") {
const payment = event.data.object;
await fulfilOrder(payment.reference, { idempotencyKey: event.id }); // dedupe on the event id
}
res.status(200).end();
});What to read next#
- Payment Intents — every field, line items, expiry and the lifecycle.
- Payment Links — accept payments without writing code.
- Testing — simulate payments, including failures, in test mode.