For developers
Add mobile money payments to your website or app.
LonghoPay gives your server one request to create a hosted checkout, signed webhooks to learn the outcome, and a sandbox that behaves like the real networks. Your customers pay with MTN Mobile Money or Orange Money, and the money reaches the business you build for.
No merchant business, payout number or verification is needed to start. The sandbox costs nothing.
Order 1001
Two tickets for the Saturday departure.
Payment amount
FCFA
Mobile money number
655010010
Your name
Aisha N.
Optional info Email for your receipt
What you get
Hosted checkout
One authenticated request returns a checkout URL. The LonghoPay page handles the phone number, the fees and the approval on the customer’s handset.
Signed webhooks
Payment succeeded, failed, processing and expired events, signed with HMAC-SHA256, retried with backoff and replayable from the portal.
A real sandbox
A sandbox credential reaches a simulator. The payer number chooses the outcome, so you can rehearse a decline or a no-answer without a real phone.
Logs, usage and status
Request logs, delivery history and API usage per application, plus a status read for the moments a webhook is late.
How the pieces talk
Four calls and one webhook. The two dashed arrows are the ones that never prove a payment on their own.
- 1. Create checkout session
- 2. Redirect to checkout_url
- 3. Approval on the handset
- 4. Signed webhook
- – Return to your site (not proof of payment)
- – Status read if a webhook is late (not proof of payment)
What your server sends
The same request in three languages. Keep the credential on the server; the browser only ever receives checkout_url.
curl -X POST https://api.longhopay.com/api/v1/developer/v1/checkout-sessions \
-H "Authorization: Bearer lp_sandbox_<key-id>_<secret>" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "X-Request-ID: 6f0d2c1e-order-1001" \
-H "Idempotency-Key: order-1001-checkout-1" \
-d '{
"external_reference": "ORDER-1001",
"amount": 5000,
"currency": "XAF",
"description": "Order 1001",
"return_url": "https://shop.example.com/payments/return",
"expires_in_seconds": 900,
"metadata": { "order_id": "1001" }
}'From account to first payment
Register and verify your email address.
Open your workspace and submit a sandbox application.
A platform administrator reviews and approves it.
Create a server credential and a webhook endpoint.
Integrate, rehearse in the sandbox, then go live with a production application.
Integration guide, step by step
From an empty account to a live payment. Each step names the screen or the request it involves, and nothing in it assumes a merchant business already exists.
1. Create your developer account and open a workspace
Register with your name and email address, open the verification link and choose a password. Then open the developer portal and open your workspace. No merchant business, payout number or business verification is needed for any of this.
One person may hold both a merchant account and a developer workspace, but each is authorised separately: registering as a developer grants no access to any merchant’s payments.
2. Submit an application and wait for approval
In Applications, submit one application per environment. Start with a sandbox application: it reaches a simulated network, so nothing you do with it moves money.
- Name and description: what the platform administrator reads when reviewing your request.
- Integration type: hosted checkout for a website or app that sends the customer to a LonghoPay page; server API for a backend that only calls the API.
- Return domains: the HTTPS hosts your return URLs will use, for example shop.example.com. A checkout whose return URL is on another host is refused.
A platform administrator reviews every application, sandbox and production alike. Until it is approved you cannot issue credentials or register webhooks. Changing the integration type, the return domains or the IP restrictions withdraws approval and asks for another review.
3. Create a server credential
Once approved, open API credentials and create a credential with only the scopes your server needs. For a hosted checkout that is payments:create, payments:read and webhooks:manage.
The shape of a credential lp_sandbox_<key-id>_<secret>4. Create a checkout session from your server
Your website or app asks your server to start a payment. Your server records the order, then asks LonghoPay for a checkout session. Every request carries the credential as a Bearer token, a unique X-Request-ID, and an Idempotency-Key that you keep with the order.
Create a checkout session curl -X POST https://api.longhopay.com/api/v1/developer/v1/checkout-sessions \ -H "Authorization: Bearer lp_sandbox_<key-id>_<secret>" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -H "X-Request-ID: 6f0d2c1e-order-1001" \ -H "Idempotency-Key: order-1001-checkout-1" \ -d '{ "external_reference": "ORDER-1001", "amount": 5000, "currency": "XAF", "description": "Order 1001", "return_url": "https://shop.example.com/payments/return", "expires_in_seconds": 900, "metadata": { "order_id": "1001" } }'- external_reference
- Required. Your own identifier, up to 120 characters, unique per payment attempt.
- amount
- Required. A positive integer in francs: 5000 means 5 000 XAF. No decimals.
- currency
- Optional. XAF, which is also the default.
- description
- Required, up to 255 characters. Shown to the customer on the checkout page.
- return_url
- Required. An HTTPS URL on one of the application’s approved domains, up to 1 000 characters.
- expires_in_seconds
- Optional, 60 to 86 400. Defaults to 900, and is clamped to the deployment’s maximum.
- metadata
- Optional object echoed back in webhooks. Never put credentials or personal data in it.
The answer is 201 with the session under data. Store its id, external_reference, amount, currency, expires_at and checkout_url. The status starts as OPEN; payment_status and status_token stay null until a customer starts paying.
The response, abridged { "success": true, "responseCode": 201, "data": { "id": "01J9X3M7Q2K8R5T1V4W6Y8Z0AB", "external_reference": "ORDER-1001", "amount": 5000, "currency": "XAF", "status": "OPEN", "payment_status": null, "status_token": null, "expires_at": "2026-09-16T12:15:00Z", "checkout_url": "https://longhopay.com/checkout/<token>" }, "request_id": "01J9X3M7Q2K8R5T1V4W6Y8Z0AC" }Retrying is safe. The same key with the same body returns the existing session, again with 201. The same key with a different amount, reference, return URL or metadata is refused with 409 IDEMPOTENCY_KEY_CONFLICT. After a timeout, retry with the original key and body, never with a new reference while the original is unresolved.
5. Send the customer to the checkout page
Redirect the customer’s browser to checkout_url. The LonghoPay page shows the amount and the fees, asks for the customer’s name and MTN or Orange number, and sends the approval request to their handset. Your credential is never used by the browser.
Once the payment has succeeded or failed, the page offers a button back to your return URL with longhopay_reference=<session id> appended. A customer arriving on that page is not proof of payment: look the order up in your own records and show what your backend knows.
6. Receive webhooks and verify their signature
In Webhooks, add an HTTPS endpoint subscribed at least to payment.succeeded and payment.failed; add payment.processing and payment.expired to follow the whole lifecycle. The signing secret is shown once, like a credential.
Headers on every delivery X-MobilePay-Event-Id: evt_01J9X3M7Q2K8R5T1V4W6Y8Z0AD X-MobilePay-Timestamp: 1789560000 X-MobilePay-Signature: t=1789560000,v1=<hex-hmac-sha256> webhook-id: evt_01J9X3M7Q2K8R5T1V4W6Y8Z0AD webhook-timestamp: 1789560000 webhook-signature: v1,<base64-hmac-sha256>Read the raw bytes before parsing JSON. X-MobilePay-Signature is an HMAC-SHA256 over <timestamp>.<raw body>, keyed with the whole signing secret including its whsec_ prefix. Compare in constant time, reject a stale timestamp, and store the event id under a unique constraint so a redelivery is a no-op. Answer 2xx quickly and do the real work from your own queue.
A Node.js verifier const crypto = require('node:crypto'); // rawBody is a Buffer of the exact bytes received; parse JSON only after this returns true. function verifyLonghoPay(rawBody, timestamp, signatureHeader, secret, nowSeconds = Math.floor(Date.now() / 1000)) { if (!Buffer.isBuffer(rawBody) || !/^\d+$/.test(timestamp ?? '')) return false; const ts = Number(timestamp); if (!Number.isSafeInteger(ts) || Math.abs(nowSeconds - ts) > 300) return false; const parts = String(signatureHeader ?? '').split(',').map((s) => s.trim()); if (!parts.includes(`t=${timestamp}`)) return false; const expected = crypto.createHmac('sha256', secret) .update(timestamp + '.').update(rawBody).digest(); return parts.some((part) => { if (!/^v1=[a-fA-F0-9]{64}$/.test(part)) return false; return crypto.timingSafeEqual(expected, Buffer.from(part.slice(3), 'hex')); }); }If you would rather use a Standard Webhooks library, give it the signing_secret_standard value and let it verify the webhook-id, webhook-timestamp and webhook-signature headers instead. Verify one scheme completely; do not mix the two.
7. Confirm the payment and fulfil the order
A payment.succeeded event looks like this. Fields may be added; never removed.
payment.succeeded { "id": "evt_01J9X3M7Q2K8R5T1V4W6Y8Z0AD", "type": "payment.succeeded", "api_version": "2026-09-11", "created_at": "2026-09-16T12:03:41.000Z", "tenant": { "id": "<merchant-public-id>" }, "data": { "payment": { "id": 123, "external_reference": "ORDER-1001", "status": "succeeded", "amount": 5000, "currency": "XAF", "fee": { "base_amount": 5000, "total_collected": 5000, "fee_payer": "MERCHANT" }, "trid": "<payment-reference>", "receipt_number": "<receipt-number>", "metadata": { "checkout_session_id": "01J9X3M7Q2K8R5T1V4W6Y8Z0AB" }, "customer": { "id": "<customer-public-id>" } } } }- Match tenant.id, data.payment.external_reference and data.payment.metadata.checkout_session_id to the order you stored.
- Check the currency, then compare what you charged with fee.base_amount and what was collected with fee.total_collected.
- Mark the order paid and queue its fulfilment in one transaction, keyed on the payment, so a replayed event cannot fulfil it twice.
- Keep success final. A processing or failed event that arrives later must never un-pay an order.
Delivery is at least once: a failed delivery is retried up to seven times over roughly a day, and any event can be replayed from the portal. Your receiver must treat a duplicate as already handled.
8. Recover when a webhook does not arrive
Run a background check for orders still open past their expiry. Fetch the session with its token, then the payment status with the status_token the session returns.
The two status reads GET https://api.longhopay.com/api/v1/checkout-sessions/<token> GET https://api.longhopay.com/api/v1/payment-intents/status/<status_token>- OPEN, no payment
- Wait until expires_at, then release the order.
- EXPIRED, no payment
- Release the order. An unused session sends no webhook.
- PROCESSING
- Keep waiting. Do not start a second charge.
- REQUIRES_RECONCILIATION
- Keep the order unresolved and contact LonghoPay operations.
- SUCCEEDED
- Validate the reference, amount and currency, then fulfil exactly once.
- FAILED or EXPIRED payment
- Offer a new reference only if the order is still available.
Webhooks and this check can race. Both must go through the same fulfilment deduplication.
9. Rehearse in the sandbox
A sandbox credential reaches a simulator, never MTN or Orange. The outcome is scripted from the payer number typed on the checkout page.
- Ends in 0000
- The payment is declined.
- Ends in 0001
- The payer never answers. It stays pending until you force an outcome.
- Anything else
- The payment succeeds.
To force the pending case to an ending, call the sandbox route with the payment reference, the trid from the status resource.
Force an outcome curl -X POST https://api.longhopay.com/api/v1/developer/v1/sandbox/collections/<trid> \ -H "Authorization: Bearer lp_sandbox_<key-id>_<secret>" \ -H "Content-Type: application/json" \ -d '{"outcome":"SUCCESS"}'FAILED is the other accepted outcome. Rehearse success, decline, the unresolved payment, an unused session expiring, a lost response retried with the same key, a webhook outage and its replay, and duplicate or out-of-order events. Sandbox payments are never settled and never appear on any merchant’s dashboard.
10. Go live
- Submit a production application with the same integration type and your real return domains, and wait for its approval.
- Give its application ID to the owner of the business that will receive the money. They authorise it under Settings → Connected applications in their merchant portal; the business must be active and verified. One application has one receiving merchant, fixed once authorised.
- Create a production credential and a production webhook endpoint, and store the new secrets. Sandbox and production keys, secrets, references and data are entirely separate.
- Take one real payment of a small amount and confirm that the webhook, the status check and the merchant’s payment list agree.
The merchant owns the funds and the payouts. Revoking the authorisation stops new checkouts; payments already submitted still reach their recorded outcome.
Errors and status codes
Branch on the HTTP status and the code field in the response, never on the message text.
- 400
- Invalid state or command. Do not retry unchanged.
- 401
- Missing, invalid, expired or revoked credential.
- 403
- The scope, IP restriction, environment or merchant authorisation refuses this.
- 404
- Absent, or deliberately hidden from this credential.
- 409
- Duplicate reference or idempotency conflict. Reconcile with the existing operation.
- 422
- Named input errors under errors. Correct them.
- 429
- Rate limited. Honour the retry guidance with bounded jitter.
- 500 / 503
- Retry only what is idempotent, keep the request_id, and pause on 503.
A client timeout during a money command is an unknown outcome. Query by your stored reference, reuse the same idempotency key, and let the webhook or the status check settle it. Keep request_id from every response for support.
The complete OpenAPI specification can be downloaded from inside the developer portal.
Start building today
Create your developer account, submit a sandbox application, and make your first test payment.