Quickstart
Register a wallet, create a payment, send the buyer to the checkout, and ship when the webhook says completed.
Four steps, and one of them is a decision rather than code. You will need a test key from the panel and somewhere the internet can reach for the webhook.
1. Register a settlement address
Before anything else, tell us which wallet you want to be paid into. Do this in the panel: adding one takes a signature from that wallet to prove you hold it, and then sits in a cooling period before it can receive.
Check it character by character.
Every payment goes here, and we cannot reverse a transfer or recover funds sent to the wrong place. The signature and the cooling period exist because this is the one setting where a mistake is unrecoverable.
2. Create a payment
Server-side, with a signed request — see
authentication for the call
helper used here. You get back a checkout_url; send the buyer to it.
const payment = await call('POST', '/v1/payments', {
amount: '49.99',
currency: 'USD',
merchant_order_id: 'order_1041',
description: 'Two tickets',
return_url: 'https://shop.example.com/thanks',
cancel_url: 'https://shop.example.com/basket',
customer: { email: 'buyer@example.com' },
});
// Store payment.id against your order before you redirect. The webhook
// arrives keyed on it, and possibly before the buyer gets back to you.
await orders.update('order_1041', { paymentId: payment.id });
redirect(payment.checkout_url);
payment = call("POST", "/v1/payments", {
"amount": "49.99",
"currency": "USD",
"merchant_order_id": "order_1041",
"description": "Two tickets",
"return_url": "https://shop.example.com/thanks",
"cancel_url": "https://shop.example.com/basket",
"customer": {"email": "buyer@example.com"},
})
# Store payment["id"] against your order before you redirect. The webhook
# arrives keyed on it, and possibly before the buyer gets back to you.
orders.update("order_1041", payment_id=payment["id"])
return redirect(payment["checkout_url"])
{
"id": "pay_3nQ8xK2mLp4R",
"status": "created",
"checkout_url": "https://checkout.payment.unifiwallet.com/c/ct_9f3a...",
"amount": "49.99",
"currency": "USD",
"expires_at": "2026-09-04T11:30:00Z",
"snapshot": {
"fee_bps": 100,
"settlement": {
"path": "direct",
"resolved_address": "0x8f2a...",
"asset": "USDC",
"chain_id": 8453
}
}
}
Note the snapshot. The fee, the rate and the destination are
fixed at creation and recorded on the payment, so what settles is what was
quoted — including weeks later, if it comes to an argument.
3. Let the buyer pay
The checkout is ours and runs on our origin. The buyer connects a wallet, picks what to pay in, and signs one transaction. No wallet code runs on your site and no private key comes near your servers.
When they are done we send them to your return_url. Treat that
redirect as a hint, not as proof — see below.
4. Act on the webhook
Register an endpoint, verify the signature, and
ship on payment.completed.
async function handle(event) {
if (event.type !== 'payment.completed') return;
const orderId = event.data.merchant_order_id;
// At-least-once delivery: the same event can arrive twice.
if (await events.seen(event.id)) return;
await events.record(event.id);
await orders.markPaid(orderId, {
paymentId: event.data.id,
settlementId: event.data.settlement_id,
});
await fulfil(orderId);
}
def handle(event):
if event["type"] != "payment.completed":
return
order_id = event["data"]["merchant_order_id"]
# At-least-once delivery: the same event can arrive twice.
if events.seen(event["id"]):
return
events.record(event["id"])
orders.mark_paid(order_id, payment_id=event["data"]["id"])
fulfil(order_id)
Do not fulfil on the redirect, and do not fulfil on settled.
A return URL can be opened by anyone, replayed, or never reached at all —
buyers close tabs. And settled only means a transaction was seen
on chain; completed means it is deep enough that a reorg will
not take it back. Shipping on either of the first two is how goods leave for
payments that never finish.
What to do while you wait
Between settling and completed there is a real
interval — seconds on some chains, minutes on others. Show the buyer something
honest rather than a spinner or a lie: their transaction is on chain and you
are waiting for confirmations.
Checking without waiting
Webhooks are the mechanism, but you can always ask. Useful on the return page, where you want to show the buyer the current state immediately:
Poll this if your webhook endpoint is down; it is the documented fallback, not a workaround.
Then break it on purpose
The happy path is the easy half. In test mode you can produce an underpayment, an expiry, a reorg and a failed settlement without a wallet or a chain. Build those branches before you go near real money, because you will meet them eventually and the first time should not be in production.