TeronaPay

Quickstart

Run your first TeronaPay payment in about five minutes. By the end of this page you will:

  1. Have a TeronaPay merchant and a sandbox API key.
  2. Send a real M-Pesa STK Push to a sandbox phone.
  3. Receive a signed webhook confirming the payment succeeded.

1. Create a merchant

Sign up at /signup. The signup flow:

  • Creates a merchant record.
  • Creates a user record (your email + bcrypt-hashed password).
  • Issues your first API key (label dashboard).

The plaintext secret is shown once at signup and on each login. Copy it to your environment now:

export NOWPESA_KEY_ID=np_xxxxxxxxxxxxxxxxxxxxxxxx
export NOWPESA_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

2. Take a payment

Send an M-Pesa STK Push to a sandbox-registered phone number (+254708374149 works in Daraja sandbox):

curl https://api.sandbox.teronapay.com/v1/payments \
  -u "$NOWPESA_KEY_ID:$NOWPESA_SECRET" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-0001" \
  -d '{
    "reference": "order-0001",
    "amount": 100.00,
    "currency": "KES",
    "channel": "mpesa_stk_push",
    "payer_phone": "+254708374149"
  }'

The response comes back immediately:

{
  "id": "8d6ff4fa-da3d-4006-8997-d2780d0e22fe",
  "reference": "order-0001",
  "amount": 100.00,
  "currency": "KES",
  "channel": "mpesa_stk_push",
  "status": "processing",
  "created_at": "2026-05-19T10:23:11.512Z"
}

processing means we've initiated the STK Push and we're waiting on the customer to enter their PIN. The final state (succeeded or failed) arrives via webhook a few seconds later — or up to 60 seconds if the customer dismisses the prompt.

3. Receive the webhook

Register an endpoint that TeronaPay will POST signed events to:

curl https://api.sandbox.teronapay.com/v1/webhooks \
  -u "$NOWPESA_KEY_ID:$NOWPESA_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-domain.com/webhooks/nowpesa",
    "event_types": ["*"]
  }'

The response includes the signing secret — store it. You'll need it to verify incoming requests:

{
  "id": "66d5050b-1cd5-4953-b93b-f4ae9fee3fd9",
  "url": "https://your-domain.com/webhooks/nowpesa",
  "event_types": ["*"],
  "signing_secret": "whsec_dff0fa03f70e1e7746a32f791a31057103e71181239c74777e78095df92f7f5a"
}

We do a one-time verification POST to your URL before activating delivery — your endpoint must respond 2xx to that probe.

4. Verify the signature

Every delivery includes the X-Nowpesa-Signature header with shape t=<unix>,v1=<hex_hmac>. Verify like this (Node.js):

import { createHmac, timingSafeEqual } from "node:crypto";
 
function verify(rawBody: string, header: string, secret: string): boolean {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const t = parts.t;
  const sig = parts.v1;
  if (!t || !sig) return false;
  const expected = createHmac("sha256", secret)
    .update(`${t}.${rawBody}`)
    .digest("hex");
  return timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}

If verify() returns false, reject the request with 401. Otherwise process the event and respond 2xx.

5. Watch it work

In your dashboard:

  • /payments shows the order with status flipping from processingsucceeded.
  • /webhooks shows the delivery log + signed request body.
  • /statement shows your running balance (now +KES 1.00).

That's the whole loop. From here:

Alternative to endpoint registration: if you don't want a persistent endpoint, pass callback_url directly on POST /v1/payments. Events for that payment go only to the specified URL, signed with your callback signing secret.

Quickstart · TeronaPay docs