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 URL | https://pay.millennialultra.com |
|---|---|
| Auth | Authorization: Bearer mpk_… |
| SDK | sdk/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" }
}
| Call | What it does |
|---|---|
GET /v1/payments/:id | Current 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/attempts | Retry with another method or number. This is the only call that starts a new prompt. |
POST /v1/payments/:id/cancel | Withdraw 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
| Event | Do this |
|---|---|
payment.succeeded | Release 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.failed | Offer a retry (failure_reason says why). |
payment.expired | Offer a new payment. |
payment.requires_review | Money may have moved but couldn't be verified. Don't release and don't charge again. An operator decides. |
payment.duplicate_charge | The customer paid twice. Refund one in the ZB portal. |
payment.cancelled | Nothing to do. |
Errors
Errors look like { "error": { "code", "message", … } }. The ones to handle:
409 already_paid | This reference has been paid. |
409 payment_under_review | Don't charge again. |
409 prompt_in_progress | A prompt is on the phone. Wait retry_after_seconds. |
409 reference_amount_conflict | Cancel the open payment, then create a new one. |
429 | Rate limited. Respect Retry-After. |
Test data (ZB sandbox)
| OneMoney | 0713456789 succeeds · 0713456780 fails |
| Cards | 5123450000000008 succeeds (3-D Secure) · …0010 declined · expiry 01/39 · CVV 100 |
| EcoCash | Approved 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.