Render events and take the order in your own UI. Your server creates and pays orders with an OAuth token.
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.
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 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.
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 partner's own registration form: ticket, buyer details, mobile money number and the waiver checkbox.
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.
Field
Required
Notes
event_id
Yes
Your organization’s event
lines
Yes
[{ ticket_type_id, quantity, participants? }], quantity 1 to 500. participants is one object per ticket.
buyer_name, buyer_email, buyer_phone
Yes
Receipts and tickets go to buyer_email
payment_method
No
momo (default) or airtel. Sets the price. card is refused with 409 payment_method_unavailable until card is available.
waiver_accepted, waiver_version
When waiver_required
true, and the event’s current version
custom_fields
No
Answers to additional_questions
discount_code
No
donation_amount
No
Whole number, minor units
partner_reference
No
Your reference, up to 128 characters. Echoed in order reads and webhooks.
Every order you create is stamped channel: partner_api for audit.
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.
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.
Card isn’t available to partners yet. Today an order created with
"payment_method": "card", or a pay call on an order priced for card, is
refused with 409 payment_method_unavailable, and each ticket’s card
quote is null.When card is enabled, it will appear in the event’s payment_methods and
card details will still never go through the Partner API: pay will charge
nothing and return next_action: { "kind": "redirect", "redirect_url" },
a Laces-hosted page where the buyer pays. The buyer is not sent back to
your site from that page, so you would learn the result from the
order.paid webhook or an order read.
When order.total is 0, pay needs no number and confirms the order
immediately:
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:
async function waitForOutcome(orderId, { timeoutMs = 180_000 } = {}) { const until = Date.now() + timeoutMs; while (Date.now() < until) { const order = await laces("GET", `/orders/${orderId}`); if (!["cart", "payment_initiated", "prompt_sent"].includes(order.status)) return order; await new Promise((r) => setTimeout(r, 4000)); } return null; // still pending: keep waiting for the webhook}
Fulfil only when status is paid.
The partner page's own confirmation once the order is paid.
Final status
Meaning
paid
Confirmed. Registrations are issued and the buyer is emailed.
failed
The payment failed or was declined. Offer to try again.
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.
failure_kind
What happened
What to tell the buyer
provider_not_reached
The charge never reached the payment provider. Nothing was charged, and it is safe to try again straight away.
”We couldn’t reach the payment service. You haven’t been charged. Please try again.”
timed_out
The buyer didn’t approve the prompt in time, or it expired
”The payment wasn’t approved in time. Try again and approve the prompt on your phone.”
declined
The buyer declined or cancelled the prompt, or can’t pay (for example not enough balance or a wrong PIN)
“The payment was declined. Check your mobile money balance, or try another number.”
other
Anything else
”The payment didn’t go through. Please try again, or contact the organizer.”
Treat an unknown value as other: more kinds may be added.