← Back to docs

Lango documentation

Accept EcoCash payments in Zimbabwe — the entire handbook, on one page.

Generated 2026-09-17. Use Save as PDF above (or your browser's Print → Save as PDF). The live API reference is at /api.

  1. Your first payment in 5 minutes
  2. Payments and their lifecycle
  3. The unknown state
  4. Fees, preview and bearers
  5. Idempotency
  6. Webhooks and signatures
  7. Settlements
  8. Currencies
  9. Payment links and QR codes
  10. The buy button
  11. Hosted checkout
  12. Direct API
  13. Sandbox and test numbers
  14. Error codes
  15. Going live
  16. Changelog
  17. Embeddable checkout
  18. SDKs & plugins

Your first payment in 5 minutes

Test keys, one API call, a working payment.

You need: a Lango account (register here — sandbox keys are issued the moment you finish) and a terminal. Nothing else.

  1. Copy your test key.

    Dashboard → Developers. Test keys start with sk_test_. They can never move real money.

  2. Create a payment.

    The customer number below is a sandbox number that always approves.

    Terminal window
    curl https://api.lango.co.zw/v1/payments \
    -H "Authorization: Bearer sk_test_YOUR_KEY" \
    -H "Idempotency-Key: quickstart-001" \
    -H "Content-Type: application/json" \
    -d '{
    "amount": 10000,
    "currency": "USD",
    "method": "ecocash",
    "reference": "QUICKSTART-1",
    "customer": { "msisdn": "263771000001" }
    }'

    The response itemises every fee before anything is charged:

    {
    "id": "pay_01J8XQ7M4K...",
    "status": "awaiting_customer",
    "currency": "USD",
    "amount": 10000,
    "charged_amount": 10300,
    "merchant_net": 10000,
    "fees": [{ "type": "commission", "amount": 300, "bearer": "customer" }],
    "reference": "QUICKSTART-1",
    "created_at": "2026-09-12T09:00:00.000Z"
    }
  3. Watch it complete.

    In the sandbox, ...000001 approves after a moment. Fetch the payment:

    Terminal window
    curl https://api.lango.co.zw/v1/payments/pay_01J8XQ7M4K... \
    -H "Authorization: Bearer sk_test_YOUR_KEY"

    status is now completed. Your dashboard’s Payments page shows the same payment with its full timeline.

  4. Hear about it without polling.

    Add a webhook endpoint (Dashboard → Developers → Webhooks). Lango signs every delivery; verify the signature and you can trust the news:

    { "type": "payment.succeeded", "data": { "id": "pay_01J8XQ7M4K...", "amount": 10000 } }

That’s a working integration. Where to next:

Payments and their lifecycle

The twelve payment statuses, what each means, and which transitions exist.

A payment is one attempt to collect money from a customer. It moves through a fixed state machine — no transition outside this table ever happens, and every transition is recorded in a timeline you can read back.

The statuses

StatusMeaningTerminal?
createdThe payment exists; no customer number yet (links and hosted checkout start here)no
initiatedWe are contacting the providerno
awaiting_customerThe PIN prompt is on the customer’s phoneno
awaiting_otpThe provider asked for a one-time codeno
awaiting_code_useThe customer received a code to use at a tillno
completedThe provider confirmed the money moved. The only status that credits your balanceyes*
failedThe provider definitively said no (declined, insufficient funds)yes
expiredThe customer never acted and the window closedyes
unknownWe asked, the network didn’t answer. Not a failure. We keep askingno
unresolvedStill unknown after 24 hours of retries; a human takes over. Your balance is not creditedyes*
settledCompleted and paid out to your bank in a settlement batchyes
refundedCompleted, then refunded in fullyes

* completed later becomes settled or refunded; unresolved can be manually resolved.

What you should key on

Amounts on the wire

All amounts are integers in minor units (cents). USD 100.00 is 10000. There are no floats anywhere in Lango, and there should be none in your integration either.

FieldMeaning
amountThe order amount — what the sale is worth
charged_amountWhat the customer actually pays (order + their fees)
merchant_netWhat lands in your balance
fees[]Every fee, itemised, with its bearer

charged_amount − merchant_net is always exactly the commission. To the cent, every time — the invariant is enforced in our ledger, not just documented here.

The unknown state

The one page that prevents double charges. Read it even if you read nothing else.

What unknown means

Mobile-money networks time out, drop callbacks, and answer late — routinely, not exceptionally. When Lango asks the provider “did this payment go through?” and the network doesn’t answer, there are exactly three truths and we don’t know which one holds:

  1. The customer paid, and the confirmation was lost.
  2. The customer didn’t pay.
  3. The customer is still deciding.

Any system that maps this situation to failed is guessing — and when it guesses wrong, the customer pays twice. Lango refuses to guess. The payment enters unknown and we chase the answer: we re-query the provider on a backoff schedule (10 seconds, 30 seconds, 2, 5, 15, 30 minutes, then hourly) until it resolves or 24 hours pass.

What you should do

If 24 hours pass

The payment becomes unresolved and a human at Lango takes over, checking provider statements directly. Your balance is not credited for unresolved payments — money you can’t prove is money you don’t have. If the money did move, resolution credits it retroactively and you’re notified; if it didn’t, the payment is failed with a paper trail.

This is rarer than it sounds — most unknown payments resolve within the first minute — but when it happens, it’s handled in the open rather than silently absorbed.

Fees, preview and bearers

Commission, who bears it, IMTT, VAT — and the preview endpoint that quotes them with the live engine.

Lango’s differentiator is legibility: every charge is computed by one engine, itemised on the wire, and quotable in advance. A quote cannot drift from a charge because they share the code.

The fees

FeeWhat it isWho sets it
CommissionLango’s fee on each collection (e.g. 3%)Your agreement
VAT15% inside the commission (not on top), broken out for your returnsStatute
IMTTTax on the payout bank transfer — 2% (USD) at time of writingStatute
Transfer feeThe bank’s flat fee per settlement transferThe bank

Statutory rates change by government notice; Lango applies the rate in force on the day, and your settlement advice names the rate used.

Bearers: who pays the commission

Your fee configuration decides who bears commission:

The invariant, enforced in the ledger on every payment: charged_amount − merchant_net = commission, exactly, in integer cents.

Preview: quote before you charge

POST /v1/preview runs the same engine with no money movement:

Terminal window
curl https://api.lango.co.zw/v1/preview \
-H "Authorization: Bearer sk_test_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "amount": 10000, "currency": "USD", "method": "ecocash", "payout_amount": 500000 }'
{
"customer_pays": 10300,
"merchant_receives": 10000,
"commission": 300,
"payout": {
"gross": 500000,
"imtt": 9804,
"transfer_fee": 200,
"net": 489996,
"effective_rate": "2.00%"
},
"warnings": []
}

Two details worth noticing:

Use preview to show customers total cost before they commit, or to build your own pricing calculators — ours (lango.co.zw/pricing) is this endpoint.

Idempotency

Why every POST takes an Idempotency-Key, and how retries become safe.

Networks fail after the server acted as often as before. If you POST a charge, the connection drops, and you retry — did you charge once or twice? With idempotency keys, the question disappears.

How it works

Every POST that creates something takes an Idempotency-Key header (it’s required on POST /v1/payments). The key is any string up to 255 characters that uniquely identifies the attempt — an order id is usually perfect.

Terminal window
curl https://api.lango.co.zw/v1/payments \
-H "Authorization: Bearer sk_test_YOUR_KEY" \
-H "Idempotency-Key: order-4417" \
-H "Content-Type: application/json" \
-d '{ "amount": 10000, "currency": "USD", "method": "ecocash", "reference": "ORD-4417" }'

Keys are held for 24 hours.

References are a second net

Independently of idempotency, every payment carries your reference, and a reference is unique per merchant. Reusing one returns 409 reference_in_use with the existing payment to fetch instead. Between the two mechanisms, “charged twice because we retried” is not a failure mode a Lango integration has.

The rule of thumb

Generate the key where the intent originates (your order id, your invoice number), not at the HTTP call site. Then any layer of your stack can retry safely — job queues, load balancers, a user mashing F5 — and the worst case is always the same response twice, never the same charge twice.

Webhooks and signatures

Signed deliveries, the retry schedule, and how to verify a Lango-Signature.

Webhooks tell your server what happened without polling. Every delivery is signed; verify the signature and you can trust the news came from Lango and wasn’t replayed.

Events

EventFired when
payment.succeededThe provider confirmed the money moved
payment.failedThe provider definitively declined
payment.expiredThe customer never acted
payment.unresolved24 hours of unknown; a human is investigating
refund.succeededA refund completed
refund.failedA refund could not be completed
settlement.paidA settlement batch paid out to your bank

There is no event for unknown — it isn’t an outcome, and an event would tempt you to treat it as one.

Verifying the signature

Every delivery carries:

Lango-Signature: t=1727286000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

t is a unix timestamp; v1 is HMAC-SHA256(secret, "{t}.{raw_body}") in hex. To verify:

  1. Split the header into t and v1.
  2. Reject if |now − t| is more than 300 seconds (replay protection).
  3. Compute HMAC-SHA256 of "{t}.{raw_body}" — the raw request body, before any JSON parsing — with your endpoint’s signing secret.
  4. Compare with v1 using a constant-time comparison.
import { createHmac, timingSafeEqual } from 'node:crypto'
function verify(header, rawBody, secret) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')))
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false
const expected = createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex')
const a = Buffer.from(expected)
const b = Buffer.from(parts.v1 ?? '')
return a.length === b.length && timingSafeEqual(a, b)
}
function verify(string $header, string $rawBody, string $secret): bool {
$parts = [];
foreach (explode(',', $header) as $pair) {
[$k, $v] = explode('=', $pair, 2);
$parts[$k] = $v;
}
if (abs(time() - (int) $parts['t']) > 300) return false;
$expected = hash_hmac('sha256', $parts['t'] . '.' . $rawBody, $secret);
return hash_equals($expected, $parts['v1'] ?? '');
}

The official SDKs ship this as one function call.

Delivery and retries

Missed something?

Every event is also in the log: GET /v1/events lists them, and POST /v1/events/{id}/replay re-queues delivery to your endpoints. After an outage on your side, replay the window you missed instead of reconciling by hand.

Settlements

How collected money reaches your bank, and how to read the advice.

Money Lango collects for you accrues in your balance (per currency, visible in the dashboard and at GET /v1/balance). On your settlement cycle, the balance pays out to your verified bank account as a settlement.

The flow

  1. Completed payments credit your balance the moment the provider confirms them.
  2. On your cycle, Lango proposes a settlement batch covering your payable balance above the minimum threshold.
  3. The batch passes Lango’s internal controls — including the solvency check (the client account must cover every merchant’s balance, always) and dual approval (no single person at Lango can release money).
  4. The transfer lands in your bank; each settled payment’s status becomes settled; you receive a settlement.paid webhook and an advice.

The advice

Every settlement comes with an advice (PDF and CSV) that itemises:

If your bookkeeper can’t reconcile a Lango advice against the bank statement in one sitting, that’s a bug — report it.

Notes

Currencies

USD and ZWG, integer minor units, and why nothing is ever summed across currencies.

Lango processes USD and ZWG. Three rules keep multi-currency handling honest:

1. Amounts are integers in minor units

USD 100.00 is 10000. ZWG 312.04 is 31204. Always two implied decimal places, never floats. Send amount: 10000, read amount: 10000 — formatting is a display concern.

When displaying, put the currency code first (USD 1,240.50), because a bare $ is ambiguous in Zimbabwe.

2. Currencies never blend

Balances, settlements, reports and totals are always per currency. Lango never sums USD and ZWG into one number, at any exchange rate, anywhere — and your integration shouldn’t either. A “total revenue” figure spanning both currencies is a fiction the moment the rate moves.

GET /v1/balance:

{
"available": [
{ "currency": "USD", "amount": 125000 },
{ "currency": "ZWG", "amount": 3120400 }
]
}

Two entries, two truths — deliberately not one.

3. A payment lives entirely in one currency

The order, the commission, the customer fee, the settlement — all in the payment’s currency. There is no cross-currency payment and no automatic conversion. If you price in USD and want to accept ZWG, create ZWG payments at your own rate, on your own terms, explicitly.

Statutory rates differ by currency

IMTT and other statutory charges can carry different rates per currency (and change by government notice). Preview quotes the rate in force; your settlement advice names the rate that was applied.

The buy button

One script tag and a div — a payment button on any website, even one you can't really edit.

The buy button puts a Lango payment button on any page — a WordPress site, a school’s ancient CMS, a Google Sites page. There is no key in the page and nothing to steal: the button rides on a payment link, and a link token only lets a customer pay you.

Setup

  1. Create a reusable payment link (dashboard → Payment links → New → Type: Reusable), or a single-use one for a specific invoice.
  2. Paste this where the button should appear:
<script async src="https://checkout.lango.co.zw/buy-button.js"></script>
<div
data-lango-link="plk_01J8XQ..."
data-lango-label="Pay with EcoCash"
data-lango-amount="USD 10.00"
></div>

That’s the entire integration. Clicking opens Lango’s hosted checkout for that link; the customer approves on their phone; the payment lands in your dashboard and webhooks with the link’s reference.

Attributes

AttributeRequiredDefaultNotes
data-lango-linkyesThe link’s token (plk_...), from dashboard or API
data-lango-labelnoPay with EcoCashThe button text
data-lango-amountnoDisplay only, e.g. USD 10.00 — shown after the label

Multiple buttons on one page work; the script renders every [data-lango-link] it finds.

Notes

Hosted checkout

Create a payment from your backend, send the customer to checkout_url, and let Lango handle the rest.

Hosted checkout is the integration most stores want: your backend creates the payment, the customer pays on checkout.lango.co.zw, and Lango handles the prompt, the polling, the timeouts and the copy — including the states that protect you from double charges.

The flow

  1. Create the payment without a customer number:

    Terminal window
    curl https://api.lango.co.zw/v1/payments \
    -H "Authorization: Bearer sk_test_YOUR_KEY" \
    -H "Idempotency-Key: order-4417" \
    -H "Content-Type: application/json" \
    -d '{
    "amount": 10000,
    "currency": "USD",
    "method": "ecocash",
    "reference": "ORD-4417",
    "return_url": "https://yourstore.co.zw/orders/4417/thanks"
    }'

    Omitting customer.msisdn is what makes it hosted: the customer enters their own number on the checkout page, and the charge happens there.

  2. Redirect the customer to the checkout_url in the response (https://checkout.lango.co.zw/c/tok_...).

  3. The customer pays: they see your name, the amount, any customer-borne fee as its own line; they enter their EcoCash number; the PIN prompt lands on their phone with a visible countdown.

  4. You hear the outcome on your webhook (payment.succeeded / payment.failed), and the customer is offered the return_url back to you.

Rules worth knowing

What checkout does that you don’t have to

Direct API

Charge a known customer number in one call — for apps and platforms that own the whole flow.

If you collect the customer’s EcoCash number in your own product (a delivery app, a utility’s own portal), charge directly: one call with customer.msisdn, and the PIN prompt goes to the phone immediately.

Terminal window
curl https://api.lango.co.zw/v1/payments \
-H "Authorization: Bearer sk_test_YOUR_KEY" \
-H "Idempotency-Key: order-4417" \
-H "Content-Type: application/json" \
-d '{
"amount": 10000,
"currency": "USD",
"method": "ecocash",
"reference": "ORD-4417",
"customer": { "msisdn": "263771234567" },
"metadata": { "order_id": "4417", "depot": "harare-north" }
}'

The response is awaiting_customer: the prompt is on their phone. From here:

Your responsibilities in this mode

Because your UI owns the experience, the checkout page’s protections become your job:

  1. Show the fee before charging if your customer bears it. charged_amount in the create response (or preview beforehand) is the number they must see. Surprising people on the PIN prompt kills conversion and trust.
  2. Handle unknown honestly. “We’re confirming with EcoCash — don’t pay again.” Never “failed” while it’s still unknown. The whole page on this is required reading for direct integrations.
  3. Tell them to check their phone — with the masked number (customer_msisdn_masked in the response) and a countdown matching the payment’s expires_at.
  4. Validate numbers loosely, submit strictly. Accept 0771 234 567 from humans; send 263771234567. Lango normalises common Zimbabwean formats and rejects what can’t be a Zimbabwean mobile number, with a human-readable message you can show verbatim.

Metadata

metadata stores up to your own JSON object on the payment and comes back on every read and webhook — use it for your internal ids so reconciliation is a lookup, not a join.

Listing and pagination

GET /v1/payments?limit=25&starting_after=pay_... pages newest-first. Filters: status, method, currency, and search (matches references). The dashboard’s payment list is this endpoint.

Sandbox and test numbers

Ten deterministic numbers that force every outcome — break it before your customers do.

Test keys (sk_test_) hit the same API against a simulated provider. No real money can move on a test key, ever — the environments are separated at the key level, not by a flag you could get wrong.

The test numbers

The last two digits of the customer number choose the scenario. Any other suffix behaves as success.

NumberScenarioWhat happensFinal status
263771000001SuccessCustomer approves promptlycompleted
263771000002PIN declinedCustomer rejects the promptfailed
263771000003Customer timeoutPrompt delivered, never answeredexpired
263771000004Insufficient fundsWallet can’t cover itfailed
263771000005Invalid numberProvider rejects the MSISDNfailed
263771000006Duplicate referenceProvider claims it has seen this referencefailed
263771000007Delayed answerProvider answers “pending” twice, confirms on the third querycompleted
263771000008Callback never arrivesSuccess, but the provider never calls back — our resolver discovers it by askingcompleted
263771000009Provider HTTP 500The charge call itself errors — payment goes unknown, resolves on queryunknowncompleted
263771000010Connection timeoutNothing ever answers — the full unknown journeyunknownunresolved

What to actually test

Before going live, walk your integration through at least:

  1. The happy path (...01): create → webhook → fulfil.
  2. A definitive failure (...02): your UI says “declined”, your books say nothing owed.
  3. The delayed answer (...07): your code waits for the webhook rather than timing out at 5 seconds and guessing.
  4. The unknown path (...09): your UI says “confirming — don’t pay again”, not “failed”. This is the test that protects your customers from double charges.
  5. A retry with the same Idempotency-Key: confirm you get the same payment back, not a second one.

Forcing outcomes without numbers

POST /v1/test/simulate (test keys only) advances a payment directly — useful in automated test suites where you don’t want to wait for the resolver’s schedule:

Terminal window
curl https://api.lango.co.zw/v1/test/simulate \
-H "Authorization: Bearer sk_test_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "payment_id": "pay_...", "outcome": "completed" }'

Webhooks in the sandbox

Test-mode webhooks deliver and sign exactly like live ones. Point an endpoint at a tunnel (ngrok or similar) during development, and use POST /v1/events/{id}/replay to re-fire any event while debugging your handler.

Error codes

Every error, its cause, and the remedy — in one searchable table.

Every error is JSON in one envelope, with a message written for humans and a request_id you can quote to support:

{
"error": {
"type": "invalid_request",
"code": "parameter_invalid",
"message": "amount: Number must be greater than 0",
"param": "amount",
"request_id": "req_01J8XQ7M4K..."
}
}

Types

typeHTTPMeaning
invalid_request400The request itself is wrong — fix and resend
authentication_error401Missing or invalid API key
permission_error403Valid key, missing scope
not_found404No such object for this merchant
idempotency_conflict409Key reuse conflict — see below
rate_limited429Slow down; retry after a pause
api_error500Our fault. Safe to query, then retry

Codes

CodeCauseRemedy
parameter_invalidA field failed validation; param names itFix the named field; the message says what’s wrong
idempotency_key_requiredPOST /v1/payments without an Idempotency-Key headerAdd the header — why it’s mandatory
idempotency_key_reusedSame key, different bodyYou have two different requests sharing a key; give each intent its own key
request_in_progressSame key while the first attempt is still executingBack off briefly and retry; you’ll get the original response replayed
reference_in_useThis merchant already has a payment with this referenceFetch the existing payment instead of creating a duplicate
msisdn_invalidNot a Zimbabwean mobile numberNormalise to 2637XXXXXXXX; show the error message to the user verbatim
resource_missingThe id doesn’t exist, or belongs to another merchantCheck the id and which key (test vs live) you’re using — objects don’t cross environments
link_expiredThe payment link’s expires_at has passedCreate a fresh link
link_usedA single-use link was already paid and its payment can’t be resolvedCreate a fresh link; investigate the original payment in the dashboard
refund_amount_invalidRefund exceeds what remains refundableCheck prior partial refunds with GET /v1/refunds/{id}
rate_limitedToo many requestsExponential backoff; respect any Retry-After header
internalSomething failed on our sideQuery the payment before retrying — the charge may have succeeded — then retry with the same idempotency key

The retry rule

For 5xx and network failures on a POST: query first, then retry with the same Idempotency-Key. That order guarantees you never create a duplicate even when our answer was lost on the wire. For 400-class errors, retrying without changing the request will never help.

Going live

The verification checklist, live keys, and launch checks.

Test keys are yours from registration. Live keys are issued — never self-served — once your business is verified. Here is the whole path.

Verification checklist

In Dashboard → Settings, complete:

  1. Business details — legal name, trading name, address, contact.
  2. Documents — certificate of incorporation, CR14, proof of address, and a director’s ID. Reviewed by a human at Lango, usually within two business days.
  3. Bank account — where settlements go. We verify the account name matches your legal name; a name mismatch is a hard stop, not a warning. New and changed accounts have a 48-hour cooling-off before they can receive settlements.
  4. The agreement — your fee schedule and terms, accepted in the dashboard by a director.

When all four are green, Lango review flips your account to live and live keys appear in Dashboard → Developers.

Launch checks

Before pointing customers at it:

Key hygiene

After launch

Watch the dashboard’s home attention panel for the first days: it surfaces failing webhooks, unresolved payments, and anything needing action. Settlement advices should reconcile against your bank statement to the cent — if one doesn’t, that’s our bug: support@lango.co.zw with the advice number.

Changelog

Every API change, dated. Additive changes ship freely; breaking changes get a version and a migration window.

Lango’s compatibility promise: additive changes (new fields, new endpoints, new event types, new optional parameters) ship without notice — build your parsers to ignore fields they don’t know. Breaking changes get a dated entry here, a migration window, and direct email to affected merchants.

2026-09-12

2026-09-11

Embeddable checkout

Let customers pay in a popup on your own site with one script and a publishable key — no redirect, no backend required.

The embeddable checkout opens Lango’s hosted pay page in a modal on your own site. The customer never leaves your page; when they finish, the popup tells your page and closes. It needs one script and a publishable key — no backend required.

1. Get a publishable key

In your dashboard, go to Developers → Keys → Publishable keys and create one.

A publishable (pk_) key is safe to put in a web page. Unlike a secret sk_ key it grants no read access and cannot move money — the most it can do is start a checkout that can only ever pay you. Lock each key to the domains where your button lives (its allowed origins); the popup will only open on those sites.

2. Add the button

Drop in the script once, then mark any element with data-lango-checkout:

<script src="https://checkout.lango.co.zw/embed.js"></script>
<button
data-lango-checkout
data-lango-key="pk_live_your_key"
data-lango-amount="10000"
data-lango-currency="USD"
data-lango-reference="ORDER-1042"
>
Pay with EcoCash
</button>

data-lango-amount is in minor units10000 is USD 100.00. That is the whole integration: a click opens the popup, the customer approves on their phone, and you get the result.

AttributeRequiredWhat it is
data-lango-keyyesYour publishable pk_ key
data-lango-amountyesAmount in minor units (cents)
data-lango-currencynoUSD (default) or ZWG
data-lango-referenceyesYour order reference
data-lango-return-urlnoWhere to send the customer after paying

3. Or call it yourself

For full control, call Lango.checkout(...) from your own code and react to the result:

<script src="https://checkout.lango.co.zw/embed.js"></script>
<script>
document.querySelector('#pay').addEventListener('click', function () {
Lango.checkout({
key: 'pk_live_your_key',
amount: 10000,
currency: 'USD',
reference: 'ORDER-1042',
onSuccess: function () {
window.location = '/thank-you'
},
onClose: function () {
console.log('checkout closed')
},
})
})
</script>

With the button above, the amount lives in the page, so a determined customer could change it before paying. For anything that matters, create the checkout on your server with your secret key and hand the popup the resulting token — the amount is then fixed server-side and cannot be edited:

Terminal window
curl https://api.lango.co.zw/v1/checkout_sessions \
-H "Authorization: Bearer pk_live_your_key" \
-H "Content-Type: application/json" \
-d '{ "amount": 10000, "currency": "USD", "reference": "ORDER-1042" }'
# → { "checkout_token": "tok_...", "checkout_url": "https://checkout.lango.co.zw/c/tok_..." }
Lango.checkout({ key: 'pk_live_your_key', token: 'tok_...' })

You can also skip the popup entirely and just redirect the customer to the returned checkout_url.

How it’s kept secure

Reacting to the result

onSuccess fires when the payment completes. Treat it as a UI cue only — fulfil the order from the webhook, which is signed and can’t be spoofed. A payment that is still confirming is unknown, never failed: see the unknown state.

SDKs & plugins

Official Lango libraries for PHP, Node and Python, plus a WooCommerce plugin — so you don't hand-roll signing, idempotency or retries.

Lango ships official libraries so you never hand-roll the fiddly parts — request signing, idempotency keys, webhook signature verification, and the retry rules around the unknown state. Every library speaks the same API and follows the same behaviour described throughout these docs.

Libraries

What every library gives you

Getting the libraries

Install commands land here as each library publishes to its registry (Packagist, npm, PyPI and the WordPress plugin directory). In the meantime, ask us for access and start against the API reference and the five-minute quickstart — the SDKs are thin wrappers over exactly those calls.

Fulfil orders from the signed payment.succeeded webhook, which every library helps you verify — it is the source of truth, not the browser.