Quickstart
From a new account to an order that marks itself paid. Most merchants finish this in an afternoon.
- Create your account and get approved
Sign up at app.skanpay.website and upload your business documents under Verification. SkanPay reviews them and switches on live payments for your account. Until that happens, API keys cannot be created and any key returns
live_mode_not_enabled. - Connect your app and generate keys
In the dashboard open Connected apps, add your website or app (a name and its domain), then choose Generate keys. You get a publishable key (
pk_live_…) and a secret key (sk_live_…). The secret key is shown once; SkanPay stores only a hash of it. Put it in your server's environment:SKANPAY_SECRET_KEY=sk_live_... - Set your webhook URL
On the same app, enter the
https://URL on your server that should receive payment outcomes, for examplehttps://shop.example.com/webhooks/skanpay. Saving it shows the webhook signing secret (whsec_…). Store it next to your secret key:SKANPAY_WEBHOOK_SECRET=whsec_... - Charge a customer
When the customer checks out, call
POST /v1/chargesfrom your server. Use your order id as both thereferenceand theIdempotency-Key, so a retry can never charge twice.curl -X POST https://api.skanpay.website/v1/charges \ -H "Authorization: Bearer $SKANPAY_SECRET_KEY" \ -H "Idempotency-Key: order-9001" \ -H "Content-Type: application/json" \ -d '{ "amount": "50000", "currency": "UGX", "msisdn": "+256772123456", "reference": "order-9001", "description": "2 x T-shirt", "metadata": { "cart_id": "c_42" } }'The response comes back immediately with the prompt already on the customer's phone:
{ "id": "chg_01J9Z3K8Q4W6XK2M7N5P0R3T8V", "object": "charge", "status": "AWAITING_CUSTOMER", "amount": "50000", "currency": "UGX", "country": "UG", "network": "MTN", "msisdn_masked": "+2567****456", "reference": "order-9001", "fee": "0", "net": "50000", "failure_code": null, ... }Tell the customer to check their phone and enter their PIN. Don't mark the order paid yet, and don't mark it failed either:
AWAITING_CUSTOMERjust means “waiting”. - Receive the webhook and fulfil the order
When the customer approves, SkanPay POSTs a
charge.successfulevent to your webhook URL. Verify the signature over the raw request body, then mark the order paid, keyed ondata.object.reference.// server.mjs (npm install express) // Run: SKANPAY_WEBHOOK_SECRET=whsec_... node server.mjs import express from "express"; import { createHmac, timingSafeEqual } from "node:crypto"; const TOLERANCE_SECONDS = 300; function verifySkanPaySignature(rawBody, header, secret) { const parts = Object.fromEntries( String(header ?? "").split(",").map((pair) => pair.trim().split("=", 2)), ); const t = Number(parts.t); if (!Number.isInteger(t)) return false; if (Math.abs(Date.now() / 1000 - t) > TOLERANCE_SECONDS) return false; const expected = createHmac("sha256", secret) .update(`${t}.`) .update(rawBody) // the raw bytes, exactly as received .digest(); const presented = Buffer.from(parts.v1 ?? "", "hex"); return presented.length === expected.length && timingSafeEqual(presented, expected); } const app = express(); // express.raw, NOT express.json: the signature covers the raw bytes. app.post("/webhooks/skanpay", express.raw({ type: "application/json" }), async (req, res) => { const ok = verifySkanPaySignature( req.body, req.get("SkanPay-Signature"), process.env.SKANPAY_WEBHOOK_SECRET, ); if (!ok) return res.sendStatus(400); const event = JSON.parse(req.body.toString("utf8")); // At-least-once delivery: ignore an event id you have already handled. // (Use a unique column in your database; this is where you check it.) const object = event.data.object; switch (event.type) { case "charge.successful": console.log(`mark order ${object.reference} paid (${object.amount} ${object.currency})`); break; case "charge.failed": console.log(`order ${object.reference} not paid: ${object.failure_code}`); break; } res.sendStatus(200); // acknowledge quickly; do slow work after responding }); app.listen(8080, () => console.log("listening on :8080")); - Try it with a real, small amount
There is no sandbox: charge your own phone for the minimum amount (USh 500 in Uganda), approve it, and watch the webhook arrive. The transaction, its full status trail and the webhook delivery all show in the dashboard, and you can replay the delivery from there while you debug your handler.
Next
- Cash-in guide: statuses, failures, timeouts and the customer experience.
- Go-live checklist: what to check before real customers pay.