TenkiPay
Developer Documentation

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/json

Signing

HMAC-SHA256 v2

Quickstart

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. 1. Create an app

    Open the Business Portal, then Developer Center, and create one app per product or deployment.

  2. 2. Issue a test key

    Choose only the scopes your backend requires and store the secret in a secret manager.

  3. 3. Sign a request

    Sign the exact method, path, timestamp, and raw JSON body with HMAC SHA-256.

  4. 4. Simulate payment

    Open the returned checkout URL and simulate success or failure without moving funds.

Sandbox first — no real money
  1. 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:read and checkout.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 place sk_test_... in browser JavaScript, a mobile app, logs, source control, or support messages.
  2. 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. 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), and X-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 a GET request with no body, the final canonical-string component is empty.

  4. 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-Key for 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 returns 409 Conflict — stop and investigate, do not auto-generate another key. Keep keys 8–191 characters using letters, digits, ._:-.
  5. 5

    Redirect the customer and verify via webhook or signed GET

    Send the customer's browser to checkout_url on me.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 reports ACSC with matching reference, amount, currency, and environment.

Going live

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.

Read the full E-commerce integration guide

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.

ServiceWhat it enablesIntegration surfaceAccess
TenkiPay CheckoutHosted web and QR wallet checkout.Merchant API v1Sandbox and approved live keys
CollectionsInvoices, public payment links, and reconciliation.Merchant API v1Sandbox and approved live keys
TenkiPassTicket, voucher, and entitlement verification.Merchant API v1Live approval for verification and redemption
TenkiOpsCatalog, stock, sales, fulfilment, hotel, and checkout workflows.Merchant API v1Sandbox and approved live keys
TenkiMarkitPublic storefronts, listings, carts, orders, and receipts.Business Portal and public marketplaceBuilt-in commerce channel
School FeesTuition, boarding or hostel, and other school-defined charges with student references and receipts.Business and customer channelsRole-based access
PayTag, Pay By ID, and QRWallet discovery and reference-based payment.Customer and business channelsRole-based access; use Checkout for external acceptance
GovPay, WanGov, and EduPayApplication lookup and assisted public-service payment.Customer and agent channelsApproved role and provider configuration
Remittance and agent servicesDomestic remittance, cash-in, cash-out, and assisted payments.Approved Agent API and portalOperational and compliance approval required
Business finance workflowsBulk payment, recurring payment, forms, requests, and approvals.Business PortalBuilt-in business services
Integration rule: if a path is not present in the OpenAPI document, do not call it with a merchant key. Use the named TenkiPay channel or request a supported partner integration through Developer Center.

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.

EnvironmentPublic keySecret keyMoney movementAccess
Sandboxpk_test_...sk_test_...Never moves real fundsAvailable to an active developer app
Livepk_live_...sk_live_...Moves real fundsBusiness and app approval required
Secret handling: the secret key is shown once when issued. Store it only in a server-side secret manager. Never put it in JavaScript delivered to a browser, a Flutter application, an Android APK, an iOS binary, source control, logs, analytics, or support messages.

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.

Browse code samples
Express.js
Go Fiber
Laravel
ASP.NET Core
FastAPI
Jakarta Servlet
Next.js simulator

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.

HeaderPurpose
X-TenkiPay-KeyPublic app credential.
X-TenkiPay-TimestampISO-8601 request time used for replay protection.
X-TenkiPay-SignatureLowercase hexadecimal HMAC-SHA256 signature.
Idempotency-KeyRequired 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.

Common signature failure: signing pretty-printed JSON and sending compact JSON, changing field order after signing, signing only /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.

RequestTenkiPay behavior
New key and valid bodyExecutes once and stores the response.
Same key and byte-identical bodyReturns the stored result without executing the operation again.
Same key and different body409 Conflict. Stop and investigate; do not automatically generate another key.
Same request still processing409 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 live

Create 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/sessions

Create 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"
  }
}

Recommended integration

E-commerce checkout integration

Add one payment button to your store. The browser sends only your local order ID to your backend; your backend loads the trusted total, creates a signed TenkiPay session, and redirects the customer to hosted checkout.

StepRuns inRequired action
1. StartStorefrontPOST only the local order_id to your own backend with CSRF protection.
2. CreateMerchant backendReload the order, calculate the total, sign the exact JSON body, and create one checkout with a persisted idempotency key.
3. PayTenkiPayRedirect to checkout_url. TenkiPay handles wallet login or QR approval.
4. NotifyWebhook endpointVerify the raw-body HMAC and timestamp, deduplicate the delivery, then enqueue processing.
5. VerifyMerchant workerRetrieve the session with a signed GET and match ACSC, reference, amount, currency, and environment.
6. FulfilMerchant databaseMark the order paid exactly once, retain TenkiPay references, then release goods or service.

Storefront button

Use your framework's CSRF field. Do not submit an amount.

<form method="POST" action="/payments/tenkipay">
  <input type="hidden" name="_csrf" value="{{ csrfToken }}">
  <input type="hidden" name="order_id" value="ORDER-1001">
  <button type="submit">Pay with TenkiPay</button>
</form>

Merchant route

Authentication and CSRF middleware run before this handler.

app.post('/payments/tenkipay', requireUser, verifyCsrf, async (req, res) => {
  const order = await orders.findPendingForCustomer(
    req.body.order_id,
    req.user.id
  );
  if (!order) return res.sendStatus(404);

  // Return a canonical decimal string such as "249.50", not a JS float.
  order.expectedAmount = await orders.recalculateTotal(order.id);

  const session = await createSignedTenkiPayCheckout(order);
  await orders.attachCheckout(order.id, {
    sessionId: session.session_id,
    idempotencyKey: `checkout:${order.id}:v1`
  });

  return res.redirect(303, session.checkout_url);
});
Copy the complete Node.js signing helper
import crypto from 'node:crypto';

const TENKIPAY_ORIGIN = process.env.TENKIPAY_BASE_URL || 'https://me.tenkipay.com';

async function signedTenkiPayRequest(method, path, payload, idempotencyKey) {
  const body = payload === null ? '' : JSON.stringify(payload);
  const timestamp = new Date().toISOString();
  const canonical = [timestamp, method.toUpperCase(), path, body].join('\n');
  const signature = crypto.createHmac('sha256', process.env.TENKIPAY_SECRET_KEY)
    .update(canonical, 'utf8')
    .digest('hex');

  const response = await fetch(`${TENKIPAY_ORIGIN}${path}`, {
    method,
    headers: {
      Accept: 'application/json',
      'Content-Type': 'application/json',
      'X-TenkiPay-Key': process.env.TENKIPAY_PUBLIC_KEY,
      'X-TenkiPay-Timestamp': timestamp,
      'X-TenkiPay-Signature': signature,
      ...(idempotencyKey ? { 'Idempotency-Key': idempotencyKey } : {})
    },
    ...(body ? { body } : {}),
    signal: AbortSignal.timeout(15000)
  });

  const result = await response.json();
  if (!response.ok) throw new Error(`TenkiPay ${response.status}: ${result.message}`);
  return result.data;
}

function createSignedTenkiPayCheckout(order) {
  return signedTenkiPayRequest(
    'POST',
    '/api/v1/merchant/checkout/sessions',
    {
      merchant_reference: order.reference,
      amount: order.expectedAmount,
      currency: order.currency,
      description: `Order ${order.reference}`,
      success_url: `${process.env.STORE_URL}/orders/${order.id}/payment-return`,
      cancel_url: `${process.env.STORE_URL}/orders/${order.id}`
    },
    `checkout:${order.id}:v1`
  );
}

function retrieveTenkiPayCheckout(sessionId) {
  const safeSessionId = encodeURIComponent(sessionId);
  return signedTenkiPayRequest(
    'GET',
    `/api/v1/merchant/checkout/sessions/${safeSessionId}`,
    null
  );
}

Verify before fulfilment

After a verified webhook, call the signed GET endpoint. Reject any mismatch in status, reference, amount, currency, or environment. Update the order with an atomic "paid only if unpaid" operation so webhook retries cannot fulfil twice.

Return URL behavior

Show a processing page and ask your backend for the order status. A redirect or browser message improves the customer experience but is never payment evidence.

Security boundary: TenkiPay does not accept an unsigned browser payment form. Never place sk_test_... or sk_live_... in HTML, JavaScript, a mobile app, or a public repository. Recalculate the order total from your server-side product, tax, discount, and delivery records.

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.

StatusTerminalMeaningMerchant action
AUTH_PENDINGNoWaiting for customer authorization.Do not fulfil. Continue waiting or retrieve later.
ACSCYesPayment completed and settled in the TenkiPay ledger.Verify amount and reference, then fulfil once.
RJCTYesAuthorization or simulated payment was rejected.Keep the order unpaid and offer a new checkout.
FAILEDYesPayment processing failed.Keep the order unpaid and investigate before retrying.
EXPIREDYesThe checkout expiry time passed.Create a new session with a new idempotency key.
CANCELLEDYesThe customer cancelled the hosted checkout.Keep the order unpaid.
Fulfilment rule: a success redirect is never sufficient. Fulfil only after a verified webhook or signed server-side retrieval reports ACSC and the amount, currency, environment, and merchant reference match your order.

Invoices and collection requests

Sandbox and live

Issue 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/invoices

Scope: 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 channel

TenkiMarkit 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 productRecommended pathPayment integration
Sell directly on TenkiPayManage the store, categories, listings, stock, and orders in TenkiMarkit.Built in
Existing website or mobile appKeep your own catalog and order system, then create a TenkiPay Checkout session from your backend.checkout.sessions:write
Existing ERP or operations systemUse 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 required

Integrate 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/verify

Scope: entitlements:read. Inspect validity without consuming the entitlement.

POST /api/v1/merchant/entitlements/redeem

Scope: entitlements:write. Consume it once with an idempotency key.

{ "token": "TKPASS-..." }
Two-step gate flow: call verify while preparing to admit or serve the holder, display the item and validity to the operator, then call redeem only at the point of delivery. A valid verification does not reserve the entitlement. Handle a later redemption conflict as “already used” and do not admit twice.

TenkiOps business operations

Sandbox and live

Connect 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.

ResourceEndpointsScope
Workspaces/operations/workspacesoperations:read
operations:write
Catalog and stock/operations/workspaces/{workspace_id}/catalog
/catalog/{item_id}/stock-adjustments
operations:read
operations:write
Sales and collection/operations/workspaces/{workspace_id}/sales
/operations/sales/{sale_id}/checkout
operations:read
operations:write
Restaurant and delivery fulfilment/operations/sales/{sale_id}/workflow/advanceoperations:write
Hotel rooms and stays/operations/workspaces/{workspace_id}/hotel/rooms
/operations/workspaces/{workspace_id}/hotel/stays
operations:read
operations: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.

HeaderValue
X-TenkiPay-DeliveryUnique delivery identifier. Persist it as your deduplication key.
X-TenkiPay-EventEvent name, such as checkout.payment_completed.
X-TenkiPay-TimestampUnix timestamp in seconds used in the signature.
X-TenkiPay-Signaturev1= 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);
  }
);
Delivery safety: verify before parsing or acting, reject timestamps older than five minutes, deduplicate by 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.
Retries: a timeout, network error, or non-2xx response is retried after approximately 1 minute, 5 minutes, 15 minutes, and 1 hour. After five unsuccessful attempts the delivery is marked dead and requires operator review or replay from Developer Center.

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."]
  }
}
HTTPMeaningRetry?
400Missing or invalid idempotency/header input.Fix the request first.
401Missing, stale, or invalid signature/key.Regenerate timestamp and signature; do not loop.
403Scope, app, environment, or business access denied.Correct access configuration.
404Resource is absent or outside the calling app/environment.Do not retry unchanged.
409Idempotency conflict, operation in progress, or business-state conflict.Retry only an in-progress request with the same key/body.
422Validation or business rule failed.Correct the data or workflow state.
429Rate limit exceeded.Back off with jitter, then retry safely.
500 / 503Temporary 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.

Success test: create a test checkout, open it, choose simulated success, verify the signed webhook, retrieve ACSC, and confirm your order is fulfilled once.
Failure test: simulate failure, timeout your webhook endpoint, replay the same idempotent request, alter its body to produce a conflict, and confirm no order is fulfilled.

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.

ScopePermits
checkout.sessions:read / :writeRetrieve or create hosted checkout sessions.
invoices:read / :writeRetrieve or create invoices and collection requests.
entitlements:read / :writeVerify or redeem TenkiPass entitlements. Live key required.
operations:read / :writeRead or modify workspaces, catalog, stock, sales, rooms, and stays.
Production checklist: use least-privilege live scopes; keep secrets in a managed vault; run HMAC v2; use HTTPS redirects and webhooks; validate redirect origins; verify raw-body webhook signatures; deduplicate deliveries; make every write idempotent; reconcile ACSC payments; monitor 401, 409, 429, 5xx, and dead webhooks; test key and webhook-secret rotation; and maintain an incident contact.
Versioning: the URL major version is /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.