UnifiWallet Payments

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);

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);
}

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:

GET /v1/payments/pay_3nQ8xK2mLp4R

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.