Documentation
Newton Labs Pay is an M-Pesa payments gateway for Kenyan apps. Create payment links, trigger STK push prompts, settle funds straight to your till, and get a signed webhook for every result. This reference covers the REST API.
The API is organised around predictable, resource-oriented URLs and returns JSON for every response. All requests go to the base URL:
https://api.newtonlabs.ke/v1 If you've never touched the API, start in the dashboard — payment links work without writing a line of code. When you're ready to automate, the whole surface is three things: a key, REST endpoints, and signed webhooks.
Authentication
The API authenticates with a secret key. Send it as a bearer token in the Authorization header on every request:
Each account has two key pairs. They never mix — a request fails if the key doesn't match the mode you're calling.
| Key prefix | Mode | Behaviour |
|---|---|---|
nlp_test_ | Test | Simulates the full payment lifecycle. No real M-Pesa prompt, no money moves. Use it freely while building. |
nlp_live_ | Live | Triggers real STK push prompts and settles real funds. Available once your KYC is approved. |
Create an app and copy its key on the Apps page in the dashboard. Treat the secret like a password — keep it server-side, never ship it in client code or commit it to a repo. Requests over plain HTTP are rejected; the API is TLS only.
Create a payment link
A payment link is a hosted M-Pesa checkout page. Create one with a POST to /v1/links; share the returned url with your customer over WhatsApp, SMS, or your own UI.
POST/v1/links
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
amount | integer | Required | Amount to charge, in the smallest currency unit (whole shillings for KES). Must be at least 10. |
currency | string | Optional | Three-letter ISO code. Defaults to KES — the only currency supported today. |
reference | string | Required | Your own identifier for the order or invoice. Echoed back on the payment object and every webhook. |
webhook_url | string | Optional | HTTPS URL we POST the result to. Falls back to your account-level webhook endpoint if omitted. |
expiry | integer | Optional | Minutes until the link expires unpaid. Defaults to 1440 (24 hours). Min 5, max 10080. |
Request & response
curl https://api.newtonlabs.ke/v1/links \
-H "Authorization: Bearer nlp_live_8fK2x..." \
-H "Content-Type: application/json" \
-d '{
"amount": 2500,
"currency": "KES",
"reference": "order_4821",
"webhook_url": "https://you.app/paid"
}'
# 201 Created
{
"id": "lnk_9X7K2",
"url": "https://pay.newtonlabs.ke/9X7K2",
"status": "pending",
"amount": 2500,
"currency": "KES",
"reference": "order_4821",
"receipt": null,
"created_at": "2026-05-20T09:14:02Z"
}
The payment object
Every link returns a payment object. The same shape is sent on the payload of each webhook, so you can store it once and update it as the status changes.
| Field | Type | Description |
|---|---|---|
id | string | Unique link identifier, prefixed lnk_. Use it to look the link up later. |
url | string | The hosted M-Pesa checkout page. This is what you share with the customer. |
status | string | One of pending, paid, failed, or expired. |
amount | integer | Amount charged, in whole shillings. |
reference | string | The identifier you supplied when creating the link. |
receipt | string | The M-Pesa confirmation code (e.g. SGH7X2K9Q1). null until the link is paid. |
created_at | string | ISO 8601 timestamp (UTC) of when the link was created. |
Webhooks
Payments are asynchronous — the customer approves the STK prompt on their phone, which can take seconds or minutes. Rather than poll, give us a webhook_url and we'll POST a JSON event the moment something changes.
Event types
| Event | Fires when |
|---|---|
link.paid | The customer completed payment. Funds are settling to your till. |
link.failed | The STK push was declined, timed out, or the customer had insufficient funds. |
link.expired | The link reached its expiry window without being paid. |
refund.completed | A refund you issued has landed back with the customer. |
Verifying the signature
Every webhook carries an X-Newton-Signature header — an HMAC-SHA256 of the raw request body, keyed with your webhook secret. Always verify it before trusting an event, and compare against the raw body, not a re-serialised object.
import express from "express";
import crypto from "node:crypto";
const SECRET = process.env.NEWTON_WEBHOOK_SECRET;
app.post("/paid", express.raw({ type: "*/*" }), (req, res) => {
const signature = req.headers["x-newton-signature"];
const expected = "sha256=" + crypto
.createHmac("sha256", SECRET)
.update(req.body)
.digest("hex");
if (signature !== expected) {
return res.status(401).end();
}
const event = JSON.parse(req.body);
if (event.type === "link.paid") {
markOrderPaid(event.data.reference);
}
// Reply 2xx within 10s, or we retry.
res.json({ received: true });
});
Respond with any 2xx status within 10 seconds. If we don't get one, we retry with exponential backoff for up to 24 hours.
Going live
Build and test against nlp_test_ keys for as long as you like — test mode runs the exact same API and webhook flow, it just never touches real money. Switching to live is a key swap, nothing more.
- Integrate and test end-to-end with your
nlp_test_key — create a link, pay it in the test checkout, confirm your webhook handler runs. - Complete merchant onboarding and KYC so your account can be approved for live payments.
- Once approved, create an app on the dashboard's Apps page and copy its
app_key +sk_secret. - Swap the key in your server environment and point
webhook_urlat your production endpoint. - Run one small real transaction to confirm settlement reaches your till, then you're live.
Errors
The API uses conventional HTTP status codes. A 2xx means success, a 4xx means a problem with the request (and the JSON body explains what), and a 5xx means something failed on our side.
| Code | Meaning |
|---|---|
400 Bad Request | The request was malformed — invalid JSON or a missing required field. |
401 Unauthorized | The API key is missing, invalid, or revoked. |
404 Not Found | The requested resource — usually a link ID — doesn't exist. |
422 Unprocessable | The request was well-formed but a value was rejected, e.g. an amount below the minimum. |
429 Too Many Requests | You've exceeded the rate limit. Back off and retry after the window resets. |
500 Server Error | Something broke on our end. Safe to retry; if it persists, check status.newtonlabs.ke. |
Stuck on something this page doesn't cover? Email hello@newtonlabs.ke — a real engineer answers.