TenkiPay Developer Platform
Build payments, commerce, and operations with TenkiPay
Integrate secure hosted checkout, payable collections, TenkiPass verification, and TenkiOps business workflows through one signed API platform. TenkiMarkit and TenkiPay's customer and agent channels extend the same infrastructure into built-in commerce and financial services.
TenkiPay Checkout
Accept wallet payments without handling customer credentials.
Collections
Create invoices, payment links, and stable reconciliation references.
TenkiMarkit
Operate a built-in storefront, product catalog, cart, and order channel.
TenkiPass
Verify and redeem tickets, vouchers, and service entitlements.
TenkiOps
Connect catalog, stock, sales, fulfilment, and hotel operations.
Events
Receive signed notifications with retries and delivery tracking.
Merchant API base URL
https://me.tenkipay.com/api/v1/merchant
Content type
application/jsonSigning
HMAC-SHA256 v2Quickstart
Integrate hosted checkout into your web application in four high-level steps, then follow the end-to-end guide below to go from zero to a verified payment in the sandbox and then live.
- 1. Create an app
Open the Business Portal, then Developer Center, and create one app per product or deployment.
- 2. Issue a test key
Choose only the scopes your backend requires and store the secret in a secret manager.
- 3. Sign a request
Sign the exact method, path, timestamp, and raw JSON body with HMAC SHA-256.
- 4. Simulate payment
Open the returned checkout URL and simulate success or failure without moving funds.
-
1
Create an app and issue test keys
In the Business Portal → Developer Center, create one app per product or deployment. Issue a sandbox key pair with only the scopes your backend needs —
checkout.sessions:readandcheckout.sessions:write. Copy the public key (pk_test_...), the secret key (sk_test_...), and the webhook signing secret (whsec_test_...).Security: the secret key is shown once when issued. Store it in a server-side secret manager. Never placesk_test_...in browser JavaScript, a mobile app, logs, source control, or support messages. -
2
Configure environment variables
Both sandbox and live use the same API host and base path. The key prefix selects the environment. Keep the public key safe to expose, but keep both secrets server-only.
# Sandbox — these keys move NO real money TENKIPAY_PUBLIC_KEY=pk_test_... # public credential, safe in client code TENKIPAY_SECRET_KEY=sk_test_... # server-side only, never exposed TENKIPAY_WEBHOOK_SECRET=whsec_test_...# server-side only, for webhook verification TENKIPAY_BASE_URL=https://me.tenkipay.com STORE_URL=http://localhost:3000 # your app's public origin -
3
Sign requests with HMAC-SHA256 v2
Every merchant API request carries
X-TenkiPay-Key,X-TenkiPay-Timestamp(ISO-8601 UTC, within five minutes of TenkiPay time), andX-TenkiPay-Signature. Sign the exact JSON bytes you transmit — serialize once, sign it, send the same string.// Node 18+ (no extra dependencies for signing) import crypto from 'node:crypto'; const publicKey = process.env.TENKIPAY_PUBLIC_KEY; const secretKey = process.env.TENKIPAY_SECRET_KEY; const path = '/api/v1/merchant/checkout/sessions'; const payload = { merchant_reference: 'ORDER-1001', amount: '250.00', currency: 'SLE', description: 'Order 1001', success_url: 'https://shop.example.com/orders/1001/paid', cancel_url: 'https://shop.example.com/orders/1001' }; const body = JSON.stringify(payload); const timestamp = new Date().toISOString(); const canonical = [timestamp, 'POST', path, body].join('\n'); const signature = crypto.createHmac('sha256', secretKey) .update(canonical, 'utf8') .digest('hex'); // lowercase hex, no "v2=" prefix const response = await fetch(`https://me.tenkipay.com${path}`, { method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json', 'X-TenkiPay-Key': publicKey, 'X-TenkiPay-Timestamp': timestamp, 'X-TenkiPay-Signature': signature, 'Idempotency-Key': 'order-1001-checkout-v1' }, body }); const result = await response.json(); if (!response.ok) throw new Error(`${response.status}: ${result.message}`); // Redirect the CUSTOMER's browser, never your backend, to this URL. console.log(result.data.checkout_url);Signing failures often come from pretty-printing JSON after signing, reordering fields, signing only
/checkout/sessions, using local time without an offset, or reusing a stale timestamp. For aGETrequest with no body, the final canonical-string component is empty. -
4
Create a checkout session from your backend
Your storefront never calls TenkiPay directly. The browser posts a local order ID to your backend; your backend reloads the order, recalculates the amount from trusted records, and creates the session. Use a new
Idempotency-Keyfor each logical write and persist it with the order.Idempotency: a new key with a valid body executes once; the same key with a byte-identical body returns the stored result; the same key with a different body returns409 Conflict— stop and investigate, do not auto-generate another key. Keep keys 8–191 characters using letters, digits,._:-. -
5
Redirect the customer and verify via webhook or signed GET
Send the customer's browser to
checkout_urlonme.tenkipay.com. TenkiPay handles wallet sign-in and QR approval. A success redirect is never proof of payment — fulfil only after a verified webhook or a signed session retrieval reportsACSCwith matching reference, amount, currency, and environment.
Test the full flow in the sandbox first: simulate success and failure, verify the signed webhook, retrieve ACSC, and confirm your order is fulfilled exactly once. When ready, submit live access from Developer Center and issue a live key pair (pk_live_... / sk_live_...), which moves real money. Review the scopes, HTTPS redirect domains, webhook handling, and the full production checklist (secret vault, raw-body webhook verification, delivery deduplication, idempotent writes, reconciliation, monitoring, and key rotation) before deploying.
Choose the right TenkiPay surface
TenkiPay combines developer APIs with first-party customer, business, and agent channels. The HMAC merchant key grants access only to endpoints published in the OpenAPI contract; role-based app APIs require their own approved account and access token.
| Service | What it enables | Integration surface | Access |
|---|---|---|---|
| TenkiPay Checkout | Hosted web and QR wallet checkout. | Merchant API v1 | Sandbox and approved live keys |
| Collections | Invoices, public payment links, and reconciliation. | Merchant API v1 | Sandbox and approved live keys |
| TenkiPass | Ticket, voucher, and entitlement verification. | Merchant API v1 | Live approval for verification and redemption |
| TenkiOps | Catalog, stock, sales, fulfilment, hotel, and checkout workflows. | Merchant API v1 | Sandbox and approved live keys |
| TenkiMarkit | Public storefronts, listings, carts, orders, and receipts. | Business Portal and public marketplace | Built-in commerce channel |
| School Fees | Tuition, boarding or hostel, and other school-defined charges with student references and receipts. | Business and customer channels | Role-based access |
| PayTag, Pay By ID, and QR | Wallet discovery and reference-based payment. | Customer and business channels | Role-based access; use Checkout for external acceptance |
| GovPay, WanGov, and EduPay | Application lookup and assisted public-service payment. | Customer and agent channels | Approved role and provider configuration |
| Remittance and agent services | Domestic remittance, cash-in, cash-out, and assisted payments. | Approved Agent API and portal | Operational and compliance approval required |
| Business finance workflows | Bulk payment, recurring payment, forms, requests, and approvals. | Business Portal | Built-in business services |
Environments and credentials
The API hostname is the same in both environments. The credential prefix selects the environment and every resource is isolated by business, developer app, and environment.
| Environment | Public key | Secret key | Money movement | Access |
|---|---|---|---|---|
| Sandbox | pk_test_... | sk_test_... | Never moves real funds | Available to an active developer app |
| Live | pk_live_... | sk_live_... | Moves real funds | Business and app approval required |
Credentials expire after 180 days in sandbox and 365 days in live. An app may have at most 10 active keys per environment. Rotate by issuing a replacement, deploying it, confirming successful traffic, and then revoking the old key.
Reference integration kit
Use the same secure flow in your framework
Commented projects are available for Express.js, Go Fiber, Laravel, ASP.NET Core, FastAPI, Jakarta Servlet, and a runnable Next.js sandbox simulator. They are reference implementations rather than versioned SDK packages.
Authentication
Every merchant API request is authenticated with an HMAC signature. Send the three authentication headers on reads and writes; send Idempotency-Key on write endpoints that require it.
| Header | Purpose |
|---|---|
| X-TenkiPay-Key | Public app credential. |
| X-TenkiPay-Timestamp | ISO-8601 request time used for replay protection. |
| X-TenkiPay-Signature | Lowercase hexadecimal HMAC-SHA256 signature. |
| Idempotency-Key | Required where shown in the endpoint reference. It identifies one logical write operation. |
Canonical string
{timestamp}\n{UPPERCASE_METHOD}\n{/api/path}\n{exact_raw_body}
Timestamp: use an ISO-8601 UTC timestamp such as 2026-08-05T12:30:45.000Z. It must be within five minutes of TenkiPay server time.
Path: include the leading slash and complete API path, for example /api/v1/merchant/checkout/sessions. Do not include the scheme, hostname, fragment, or query string.
Body: sign the exact bytes you transmit. Serialize JSON once, keep that string, sign it, and send the same string. For a GET request with no body, the final canonical-string component is empty.
Signature: compute HMAC-SHA256 with the secret key and send the lowercase hexadecimal digest without a v2= prefix.
/checkout/sessions, using local time without an offset, or reusing a stale timestamp.Idempotency and safe retries
Idempotency prevents a timeout or network retry from creating duplicate payment resources. Generate the key in your backend and persist it with your local operation before calling TenkiPay.
| Request | TenkiPay behavior |
|---|---|
| New key and valid body | Executes once and stores the response. |
| Same key and byte-identical body | Returns the stored result without executing the operation again. |
| Same key and different body | 409 Conflict. Stop and investigate; do not automatically generate another key. |
| Same request still processing | 409 Conflict. Retry later with the same key and same body. |
Keys must be 8-191 characters and may contain letters, numbers, period, underscore, colon, and hyphen. A practical format is order-1001-checkout-v1. Use a new key only for a genuinely new business operation.
TenkiPay Checkout
Sandbox and liveCreate a server-side payment session and send the customer to the returned URL. The hosted page supports TenkiPay account sign-in and QR approval from an already signed-in TenkiPay app. Poll the merchant API or treat the signed webhook as the fulfilment trigger.
POST /api/v1/merchant/checkout/sessionsCreate a fixed-amount hosted checkout. Scope: checkout.sessions:write. Idempotency required.
GET /api/v1/merchant/checkout/sessions/{session_id}Retrieve authoritative status and amount. Scope: checkout.sessions:read.
Create request
{
"merchant_reference": "ORDER-1001",
"amount": "250.00",
"currency": "SLE",
"description": "Online store order 1001",
"success_url": "https://shop.example.com/orders/1001/paid",
"cancel_url": "https://shop.example.com/orders/1001",
"customer_name": "Optional customer name",
"customer_phone": "+23276000000"
}
201 response
{
"status": true,
"message": "Checkout session created.",
"data": {
"session_id": "CS-20260805-EXAMPLE",
"checkout_url": "https://me.tenkipay.com/checkout/CS-20260805-EXAMPLE",
"merchant_reference": "ORDER-1001",
"status": "AUTH_PENDING",
"environment": "test",
"approval_methods": ["TENKIPAY_APP_QR", "TENKIPAY_WEB_LOGIN"],
"amount": "250.00",
"currency": "SLE",
"expires_at": "2026-08-05T13:00:00Z"
}
}
Browser handoff
Redirect directly to checkout_url, or open it in a merchant-created popup. After app approval, TenkiPay sends a tenkipay.checkout.completed message to the verified success URL origin, closes the popup when the browser permits it, and otherwise redirects to success_url. When the customer cancels from a popup, TenkiPay sends a tenkipay.checkout.cancelled message to the verified cancel URL origin and closes the popup; outside a popup it goes back or redirects to cancel_url. Browser messages and redirects improve customer experience; they are not proof of payment.
window.addEventListener('message', async (event) => {
if (event.origin !== 'https://me.tenkipay.com') return;
switch (event.data?.type) {
case 'tenkipay.checkout.completed':
// Verify the session from your server before fulfilling the order.
await refreshOrderStatus(event.data.session_id);
break;
case 'tenkipay.checkout.cancelled':
// Customer cancelled. Keep the order unpaid and free any reserves.
await refreshOrderStatus(event.data.session_id);
break;
}
});
Checkout status model
Treat status values as case-sensitive stable codes. Only ACSC means the payment completed successfully.
| Status | Terminal | Meaning | Merchant action |
|---|---|---|---|
AUTH_PENDING | No | Waiting for customer authorization. | Do not fulfil. Continue waiting or retrieve later. |
ACSC | Yes | Payment completed and settled in the TenkiPay ledger. | Verify amount and reference, then fulfil once. |
RJCT | Yes | Authorization or simulated payment was rejected. | Keep the order unpaid and offer a new checkout. |
FAILED | Yes | Payment processing failed. | Keep the order unpaid and investigate before retrying. |
EXPIRED | Yes | The checkout expiry time passed. | Create a new session with a new idempotency key. |
CANCELLED | Yes | The customer cancelled the hosted checkout. | Keep the order unpaid. |
ACSC and the amount, currency, environment, and merchant reference match your order.Invoices and collection requests
Sandbox and liveIssue an itemized invoice with a stable external reference. TenkiPay returns a collection reference and payment URL that can be shared, embedded, or rendered as a QR code.
POST /api/v1/merchant/invoicesScope: invoices:write. Idempotency required. Include 1-50 line items and either customer email or phone.
GET /api/v1/merchant/invoices/{reference}Scope: invoices:read. The reference may be the invoice number, collection reference, or your external reference.
{
"external_reference": "INV-ERP-1042",
"customer_name": "Aminata Kamara",
"customer_email": "aminata@example.com",
"description": "August services",
"currency": "SLE",
"allow_partial_payments": false,
"issued_on": "2026-08-05",
"due_on": "2026-08-20",
"publish": true,
"items": [
{ "description": "Professional services", "quantity": 1, "unit_amount": "850.00" }
]
}
Reconcile using amount_due, amount_paid, and amount_outstanding. Test invoices and all resulting payment resources remain in the sandbox environment.
TenkiMarkit commerce
Built-in channelTenkiMarkit is TenkiPay's native commerce channel. A business publishes its storefront and listings, customers discover products through the public marketplace, and the platform carries the cart, order, payment, and receipt flow without a separate ecommerce integration.
| Your product | Recommended path | Payment integration |
|---|---|---|
| Sell directly on TenkiPay | Manage the store, categories, listings, stock, and orders in TenkiMarkit. | Built in |
| Existing website or mobile app | Keep your own catalog and order system, then create a TenkiPay Checkout session from your backend. | checkout.sessions:write |
| Existing ERP or operations system | Use TenkiOps for programmatic catalog, stock, sales, fulfilment, and collection workflows. | operations:read / operations:write |
TenkiMarkit catalog and order administration are not exposed as public Merchant API v1 endpoints. This prevents developers from depending on internal portal routes; integrations should use the documented Checkout or TenkiOps contracts.
TenkiPass verification and fulfilment
Live approval requiredIntegrate TenkiPass verification at a gate, service desk, vehicle, or partner application. Redemption is idempotent and should occur only when the service is actually delivered.
POST /api/v1/merchant/entitlements/verifyScope: entitlements:read. Inspect validity without consuming the entitlement.
POST /api/v1/merchant/entitlements/redeemScope: entitlements:write. Consume it once with an idempotency key.
{ "token": "TKPASS-..." }
TenkiOps business operations
Sandbox and liveConnect an existing hotel, retail, fuel, pharmacy, school, restaurant, or delivery system to TenkiOps. Workspaces, catalog, sales, fulfilment context, stock, rooms, stays, and checkout remain isolated by business and API environment.
| Resource | Endpoints | Scope |
|---|---|---|
| Workspaces | /operations/workspaces | operations:readoperations:write |
| Catalog and stock | /operations/workspaces/{workspace_id}/catalog /catalog/{item_id}/stock-adjustments | operations:readoperations:write |
| Sales and collection | /operations/workspaces/{workspace_id}/sales /operations/sales/{sale_id}/checkout | operations:readoperations:write |
| Restaurant and delivery fulfilment | /operations/sales/{sale_id}/workflow/advance | operations:write |
| Hotel rooms and stays | /operations/workspaces/{workspace_id}/hotel/rooms /operations/workspaces/{workspace_id}/hotel/stays | operations:readoperations:write |
School fees
Select the server-published charge. TenkiPay records fee_type, fee_name, student_id, and student_name; class, term, and academic year are included only when configured by the school.
Restaurant
Send order_mode and a table reference for dine-in orders. Advance RECEIVED, PREPARING, READY, and SERVED independently of payment.
Delivery service
Send recipient, phone, pickup, and drop-off details. Advance BOOKED, PICKED_UP, and DELIVERED independently of payment.
All paths in this table are relative to /api/v1/merchant. GET calls require operations:read; POST calls require operations:write and an idempotency key. A sale can have multiple checkout attempts. Stock is posted only after verified payment, and a hotel room remains occupied until the final checkout balance is settled.
Webhooks
Webhooks are the asynchronous source of payment notifications. Configure separate test and live HTTPS endpoints in Developer Center. The signing secret is shown once and is different from your API secret key.
| Header | Value |
|---|---|
X-TenkiPay-Delivery | Unique delivery identifier. Persist it as your deduplication key. |
X-TenkiPay-Event | Event name, such as checkout.payment_completed. |
X-TenkiPay-Timestamp | Unix timestamp in seconds used in the signature. |
X-TenkiPay-Signature | v1= followed by the lowercase hexadecimal HMAC-SHA256 digest. |
Webhook signing payload
{unix_timestamp}.{exact_raw_request_body}Payment event payload
{
"id": "evt_live_01J...",
"event": "checkout.payment_completed",
"livemode": true,
"api_version": "2026-08-05",
"created_at": "2026-08-05T12:35:18Z",
"data": {
"object": {
"session_id": "CS-20260805-EXAMPLE",
"merchant_reference": "ORDER-1001",
"status": "ACSC",
"environment": "live",
"amount": "250.00",
"currency": "SLE",
"transaction_id": "TXN-...",
"message_id": "MSG-...",
"end_to_end_id": "E2E-...",
"uetr": "...",
"merchant": { "merchant_code": "MCH-...", "business_name": "Example Ltd", "display_name": "Example" },
"debtor": { "name": "Customer", "account": "..." },
"creditor": { "name": "Example Ltd", "account": "..." },
"authorized_at": "2026-08-05T12:35:17Z",
"completed_at": "2026-08-05T12:35:18Z"
}
}
}
Node.js signature verification
Preserve the raw request body before JSON parsing.
import crypto from 'node:crypto';
import express from 'express';
const app = express();
app.post('/webhooks/tenkipay',
express.raw({ type: 'application/json', limit: '256kb' }),
async (req, res) => {
const deliveryId = req.get('X-TenkiPay-Delivery');
const timestamp = req.get('X-TenkiPay-Timestamp');
const received = req.get('X-TenkiPay-Signature') || '';
const rawBody = req.body.toString('utf8');
const timestampNumber = Number(timestamp);
if (!deliveryId || !timestamp || !/^v1=[a-f0-9]{64}$/.test(received)) {
return res.sendStatus(401);
}
if (!Number.isInteger(timestampNumber) ||
Math.abs(Date.now() / 1000 - timestampNumber) > 300) {
return res.sendStatus(401);
}
const signedPayload = Buffer.concat([
Buffer.from(`${timestamp}.`, 'utf8'),
req.body
]);
const digest = crypto
.createHmac('sha256', process.env.TENKIPAY_WEBHOOK_SECRET)
.update(signedPayload)
.digest('hex');
const expected = `v1=${digest}`;
const valid = received.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
if (!valid) return res.sendStatus(401);
const event = JSON.parse(rawBody);
await enqueueOnce(deliveryId, event);
return res.sendStatus(204);
}
);
X-TenkiPay-Delivery, enqueue work, and return a 2xx response quickly. Never fulfil from a browser redirect. Keep test and live endpoints and secrets separate.Errors, rate limits, and retry decisions
All API errors use a predictable JSON envelope. Validation errors include field-level details; internal exceptions are logged by TenkiPay and returned as sanitized messages.
{
"status": false,
"message": "Validation failed.",
"errors": {
"amount": ["The amount field must have 0-2 decimal places."]
}
}
| HTTP | Meaning | Retry? |
|---|---|---|
400 | Missing or invalid idempotency/header input. | Fix the request first. |
401 | Missing, stale, or invalid signature/key. | Regenerate timestamp and signature; do not loop. |
403 | Scope, app, environment, or business access denied. | Correct access configuration. |
404 | Resource is absent or outside the calling app/environment. | Do not retry unchanged. |
409 | Idempotency conflict, operation in progress, or business-state conflict. | Retry only an in-progress request with the same key/body. |
422 | Validation or business rule failed. | Correct the data or workflow state. |
429 | Rate limit exceeded. | Back off with jitter, then retry safely. |
500 / 503 | Temporary TenkiPay failure. | Retry idempotent reads, or writes with the original key and identical body. |
Use exponential backoff with jitter and a finite retry budget. A reasonable starting policy is 1, 2, 5, 10, and 30 seconds. When the final write response is uncertain, retrieve the resource by its TenkiPay or merchant reference before creating anything new.
Sandbox
Keys beginning with pk_test_ create test resources. The hosted simulator can complete or fail checkouts and emit test webhooks, but the wallet execution service rejects every sandbox session before authentication, debit, credit, ledger, or settlement logic.
ACSC, and confirm your order is fulfilled once.Go live
Submit live access from the Developer Center. TenkiPay reviews the business approval, integration ownership, redirect domains, webhook handling, operational contacts, and intended scopes. Approval belongs to one app; it does not automatically authorize another app or grant wallet transfer authority.
| Scope | Permits |
|---|---|
checkout.sessions:read / :write | Retrieve or create hosted checkout sessions. |
invoices:read / :write | Retrieve or create invoices and collection requests. |
entitlements:read / :write | Verify or redeem TenkiPass entitlements. Live key required. |
operations:read / :write | Read or modify workspaces, catalog, stock, sales, rooms, and stays. |
ACSC payments; monitor 401, 409, 429, 5xx, and dead webhooks; test key and webhook-secret rotation; and maintain an incident contact./v1. Additive fields may be introduced without changing the major version, so clients must ignore unknown JSON fields. TenkiPay will not silently rename or remove documented fields inside v1; breaking changes require a new major API version or a dated webhook api_version.