Skip to main content
Build your own event pages and checkout form, and drive the order from your server. Mobile money is fully native: the buyer approves a prompt on their phone without leaving your site. Buyers pay with mobile money (MTN or Airtel) today; card is coming soon. You need: a server, and an API key with Read events and Create orders. Add Read orders only if you want to poll order status (it returns personal data); webhooks work without it.
The client secret (LCSSECK-…-X) is server-only. It mints tokens that can create and pay orders. Keep it in your server’s environment or secret manager, never in a browser, a mobile app or source control. The token endpoint refuses any request that carries an Origin header, so it cannot be called from a web page even by mistake.
Every key, token and id on this site is a made-up example value. Your real keys are issued to you in the Laces app, under Settings → Developers.

The flow

1. Get a token

Cache it for its hour. See Authentication for the full helper.

2. Show events and prices

List your events, then read one for its ticket types and fees.
Response (abbreviated)
What to do with it:
  • Prices. Show price_minor, laces_fee_minor and total_minor exactly as returned. The top-level pair is MTN mobile money; airtel has its own. Never compute fees. See Fees and totals.
  • payment_methods lists the methods you can sell right now. Today it is ["momo", "airtel"], and each ticket’s card is null: card is coming soon. Offer only the methods in this list, and build your picker from it so card appears on its own once Laces turns it on.
  • payments_enabled: false means your organization isn’t cleared to take payments yet. Show “Registration not open” rather than a buy button that can’t work.
  • waiver_required: true means you must show the event’s waiver (the text covered by your agreement with the organizer), collect the buyer’s agreement, and send waiver_accepted: true with this exact waiver_version on the order.
  • additional_questions are the organizer’s questions. Ask them, and send answers in custom_fields, keyed by key.
  • Distances. If ticket types have distance_km, send each participant’s distance_km on the order.
Cache event reads for a minute or two.
A Tickets table listing Free Fun Run at 0 UGX and 10K Entry at 50,000 UGX with a 1,950 UGX mobile money fee and a 51,950 UGX total

A partner page built from the event read: each ticket's price, mobile money fee and total, shown exactly as returned. The example site and race are made up.

3. Create the order

When the buyer submits, create the order. This holds their places until hold_expires_at; no money moves yet. Choose payment_method now: it sets the price. momo is MTN Mobile Money only (the default) and airtel is Airtel Money. Ask the buyer how they’ll pay, or infer it from their number’s network, before you create the order. Paying a momo order from an Airtel number (or an airtel order from an MTN number) is refused with 409 reprice_required, and you must create a new order with the right method. card is refused with 409 payment_method_unavailable until card is available (check the event’s payment_methods).
A 'Your details' form with a ticket picker set to 10K Entry, full name, email and mobile money number fields, an 'I accept the race waiver' checkbox and a Register and pay button

A partner's own registration form: ticket, buyer details, mobile money number and the waiver checkbox.

Response 201 (200 on an idempotent replay):
  • order.total is the amount due. Show it on your confirm step. A multi-ticket cart costs less than the per-ticket quotes added up, because part of the fee is per order.
  • checkout_lines is a ready-made breakdown for your UI.
  • order.id is what you pay. order_code is what the buyer sees on their ticket and receipt.
  • order is the same partner order view that GET /orders/{orderId} returns (without registrations): timestamps are ISO 8601 UTC, and buyer.phone is in E.164 form (+2567…) whatever format the buyer typed.
Every order you create is stamped channel: partner_api for audit.

4. Pay

Call pay with a new Idempotency-Key (not the create key).
Send the payer’s number as msisdn: an MTN Uganda number for a momo order, an Airtel Uganda number for an airtel order.
Laces sends a prompt to the payer’s phone, and the call returns at once with payment_status: "prompt_sent". Tell the buyer to check their phone and enter their PIN, then wait for the result (step 5). Branch on next_action.kind.
A message reading 'Check your phone to approve the payment of 51,950 UGX', with the order code

Illustrative: a partner page telling the buyer to approve the prompt on their phone.

A number on the wrong network for the order’s payment_method (an Airtel number on a momo order, or an MTN number on an airtel order) is 409 reprice_required, and no prompt is sent. Create a new order with the right payment_method (a new create idempotency key), then pay that one.409 prompt_in_progress means a prompt is already waiting on the phone. It is not a failure: wait the seconds in Retry-After (also in details.retry_after_seconds) before trying again.

5. Learn the result

Webhooks (recommended). order.paid or order.failed arrives at your endpoint. Match it to your cart with attribution.partner_reference. See Webhooks. Polling (needs orders:read). Read the order every few seconds until status leaves payment_initiated / prompt_sent:
Fulfil only when status is paid.
A green confirmation reading 'You're registered!' with an order code and the email the confirmation was sent to

The partner page's own confirmation once the order is paid.

Why it failed: failure_kind

A failed or expired order carries failure_kind, a short, safe reason you can branch on. It is on order reads, on the order.failed webhook (data.order.failure_kind) and on the pay response. It is null while the order hasn’t failed, and it is never the payment provider’s own wording. Treat an unknown value as other: more kinds may be added.

Trying again after a failure

If an attempt fails or expires, the pay response also includes retry_safe:
  • true: a fresh attempt can’t double-charge. Call pay again with a new idempotency key (and the same or a different number on the same network).
  • false: don’t retry automatically. Check the order’s status first.
failure_kind: "provider_not_reached" always comes with retry_safe: true. 409 hold_expired means the places were released: create a new order.

Errors

The full list is on Errors.

A complete server example (Node)