ConfirmApp Integration

POS and ERP API Documentation

Connect any POS, ERP, or order engine to read live payments and clear orders in seconds.

Building a school finance integration? Read the handoff checklist for school developers so ledger and receipt work stays on your side with clear expectations.

Architecture (upstream capture)

ConfirmApp is the upstream capture and sync layer. The Android app reads real M-PESA confirmation SMS on the registered handset, applies provider rules you configure in Admin, and syncs structured payment rows into the client-scoped inbox in the cloud.

Your product (POS, ERP, school finance portal, billing system, etc.) is the downstream system of record. It owns matching payments to orders or students, balances, reporting, and customer-facing artefacts such as PDF receipts, statements, or parent notifications. ConfirmApp does not replace those features. It feeds confirmed payment data quickly so downstream automation can run.

Typical flow: handset captures SMS → cloud inbox updates → your backend polls GET /api/integration/live or receives payment.claimed webhooks after POST /api/integration/claim → your system posts receipts, updates ledgers, and notifies users from your own stack.

Schools example: a school finance portal generates fee receipts and manages student balances. ConfirmApp on the bursar or office phone stays the capture path for paybill or till SMS. Integrate the portal with the same API and webhook patterns documented below so payment rows appear in your collection pipeline without manual retyping.

Delju POS M-Pesa pool (optional relay)

If your retail stack runs Delju and exposes POST /api/mpesa-pool/ingest, enable Relay captured payments to Delju M-Pesa pool per client in Admin. A scheduled job forwards new inbox rows so Delju POS polling and STK helpers can match the same M-Pesa transaction ids as ConfirmApp capture.

Server env: DELJU_MPESA_POOL_INGEST_URL, DELJU_MPESA_INGEST_SECRET(must match Delju's ingest bearer secret), and CRON_SECRET. Cron calls GET /api/cron/delju-mpesa-relay with Authorization: Bearer …. Relayed documents get deljuPoolRelayedAt in Firestore so each payment is only sent once. See the Admin portal README for the full checklist.

LAN and offline-first POS

Some branches run POS on a local network with a branch server or till PC (for example lodge or retail sites like Brooks) and keep sales, stock, and pending M-Pesa matches in a local database (SQLite, IndexedDB, Room, or similar). ConfirmApp stays the cloud capture layer; your POS still uses the same Integration API when online.

Typical split: ConfirmApp handset captures paybill/till SMS → cloud inbox updates → branch POS polls GET /api/integration/live when internet is available → cashier matches payment to a local order → POS calls POST /api/integration/claim → your local DB marks the order paid. Catalog, receipts, and stock remain on your branch stack.

Recommended API routing

  • Primary (cloud): https://confirmapp.pewang.company — live inbox and claims when WAN is up.
  • Optional local fallback: http://<branch-local-ip>:<port> — only if you run a branch proxy that mirrors the same Integration API contract on LAN. Do not expose this service to the public internet.
  • Timeout before fallback: 2–3 seconds on live/claim calls.
  • Retry: exponential backoff with jitter; reuse the same idempotency key on retries.

Local database and offline queue

When WAN is down, the till should still complete cash sales locally. For M-Pesa, cache the last successful GET /api/integration/live response in local storage and let the cashier pick a payment that was captured before the outage. Queue claim requests locally until connectivity returns.

// Local claim queue row (SQLite / IndexedDB / Room)
{
  "id": "claim_20260616_001",
  "mpesaCode": "UE93P3YJZA",
  "orderNumber": "POS-ORDER-94822",
  "reference": "POS-REF-94822",
  "claimAmountKes": 1200,
  "idempotencyKey": "brooks-till-2-20260616-001",
  "status": "pending",          // pending | sent | conflict_claimed_remote | failed
  "attempts": 0,
  "lastError": null,
  "createdAt": "2026-06-16T10:15:00.000Z"
}
  • Persist pending claims in local DB; flush in FIFO order when online.
  • Use a stable orderNumber / reference per checkout (same value on every retry).
  • Generate one idempotencyKey per claim attempt and send it in your own logs; ConfirmApp dedupes by mpesaCode + claim state, not by your key.
  • Poll live inbox every 2–5 seconds while checkout is open; back off to 10–30 seconds on the sales floor when idle.

Conflict handling

  • Cloud returns payment already fully claimed → mark local queue row conflict_claimed_remote and surface for supervisor review.
  • Cloud returns payment not found → retry (payment may still be syncing from the handset), then manual review.
  • Claim succeeded locally but sale save failed → do not release the M-Pesa row; ConfirmApp has no unclaim endpoint. Fix stock or sale locally, then complete the order.

Suggested POS settings fields

  • primaryApiUrl, fallbackApiUrl, apiTimeoutMs
  • offlineQueueEnabled, maxRetryAttempts, retryBaseMs
  • idempotencyPrefix (branch or till id), syncIntervalSeconds
  • manualConflictReviewEnabled

Security

  • Always send x-client-code and x-integration-key on cloud calls (store the integration key server-side on branch proxies, not in browser localStorage).
  • Restrict LAN fallback to the branch subnet; firewall the proxy from the public internet.
  • Bank-forwarded SMS (KCB, Coop Bank, DTB, NCBA) are parsed on the ConfirmApp handset — configuretrustedSender rules in Admin per client so lodge and retail inboxes stay accurate.

Brooks-style example: till on lodge LAN, local SQLite for sales, cloud ConfirmApp on the office phone for KCB/paybill SMS → POS polls live when online → claim with lodge order reference → local receipt prints from branch DB. Same API headers and endpoints as cloud-only POS; only routing and offline queue differ.

Field capture (PayConfirm APK)

Parsed SMS from the field handset is written to ConfirmApp cloud via POST /api/integration/capture (Supabase). POS integrations still use GET /api/integration/live with the integration key below.

  • Auth: Authorization: Bearer <CAPTURE_INGEST_SECRET> and x-client-code: brookslodge (your setup code).
  • Payment body: {"kind":"payment","mpesaCode":"...","amountKes":1200,...}
  • Unparsed body: {"kind":"unparsed","fingerprint":"...","rawBody":"..."}
  • Phone inbox UI: GET /api/integration/capture/inbox?limit=50 (same capture auth).
  • Parser rules on device: GET /api/integration/provider-rules (same capture auth).

Authentication

  • Use header x-client-code from your client setup.
  • Use header x-integration-key from Admin Portal, Developer integration block.
  • Rotate keys anytime from dashboard for immediate key revocation.

Provider Parsing Rules

  • Provider parsing rules are configured only in Admin Dashboard by support team.
  • APK end users do not see or edit regex patterns.
  • Rules sync to devices automatically after client code setup.
  • Each rule validates trusted sender ID before extracting reference, amount, and name.

GET Live Payments

Endpoint: /api/integration/live?limit=50

curl -X GET "https://confirmapp.pewang.company/api/integration/live?limit=50" \
  -H "x-client-code: acme_nairobi" \
  -H "x-integration-key: pc_xxxxxxxxxxxxxx"

Response fields: mpesaCode, amountKes, senderPhone, status, claimedKes, remainingKes.

POST Claim Payment

Endpoint: /api/integration/claim

curl -X POST "https://confirmapp.pewang.company/api/integration/claim" \
  -H "Content-Type: application/json" \
  -H "x-client-code: acme_nairobi" \
  -H "x-integration-key: pc_xxxxxxxxxxxxxx" \
  -d '{
    "mpesaCode": "UE93P3YJZA",
    "orderNumber": "POS-ORDER-94822",
    "reference": "POS-REF-94822",
    "claimAmountKes": 20
  }'
  • Required body: mpesaCode
  • Required at least one: orderNumber or reference
  • claimAmountKes optional. Omit for full claim.
  • Full claim moves payment to archive. Partial claim keeps payment live.
  • On success, webhook delivery runs immediately when webhook URL is configured.

Common Error Responses

  • 401: missing or invalid integration headers.
  • 400: missing required payload fields or invalid claim amount.
  • 400: payment not found, already fully claimed, or client is inactive.

Fast POS Sync Flow

  1. Poll live endpoint every 2 to 5 seconds, or when cashier opens pending orders.
  2. Match by amount and customer phone, then assign to pending order.
  3. Call claim endpoint with order number and reference.
  4. Update order status to paid after claim success response.

Webhook Push (Optional)

  • Configure webhook URL and secret per client in Dashboard.
  • On each successful claim, ConfirmApp sends payment.claimed event.
  • Verify signature from header x-confirmapp-signature using your webhook secret.
  • Delivery retries: up to 3 attempts with short backoff.
  • Delivery logs are visible in dashboard per client.
import crypto from "node:crypto";

export function verifyConfirmAppSignature(rawBody: string, signatureHeader: string, secret: string) {
  const expected = "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));
}

Webhook payload includes mpesaCode, claimMode, claimedKes, remainingKes, orderNumber, and reference.