Skip to content

POS Integration Flows

This page is the authoritative boundary map for the standalone POS. It separates retail operations from regulated money movement and shows how Emali POS, third-party POS systems, Emali Mobile Money, and external payment providers interact.

System boundary

flowchart LR
    subgraph Merchant["Merchant estate"]
        EPOS["Emali POS<br/>cashier, Android, backoffice"]
        TPOS["Third-party POS / ERP"]
        HW["Scanner, printer,<br/>cash drawer, card terminal"]
    end

    subgraph POS["Standalone emali2-pos"]
        API["POS API<br/>sales, stock, receipts"]
        DB[("POS PostgreSQL")]
        WORKER["Payment and outbox workers"]
        ADAPTERS["Provider-neutral adapters"]
    end

    subgraph Platform["Emali Mobile Money / Core"]
        CORE["Store-scoped merchant<br/>collection API"]
        WALLET["Customer wallet,<br/>approval and ledger"]
        APP["Customer mobile app"]
    end

    subgraph Providers["Country-specific providers"]
        MOBILE["Mobile-money adapter"]
        CARD["Card-present service"]
        FISCAL["Fiscalization adapter"]
    end

    EPOS --> API
    HW --> EPOS
    TPOS -->|"OAuth machine token + exact store binding"| API
    API --> DB
    DB --> WORKER
    WORKER --> ADAPTERS
    ADAPTERS -->|"EMALI_APP"| CORE
    CORE --> APP
    APP --> WALLET
    WALLET --> CORE
    ADAPTERS --> MOBILE
    ADAPTERS --> CARD
    ADAPTERS --> FISCAL

POS owns baskets, prices, stock reservations, tender state, receipts, shifts, terminal enrollment, and reconciliation. Emali Core owns customer identity, wallet approval, money movement, and ledger settlement. The systems share neither application code nor databases.

Country integrations are adapters selected by configuration. MobileMoneyPort and the fiscalization port are generic contracts; Daraja/M-PESA and KRA/eTIMS are optional Kenya implementations, not POS domain concepts.

Choose an integration pattern

Every merchant channel enters through the standalone POS API. Emali-native cashiers and existing third-party POS systems use different OAuth clients and terminal keys, but they converge on the same store-scoped sales, payment, refund and reconciliation invariants. Neither channel receives direct Core credentials or database access.

flowchart TD
    START["Merchant needs a checkout channel"] --> CHANNEL{"Which cashier system stays at the lane?"}
    CHANNEL -->|"Emali web or Android"| NATIVE["Emali-native POS client<br/>Authorization Code + PKCE<br/>enrolled terminal key"]
    CHANNEL -->|"Existing POS, ERP or ecommerce"| MODE{"Who owns the retail record?"}
    MODE -->|"Standalone POS"| RETAIL["Retail-backend integration<br/>catalog, stock, sale and receipt APIs"]
    MODE -->|"Existing system"| PAYMENT["Payment-only integration<br/>mirrored server-priced POS sale"]
    NATIVE --> POSAPI["Standalone POS API<br/>exact merchant/store scope"]
    RETAIL --> MACHINE["Merchant machine token<br/>plus enrolled terminal proof"]
    PAYMENT --> MACHINE
    MACHINE --> POSAPI
    POSAPI --> RAIL{"Store-enabled payment rail"}
    RAIL -->|"EMALI_APP"| CORE["Emali Core merchant collection<br/>customer approval + wallet ledger"]
    RAIL -->|"MOBILE_MONEY"| MOBILE["Configured country adapter<br/>for example Daraja"]
    RAIL -->|"CARD_PRESENT"| CARD["Certified card-present boundary"]
    CORE --> RESULT["Normalized authoritative outcome"]
    MOBILE --> RESULT
    CARD --> RESULT
    RESULT --> POSAPI

The payment-only path still mirrors a POS sale because amount, currency, store, idempotency, refund allocation and reconciliation must remain server-owned. It is not a raw “charge this amount” proxy to Core.

Credential and trust matrix

Connection OAuth/client type Where private material lives POS authorization
Cashier Web → POS Public browser client, Authorization Code + PKCE Non-exportable WebCrypto terminal key in the browser; no OAuth client secret Staff assignment + exact enrolled WEB terminal proof
Android → POS Public native client, Authorization Code + PKCE Android Keystore terminal key; no OAuth client secret Staff assignment + exact enrolled Android terminal proof
Existing POS/ERP → POS Confidential MERCHANT_INTEGRATION client; prefer private_key_jwt Merchant-controlled HSM, vault or mode-0600 key file Matching azp/client_id, pos-api audience, approved realm role, persisted organization/store binding, and terminal proof for mutations
POS service → Emali Core Dedicated emali2-pos-service client; prefer private_key_jwt POS runtime secret mount only Exact issuer, core-internal-api audience, POS_SERVICE client role, matching client claims, and active Core client/organization/store binding
POS adapter → external provider Provider-specific server credential Vault or external-secret reference selected by the store profile Exact enabled capability and provider code; never selected from a client-supplied unbound provider

Client secrets, private keys, terminal keys and provider credentials are never returned by POS APIs. Public web and Android packages contain no secret.

Emali customer-app payment

The cashier starts a merchant collection, not an agent cash-out. POS must never call /api/v1/agents/withdraw/quote and does not accept an agentIdentifier in this flow.

sequenceDiagram
    autonumber
    actor Cashier
    participant POS as Emali POS
    participant Worker as POS payment worker
    participant Core as Emali Core
    participant App as Customer mobile app
    participant Ledger as Wallet ledger

    Cashier->>POS: Select EMALI_APP, amount, customer reference
    POS->>POS: Reserve stock and persist PENDING tender
    POS->>POS: Encrypt customer reference in durable attempt
    POS-->>Cashier: Prompt queued
    Worker->>Core: POST store-scoped merchant-collection request<br/>OAuth service token + opaque idempotency key
    Core->>Core: Verify issuer, audience, role, client and store binding
    Core-->>App: Push/in-app/websocket approval prompt
    App->>Core: Approve or deny with registered-device proof
    alt Approved and wallet payment commits
        Core->>Ledger: Atomic customer-to-merchant payment
        Ledger-->>Core: Committed
        Core-->>Worker: CONSUMED
        Worker->>POS: Mark tender CAPTURED and complete sale
        POS-->>Cashier: Receipt ready
    else Denied, expired or cancelled
        Core-->>Worker: DENIED / EXPIRED / CANCELLED
        Worker->>POS: Mark tender FAILED
        POS-->>Cashier: Offer another payment method
    else Outcome cannot be confirmed
        Worker->>POS: Move attempt to MANUAL_REVIEW
        POS-->>Cashier: Keep tender pending and block cash fallback
    end

Only Core status CONSUMED is a captured payment. APPROVED is not enough. POS polls with bounded backoff, removes the encrypted routing reference after dispatch, and keeps only a masked reference on the tender. A captured refund uses the original authorization UUID and the same organization/store scope.

The POS service token must satisfy all of these checks:

Check Required value
Issuer exact configured Emali realm issuer
Audience core-internal-api
azp and client_id both present, equal, and the configured POS service client
Role POS_SERVICE from the approved POS client role source
Authorization active persisted client + organization + store binding

Raw customer references, till values, secrets, and idempotency values do not belong in resource identifiers or logs.

Third-party POS and ERP clients

A merchant may integrate an existing POS, ecommerce platform, warehouse system, or ERP with the standalone POS API. This is a separate direction from the POS-to-Core service integration.

sequenceDiagram
    autonumber
    participant Admin as Merchant administrator
    participant IAM as Identity provider
    participant Client as Third-party POS / ERP
    participant POS as Standalone POS API
    participant DB as POS database
    participant Events as POS outbox / events

    Admin->>IAM: Provision confidential client
    Admin->>IAM: Add pos-api audience and approved realm role
    Admin->>POS: Bind exact client ID to organization and stores
    Client->>IAM: client_credentials<br/>(prefer private_key_jwt)
    IAM-->>Client: JWT with matching azp + client_id
    Client->>POS: API request + JWT + idempotency key
    POS->>POS: Verify issuer, pos-api audience and realm role source
    POS->>DB: Verify active exact store binding
    POS->>DB: Apply store-scoped operation atomically
    POS->>Events: Publish safe operational event
    POS-->>Client: Stable response or safe error code

The token proves client identity; the POS-owned binding decides which merchant and stores that identity can access. There is no global-store fallback. Missing or mismatched azp/client_id, a role from an unrelated client, or an inactive binding fails closed. Terminal mutations additionally require the enrolled terminal's request signature.

A runnable, secret-safe catalog proof is available in examples/third-party-pos/catalog-smoke.sh, with setup and checkout guidance in examples/third-party-pos/README.md. The preferred path creates a 60-second RS256 or ES256 private_key_jwt assertion from a protected merchant key, submits it from a temporary mode-0600 file, and removes it before the catalog call. A transitional secret-file mode remains available for existing clients. Both modes obtain a client-credentials token and print only a safe catalog count; neither prints the token, assertion, private key, client secret, merchant/store names, or catalog contents.

Protected third-party mutations can use examples/third-party-pos/terminal-proof.py. The reference signer accepts the exact body, an opaque idempotency key, and an enrolled EC or RSA private key only through mode-0600, non-symlink files. It emits only the six terminal proof headers. Its self-test shares the canonical v1 golden vector with the server, Android terminal, web cashier, and remote UAT client. The caller remains responsible for sending the exact signed bytes and a short-lived pos-api OAuth token.

Use stable API versions, an opaque Idempotency-Key for every mutation, and event/outbox delivery for downstream synchronization. Clients must not receive Keycloak secrets through POS APIs.

Third-party checkout through Emali

An existing merchant POS can use Emali only as a payment rail, or use the standalone POS as the authoritative retail backend. In both modes it calls the standalone POS API—not Emali Core and never a platform database. For checkout mutations, the merchant also enrolls a HARDWARE_POS terminal key and signs the exact method, path, body hash, nonce, timestamp and idempotency key.

sequenceDiagram
    autonumber
    participant Lane as Existing POS / ecommerce
    participant IAM as Emali identity
    participant POS as Standalone POS API
    participant Core as Emali Core
    participant App as Customer app

    Lane->>IAM: client_credentials using private_key_jwt
    IAM-->>Lane: short-lived pos-api token
    Lane->>POS: Create sale<br/>OAuth + exact store binding + terminal proof
    POS-->>Lane: Sale UUID and server-priced totals
    Lane->>POS: Collect WALLET / EMALI_APP<br/>opaque idempotency key + terminal proof
    POS-->>Lane: 202 payment pending
    POS->>Core: Store-scoped merchant collection<br/>POS service identity
    Core-->>App: Customer approval prompt
    App->>Core: Approve or deny
    Core-->>POS: CONSUMED / DENIED / EXPIRED
    Lane->>POS: Poll sale or tender status
    POS-->>Lane: Captured receipt or safe terminal failure

Current integration modes are:

Mode What the external system owns What Emali POS owns Current status
Retail backend Cashier UI/device only Catalog, stock, sale, tender, receipt, refund and reconciliation Implemented
Payment-only lane External basket and receipt presentation A mirrored server-priced sale and the electronic tender lifecycle Implemented through versioned sale APIs; requires an enrolled proof-bearing terminal
Inventory/ERP sync ERP master workflow Store-scoped catalog, stock and inventory documents Implemented for authenticated API reads/writes
Event-driven export ERP/accounting/BI consumer Signed outbound delivery with replay and retry Planned; consumers must poll safe read models until the webhook-delivery phase is implemented

The payment-only mode still creates a POS sale record so amount, currency, store, tender, idempotency, refund and reconciliation invariants remain server-owned. The external lane must not send PAN, PIN, mobile-wallet secrets, Emali customer credentials, or an agent identifier.

Other payment integrations

Every electronic provider returns a normalized POS result:

Provider family Typical initiation Captured evidence Important boundary
Emali customer app Core merchant-collection request Core CONSUMED Never use agent cash-out
External mobile money Country adapter prompt or merchant collection Provider query/callback confirmed M-PESA is one optional adapter
Card present Certified terminal/card service handoff Acquirer-approved transaction POS never handles PAN, PIN, track or EMV cryptograms
Cash Cashier confirmation Server-owned cash tender No external provider
Loyalty POS loyalty authorization POS ledger/points result Not platform money

A timeout is not a decline. When an external write may have succeeded but the answer is unknown, POS keeps the tender unresolved and sends it to manual reconciliation. It must not silently accept cash and risk charging twice.

Error isolation

The customer cash-out/agent quote API is a different product flow owned by the mobile-money clients and Core. A failure there must be mapped by those clients to a stable customer-safe state such as agent_unavailable; an upstream error body or submitted identifier must never be rendered directly.

As of the 26 July 2026 source audit, the standalone POS is isolated from that route and has a CI guard enforcing the boundary. Core/mobile commit c0ab1c6d245df43f63343422e491e1511dec5178 prepares the governed non-enumerating contract in an isolated worktree: public agent cash-out routes remain retired with 410 Gone, and Android/iOS no longer submit or display the legacy identifier-bearing request or raw response body. Its focused Core, Android, iOS contract, and iOS Simulator checks pass. That commit has not been merged as one reconciled mobile release; Android/iOS artifact rollout remains a platform release gate. The currently deployed Core API and worker run sha256:a5e4e13d3c62739ca7610b24512c889c87463deb9a30c4f6690ab63a31e0912f and the Core jar contains the 410 tombstone. The reported raw 404 therefore came from an earlier/different request path or an older client, not from the current public tombstone controller.

Deployment-facing endpoints

Surface Canonical TEST hostname Current pilot alias
Backoffice pos.test.emali2.damplabs.com backoffice.pos.damplabs.com
Cashier pos-cashier.test.emali2.damplabs.com cashier.pos.damplabs.com
API pos-api.test.emali2.damplabs.com none; production API remains disabled
Docs docs-pos.test.emali2.damplabs.com docs.pos.damplabs.com
Receipts receipts.test.emali2.damplabs.com receipts.pos.damplabs.com

The four pilot aliases currently return HTTP 200 and deliberately use TEST identity, API origins and data. They are not evidence of a production deployment. Production still requires an independent database, object storage, identity clients, secrets, certificates, provider credentials, monitoring and rollback evidence before its API is scaled above zero.