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.

LonghoPayExample
Secure payment to Buea Express Travel

Order 1001

Two tickets for the Saturday departure.

Step 1 of 4 · Details

Payment amount

FCFA 5,000

Mobile money number

655010010

Sending to +237 655 01 00 10

Your name

Aisha N.


Optional info Email for your receipt

Review payment
Show step
The hosted checkout your customer sees, step by step. The order and amounts are an example; the fee appears only when the customer is the one paying it.

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

  1. Register and verify your email address.

  2. Open your workspace and submit a sandbox application.

  3. A platform administrator reviews and approves it.

  4. Create a server credential and a webhook endpoint.

  5. 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. 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. 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. 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. 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. 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. 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. 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>" }
        }
      }
    }
    1. Match tenant.id, data.payment.external_reference and data.payment.metadata.checkout_session_id to the order you stored.
    2. Check the currency, then compare what you charged with fee.base_amount and what was collected with fee.total_collected.
    3. Mark the order paid and queue its fulfilment in one transaction, keyed on the payment, so a replayed event cannot fulfil it twice.
    4. 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. 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. 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. 10. Go live

    1. Submit a production application with the same integration type and your real return domains, and wait for its approval.
    2. 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.
    3. 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.
    4. 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.

Create a developer account