Developers

Connect a product to Ultra Pay

One HTTP API and one TypeScript file. The same steps for instaBOQ (Next.js), Layby (Express), Deliver Now (Convex) and Lavo.

Keys and modes

Each product has a secret key and a webhook secret, issued from the console. mpk_test_… keys work only on a sandbox hub, mpk_live_… keys only on the live hub. Keep the secret key on your server.

Base URLhttps://pay.millennialultra.com
AuthAuthorization: Bearer mpk_…
SDKsdk/mus-pay.ts. No dependencies. Uses fetch and Web Crypto.

Create a payment

POST /v1/payments with an Idempotency-Key header that is unique for each customer action. Send the customer to checkout_url, our branded checkout. There they choose EcoCash, OneMoney, InnBucks or card. Calling it again for the same reference continues the open payment. It never starts a second prompt.

{
  "reference": "BSMG-HRE-014",   // one reference is paid at most once
  "amount": "9.99", "currency": "USD",
  "description": "BOQ fee BSMG-HRE-014",
  "method": "hosted",           // or ecocash | onemoney | innbucks | checkout
  "phone": "0772123456",         // ecocash / onemoney only
  "customer": { "name": "…", "email": "…" },
  "return_url": "https://…/order/123",  // a registered return origin
  "metadata": { "order_id": "123" }
}
CallWhat it does
GET /v1/payments/:idCurrent state. The hub checks open attempts with ZB first, for a few seconds at most.
GET /v1/payments?reference=Every payment for one of your references, newest first.
POST /v1/payments/:id/attemptsRetry with another method or number. This is the only call that starts a new prompt.
POST /v1/payments/:id/cancelWithdraw an open payment. A late approval still reports succeeded.

Know it's paid

Release goods only when musPay.isPaid(payment, { reference, amountCents, currency }) is true. It checks the status, mode, reference, currency and the amount received.

Webhooks

Signed with your webhook secret: MusPay-Signature: t=…,v1=hmac_sha256(secret, t + "." + body). Verify on the raw body with verifyWebhook(), then fetch the payment and act on its current status, because events can arrive out of order. Answer 2xx quickly. Failed deliveries are retried for about two days.

Events

EventDo this
payment.succeededRelease it, once you've run isPaid. It may come after failed, expired or cancelled, when the customer approved late: honour it or refund it.
payment.failedOffer a retry (failure_reason says why).
payment.expiredOffer a new payment.
payment.requires_reviewMoney may have moved but couldn't be verified. Don't release and don't charge again. An operator decides.
payment.duplicate_chargeThe customer paid twice. Refund one in the ZB portal.
payment.cancelledNothing to do.

Errors

Errors look like { "error": { "code", "message", … } }. The ones to handle:

409 already_paidThis reference has been paid.
409 payment_under_reviewDon't charge again.
409 prompt_in_progressA prompt is on the phone. Wait retry_after_seconds.
409 reference_amount_conflictCancel the open payment, then create a new one.
429Rate limited. Respect Retry-After.

Test data (ZB sandbox)

OneMoney0713456789 succeeds · 0713456780 fails
Cards5123450000000008 succeeds (3-D Secure) · …0010 declined · expiry 01/39 · CVV 100
EcoCashApproved by hand by ZB staff. Book a session with them.

The rules Ultra Pay keeps for every product

A payment is paid only after a ZB status check shows the full amount in the right currency. An outage is never a decline. A prompt is never pushed twice automatically. Money that moved is never dropped: late approvals and duplicates are always reported.