Escrow as a service. Built for Cameroon's platforms.
Integrate Fonlok into any marketplace or platform with a single REST API. Seller payouts go directly to their Mobile Money number. No Fonlok account required on either side.
Use the sandbox below to test every flow end-to-end before going live. No real money moves. When you're ready, a production key unlocks the full live API.
Complete reference for all production endpoints. The live API processes real Mobile Money payments in XAF. Every request must be authenticated with a sk_live_ key obtained from your Fonlok account. Keys are available to approved integration partners — contact support@fonlok.com to apply.
Base URL
https://fonlok-backend-production.up.railway.app
Authorization header — required on every request
Authorization: Bearer sk_live_a1b2c3d4...
Fees: Fonlok charges a 3% platform fee deducted at release. The POST /v1/payments/release response includes the exact fee and net amount. Currency: Only XAF (Central African Franc) is supported.
GET/v1/ping
Health check. Verifies that your live key is valid and the API is reachable. Returns a JSON object confirming the environment. Does not initiate any transaction.
{
"object": "api_status",
"status": "ok",
"environment": "production",
"key_label": "My Integration",
"message": "Fonlok live API is operational. Real transactions will be processed.",
"timestamp": "2025-01-15T10:30:00.000Z"
}
Invoices
Create and retrieve escrow invoices. Each invoice represents one transaction — funds are held by Fonlok until you call POST /v1/payments/release. No Fonlok account is required for the seller or buyer.
POST/v1/invoices
Create a new escrow invoice on behalf of any seller on your platform. Returns a payment_url you can redirect the buyer to, or use POST /v1/payments/initiate to trigger a direct MoMo push instead.
If you supply a reference that already exists for your API key, the request returns 409 duplicate_reference. The payment_url is always present and active — share it with the buyer to let them pay directly through the Fonlok payment page.
Request body
Field
Type
Required
Description
title
string
required
Invoice title, max 200 chars. Shown to buyer on the payment page.
amount
number
required
Invoice amount in XAF. Minimum 500 XAF.
currency
string
optional
Currency code. Only "XAF" is supported. Defaults to "XAF".
seller_name
string
required
Seller's full name, max 200 chars.
seller_email
string
required
Seller's email. Fonlok sends payout confirmation and PDF receipt here.
seller_phone
string
required
Seller's MTN or Orange MoMo number in international format, e.g. 237670000001. This is where the payout is sent.
buyer_email
string
optional
Buyer's email. Fonlok sends payment confirmation and PDF receipt here.
buyer_phone
string
optional
Buyer's MoMo number. Can also be supplied later when calling POST /v1/payments/initiate.
description
string
optional
Item description, max 2000 chars.
reference
string
optional
Your internal order or reference ID, max 200 chars. Must be unique per API key. Returned as external_reference.
expires_at
string
optional
ISO 8601 expiry date, e.g. "2026-12-31". After this date the payment URL becomes inactive.
Retrieve a single invoice by its ID (the invoicenumber returned on creation). Returns the complete invoice object including current status, payment URL, timestamps, and — when disputed — secure chat links for both parties.
Status values:pending → paid → completed (or disputed). Also possible: delivered, cancelled, refunded. The payment_url field is always returned regardless of status.
Initiate and settle payments. A confirmed payment puts funds in escrow. Call /v1/payments/release when the buyer is satisfied, or /v1/payments/dispute if there is a problem.
POST/v1/payments/initiate
Send a Mobile Money USSD push prompt to the buyer's phone via Campay. The buyer receives a pop-up on their handset and approves or declines the charge. Returns a reference UUID for polling status. Only invoices in 'pending' status can be initiated.
Number format: 12 digits starting with 237. MTN numbers start with 2376, Orange with 2372. The reference UUID in the response is used for polling status. Prefer webhooks over polling in production — the payment.confirmed event fires immediately when Campay confirms.
Request body
Field
Type
Required
Description
invoice_id
string
required
Invoice ID returned when the invoice was created.
phone_number
string
required
Buyer's MTN or Orange MoMo number in international format, e.g. 237670000000. Fonlok auto-detects the network (6xx = MTN, 2xx = Orange).
buyer_email
string
optional
Buyer email for confirmation emails. Overrides the buyer_email set on the invoice.
HTTP 201 Created
{
"object": "payment",
"reference": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"invoice_id": "42-a1b2c3d4e5f6",
"amount": 850000,
"currency": "XAF",
"provider": "mtn",
"phone_number": "237670000000",
"status": "pending",
"message": "A mtn Mobile Money prompt has been sent to 237670000000. The buyer must approve it on their phone.",
"created_at": "2025-01-15T10:31:00.000Z"
}
GET/v1/payments/:reference/status
Poll the status of a payment using the reference UUID returned by POST /v1/payments/initiate. Returns the payment status and the associated invoice status.
Payment status:pending (awaiting buyer action), paid (confirmed, funds in escrow), failed (declined or timed out). Invoice status: mirrors the invoice lifecycle — pending, paid, completed, disputed, cancelled.
Release escrowed funds to the seller after the buyer confirms receipt. Fonlok deducts a 3% platform fee and disburses the net amount directly to the seller's MoMo phone. Sends PDF receipt emails to both seller and buyer. Only 'paid' invoices created via the API can be released through this endpoint.
Idempotency: Concurrent calls for the same invoice are safe — only one will succeed. If the Campay payout fails, the invoice status is restored to paid so you can retry safely. This endpoint fires a payment.released webhook event.
Request body
Field
Type
Required
Description
invoice_id
string
required
Invoice ID to release. Must be in 'paid' status and created via the API.
{
"object": "release",
"invoice_id": "42-a1b2c3d4e5f6",
"status": "completed",
"gross_amount": 850000,
"platform_fee": 17000,
"seller_receives": 833000,
"currency": "XAF",
"seller_phone": "237670000001",
"message": "833,000 XAF dispatched to 237670000001 via Mobile Money.",
"released_at": "2025-01-15T10:35:00.000Z"
}
POST/v1/payments/dispute
Flag a paid invoice as disputed when the buyer raises a complaint before funds are released. Fonlok freezes the funds, creates a moderated chat thread, and sends chat links to both parties by email. Returns secure chat links for buyer and seller. Only 'paid' API-created invoices can be disputed.
Chat links are sensitive. Each link is role-scoped — send the buyer link only to the buyer and the seller link only to the seller. You can retrieve both links again later via GET /v1/invoices/:invoice_id. Contact support@fonlok.com with the invoice ID to initiate resolution.
Request body
Field
Type
Required
Description
invoice_id
string
required
Invoice ID to dispute. Must be in 'paid' status.
reason
string
required
Concise dispute reason visible to the seller and support team. Max 1,000 characters.
context
string
optional
Additional supporting detail for the support team only. Not shown directly to the seller. Max 10,000 characters. Include order history, screenshots descriptions, timestamps, etc.
curl https://fonlok-backend-production.up.railway.app/v1/payments/dispute \
-X POST \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"invoice_id": "42-a1b2c3d4e5f6",
"reason": "Item was not as described. Phone has a cracked screen.",
"context": "Buyer reported this on 2025-01-15 at 14:30. Photos: ..."
}'
Response
JSON
{
"object": "dispute",
"invoice_id": "42-a1b2c3d4e5f6",
"status": "disputed",
"reason": "Item was not as described. Phone has a cracked screen.",
"message": "Invoice flagged as disputed. Funds are held. Share the chat links below with each party so they can communicate with Fonlok support.",
"disputed_at": "2025-01-15T10:40:00.000Z",
"chat_links": {
"buyer": "https://fonlok.com/chat/42-a1b2c3d4e5f6?token=abc123def456...&role=buyer",
"seller": "https://fonlok.com/chat/42-a1b2c3d4e5f6?token=789ghi012jkl...&role=seller"
}
}
POST/v1/wallet/pay
Pay an invoice directly from a pre-funded platform wallet — an alternative to the MoMo USSD push. The buyer's wallet balance is debited atomically and the invoice moves to 'paid' status immediately. The buyer receives a confirmation email with a release link. Use for marketplace users who have deposited funds in advance.
Insufficient funds: If the wallet balance is below the invoice amount, the request returns 409 insufficient_funds with current_balance and invoice_amount in the error body. The debit is atomic — no partial charges occur.
Request body
Field
Type
Required
Description
invoice_id
string
required
Invoice ID to fund. Must be in 'pending' status and created via the API.
user_ref
string
required
Your platform's unique reference for the wallet holder (e.g. your internal user ID). Must match a wallet registered for this API key.
{
"invoice_id": "42-a1b2c3d4e5f6",
"invoice_name": "iPhone 15 Pro",
"amount_paid": 850000,
"new_balance": 1500000,
"currency": "XAF",
"status": "funded",
"release_code": "XKCD4891",
"message": "Invoice funded from wallet and held in escrow. Buyer will receive a release link by email."
}
Wallet management
Manage pre-funded platform wallets. Wallets are created automatically on the first deposit. Each wallet is identified by a user_ref — your platform's own user ID or reference string.
POST/v1/wallet/deposit/initiate
Send a MoMo USSD push to a user's phone to top up their platform wallet. The user is charged amount + ceil(amount × 1.5%); their wallet is credited with the original amount on confirmation. Poll GET /v1/wallet/deposit/:reference/status to confirm.
The wallet is created automatically if it does not yet exist for this user_ref. The reference in the response is the Campay transaction reference — use it to poll status.
Request body
Field
Type
Required
Description
amount
integer
required
Amount to credit to the wallet in XAF. Minimum 100 XAF. The user is charged this amount plus a 1.5% fee.
phone
string
required
User's MTN or Orange MoMo number in international format, e.g. 237670000000.
user_ref
string
required
Your platform's unique reference for this user (e.g. your internal user ID). Creates the wallet on first deposit.
description
string
optional
Optional description stored on the transaction record.
HTTP 202 Accepted
{
"transaction_id": 88,
"reference": "campay-ref-abc123",
"user_ref": "user_1234",
"amount_requested": 10000,
"amount_charged": 10150,
"fee": 150,
"currency": "XAF",
"status": "pending",
"message": "Payment prompt sent to user's phone. Poll GET /v1/wallet/deposit/:reference/status to confirm."
}
GET/v1/wallet/deposit/:reference/status
Poll Campay for the status of a pending deposit. On the first SUCCESSFUL response the wallet is credited atomically. Safe to call multiple times — idempotent.
Withdraw funds from a user's wallet directly to a MoMo number. Fonlok covers Campay's ~1% disbursement fee — no fee is charged to the user. The debit is atomic; the balance is restored if the Campay disbursement fails.
Insufficient funds returns 409 insufficient_funds with current_balance. The disbursement is synchronous — the MoMo transfer is initiated immediately and the response includes the Campay reference.
Request body
Field
Type
Required
Description
amount
integer
required
Amount to withdraw in XAF. Minimum 100 XAF.
phone
string
required
Destination MoMo number in international format, e.g. 237670000000.
user_ref
string
required
Your platform reference for the wallet holder to debit.
description
string
optional
Optional description stored on the transaction record.
Register URLs to receive real-time signed notifications for invoice and payment events. Using webhooks is strongly preferred over polling — they fire as soon as an event occurs.
POST/v1/webhooks/register
Register a URL to receive Fonlok webhook event payloads. The response contains a signing secret (whsec_...) shown exactly once — store it immediately. You can register up to 5 active endpoints per API key.
The secret is shown once only. Copy it immediately and store it in a secure environment variable (e.g. FONLOK_WEBHOOK_SECRET). If lost, deactivate this endpoint and register a new one.
Request body
Field
Type
Required
Description
url
string
required
Your webhook endpoint URL. Must be a valid HTTPS URL. Fonlok POSTs signed JSON to this URL for each event.
label
string
optional
A human-readable label for this endpoint, max 80 chars. e.g. "Production webhook".
HTTP 201 Created
{
"object": "webhook",
"id": 12,
"url": "https://yourapp.com/webhooks/fonlok",
"label": "Production webhook",
"secret": "whsec_a1b2c3d4e5f6789012345678901234567890abcdef0123456789",
"created_at": "2025-01-15T10:30:00.000Z",
"_note": "Store the secret securely. It will not be shown again."
}
GET/v1/webhooks
List all registered webhook endpoints for your API key, including active status and the last time an event was delivered. Secrets are never returned after initial registration.
Deactivate a registered webhook by its numeric ID. The endpoint is marked inactive and will no longer receive deliveries. The ID is returned from GET /v1/webhooks.
Fonlok POSTs a signed JSON payload to your registered endpoint each time one of these events occurs. Always verify the X-Fonlok-Signature header before processing. Events: payment.initiated · payment.confirmed · payment.disputed · payment.dispute_resolved · payment.released · payout.completed
Verifying webhook signatures
Every event delivery includes an X-Fonlok-Signature header containing an HMAC-SHA256 hex digest of the raw request body, signed with your webhook secret. Verify it before trusting any payload.
Node.js — Express example
import crypto from "crypto";
function verifyFonlokWebhook(rawBody, signatureHeader, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody) // raw Buffer or string — before JSON.parse
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signatureHeader, "hex"),
Buffer.from(expected, "hex")
);
}
app.post(
"/webhooks/fonlok",
express.raw({ type: "application/json" }),
(req, res) => {
const sig = req.headers["x-fonlok-signature"];
if (!verifyFonlokWebhook(req.body, sig, process.env.FONLOK_WEBHOOK_SECRET)) {
return res.status(400).send("Invalid signature");
}
const event = JSON.parse(req.body.toString());
switch (event.type) {
case "payment.confirmed": /* mark order as paid */ break;
case "payment.disputed": /* flag order for review */ break;
case "payment.dispute_resolved": /* act on event.decision */ break;
case "payment.released": /* notify seller */ break;
case "payout.completed": /* alternate release path */ break;
}
res.sendStatus(200);
}
);
payment.initiated
Fired when POST /v1/payments/initiate succeeds. The MoMo prompt has been sent to the buyer but not yet approved.
Fired when POST /v1/payments/dispute is called. Funds are frozen pending resolution. Includes chat_links for buyer and seller, and the optional context field if one was supplied.
Payload
{
"object": "event",
"type": "payment.disputed",
"invoice_id": "42-a1b2c3d4e5f6",
"amount": 850000,
"currency": "XAF",
"reason": "Item was not as described.",
"context": "Buyer contacted seller on 2025-01-14...", // present only if supplied
"timestamp": "2025-01-15T10:40:00.000Z",
"chat_links": {
"buyer": "https://fonlok.com/chat/42-a1b2c3d4e5f6?token=abc123...&role=buyer",
"seller": "https://fonlok.com/chat/42-a1b2c3d4e5f6?token=def456...&role=seller"
}
}
payment.dispute_resolved
Fired by the Fonlok admin when a dispute is closed. The decision field is either "seller" (funds released to seller) or "buyer" (buyer refunded). Only fired for API-created invoices.
Fired when the buyer releases funds via the confirmation email link (the non-API release flow). Equivalent to payment.released but triggered by the buyer's manual confirmation rather than a direct API call.
All errors use a consistent shape. The error field is machine-readable; message is human-readable. Error handling you write against the sandbox works identically in production.
JSON
// 400 Validation failed
{ "error": "validation_error", "message": "amount must be at least 500 XAF." }
// 401 Missing or invalid API key
{ "error": "unauthorized", "message": "Invalid or missing API key." }
// 404 Resource not found
{ "error": "not_found", "message": "No invoice found with id '42-xyz' on your account." }
// 409 Wrong status for the operation
{ "error": "invalid_invoice_status", "message": "Cannot release an invoice with status 'pending'. Only 'paid' invoices can be released." }
// 409 Duplicate external reference
{ "error": "duplicate_reference", "message": "An invoice with reference 'order_789' already exists." }
// 409 Insufficient wallet balance
{ "error": "insufficient_funds", "message": "Wallet balance (200000 XAF) is less than invoice amount (850000 XAF).",
"current_balance": 200000, "invoice_amount": 850000 }
// 429 Rate limited
{ "error": "rate_limit_exceeded", "message": "Too many requests. Please slow down." }
// 500 Unexpected server error
{ "error": "server_error", "message": "An unexpected error occurred. Please try again." }
How the sandbox works
Everything you need to know before writing your first API call.
Completely isolated
Sandbox invoices, payments, and transactions are stored in a dedicated environment, fully separated from Fonlok's live platform. Nothing you do in the sandbox can affect real users or real money.
Deterministic test flows
Payments stay in a pending state until you explicitly confirm or fail them. This means you can test the happy path (successful payment), the failure path (declined), and edge cases — one at a time, as many times as you need.
Full API fidelity
The sandbox uses exactly the same request and response format as the live Fonlok API. When you are ready to go live, you only need to replace your sandbox key with a live key — your code stays the same.
Authenticated with scoped keys
Each sandbox key (sk_test_*) is tied to your Fonlok account and can be revoked at any time. The full key is shown once at creation — copy it immediately, as it cannot be retrieved afterwards.
Consistent error responses
Validation errors, not-found responses, and other error types use the same structure across all endpoints. Error handling you build during testing will work correctly when you go live.
Authentication
Every sandbox request must include your API key. You pass it as a Bearer token in the Authorization HTTP header on every request. This is a standard pattern used by most APIs — it tells the server which account the request belongs to, without embedding credentials in the URL.
HTTP header
Authorization: Bearer sk_test_a1b2c3d4...
Key format
sk_test_ + 32 hex chars (40 chars total)
Key security
Shown once at creation — cannot be retrieved afterwards
Key scope
Sandbox-only, tied to your account, independently revocable
Quick start
Run a complete payment lifecycle in under five minutes.
01
Create a sandbox key
Scroll down to the "API keys" section, sign in if prompted, and click "Generate key". Give it a descriptive label so you can identify it later.
02
Verify your key
Paste your key into the explorer below and send a GET /sandbox/ping request. If you receive back {"status": "ok"}, your key is working correctly and you are ready to continue.
03
Create a test invoice
Call POST /sandbox/invoices with a title, amount, and seller email. The response includes an invoice_id (it looks like inv_test_abc123) — copy it, you will need it in the next step.
04
Simulate a payment
Call POST /sandbox/payments/initiate, passing the invoice_id from step 3 and a Cameroonian mobile number (format: 237XXXXXXXXX). The response includes a payment reference — a unique ID for this payment attempt.
05
Confirm or fail the payment
Call POST /sandbox/payments/{reference}/confirm to simulate the customer approving the payment, or POST /sandbox/payments/{reference}/fail to simulate a rejection. The invoice status updates automatically to reflect the outcome.
API reference
Select an endpoint, fill in the parameters, and send a live request directly from this page.
Sandbox
Sandbox key
GET/sandbox/ping
Verifies that the sandbox is reachable and your API key is valid. Use this as a first step to confirm your integration is set up correctly.
curl -X GET \
"https://api.fonlok.com/sandbox/ping" \
-H "Authorization: Bearer sk_test_your_key_here"
Response
Click “Send request” to see the response here.
The example below shows a sample response.
Sample response
{
"object":"sandbox_status",
"status":"ok",
"environment":"sandbox",
"key_label":"Local development",
"message":"The Fonlok sandbox is live. No real transactions will be processed.",
"timestamp":"2026-07-01T12:00:00.000Z",
"_sandbox":true
}
API keys
Sandbox keys are prefixed with sk_test_ and only work in the sandbox environment. They cannot be used to access live payments, real payouts, or any user data on Fonlok's live platform.
Loading…
Live API
Live API keys
Live keys are prefixed with sk_live_ and process real payments. Submit an application below — your key is generated immediately but requires admin approval before it becomes active. You will be emailed once it is activated.
Loading...
Common questions
Can sandbox activity affect the live Fonlok platform?
No. The sandbox runs in a completely isolated environment. Anything you do during testing — including errors — has no effect on real Fonlok users, live transactions, or real money.
Can I use a sandbox key to make real payments?
No. Keys prefixed with sk_test_ are only accepted in the sandbox. They are automatically rejected by Fonlok's live platform. Processing real payments requires a separate live API key, available to approved integration partners.
Do sandbox transactions expire or get cleaned up?
Sandbox data is kept for as long as your key remains active. When you revoke a key, all test invoices and transactions associated with it are permanently removed.
Can I test webhooks in the sandbox?
Yes. Use the POST /sandbox/momo/webhook/simulate endpoint to fire a real HTTP POST to any callback URL you provide. This lets you verify that your server handles incoming Fonlok payment notifications correctly before going live.
Can I use the sandbox in my CI/CD pipeline?
Yes. Create a key labelled for your pipeline (e.g. "GitHub Actions") and store it as a secret in your CI environment. The sandbox is designed to handle automated test suites without issues.
How do I report a bug in the sandbox API?
Reach us at support@fonlok.com with the subject line "Sandbox API issue". Include the endpoint, your request body, and the response you received.
How do I get a live (production) API key?
Scroll up to the "Live API keys" section, sign in, and click "Apply for a live key". Fill in your company name, website URL, and a brief description of your use case. Your key is generated immediately but starts as pending — a Fonlok admin reviews all applications and you will be emailed once it is activated.
What happens when a buyer opens a dispute?
Call POST /v1/payments/dispute with the invoice_id, a reason (max 1,000 chars), and an optional context field (max 10,000 chars — paste conversation history or evidence here). Fonlok freezes the funds, creates a shared chat thread, emails both parties their role-scoped chat links, and returns those links in the API response. Fonlok's dispute lifecycle is binary: open → resolved. You receive a payment.disputed webhook immediately and a payment.dispute_resolved webhook (with a decision field of "seller" or "buyer") when the admin closes the case. The typical resolution SLA is 24–48 hours.
How do I verify that a webhook payload came from Fonlok?
Every delivery includes an X-Fonlok-Signature header containing an HMAC-SHA256 hex digest of the raw request body, signed with your webhook secret. Compute the same digest on your server and compare using a constant-time comparison function (e.g. crypto.timingSafeEqual in Node.js). Reject any request where the signatures do not match.
What is the platform fee and who pays it?
Fonlok deducts a 3% platform fee from the gross amount at the time of release. The seller receives the remainder directly to their Mobile Money number. There are no charges to create invoices or initiate payments — the fee is only deducted when funds are released.
Can a buyer pay from a wallet instead of MoMo?
Yes. If your platform pre-funds buyer wallets, use POST /v1/wallet/pay with the buyer's user_ref. Funds are debited atomically from the wallet and the invoice moves to 'paid' immediately — no MoMo prompt is needed.