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.
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; 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.
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.
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, published, or deployed; it remains a Core/mobile release gate pending
authoritative migration ordering. The currently deployed Core jar already
contains the 410 tombstone, so the reported raw 404 came from an earlier or
different request path rather than the current public controller.
Deployment-facing endpoints
| Surface | Test hostname |
|---|---|
| Backoffice | pos.test.emali2.damplabs.com |
| Cashier | pos-cashier.test.emali2.damplabs.com |
| API | pos-api.test.emali2.damplabs.com |
| Docs | docs-pos.test.emali2.damplabs.com |
| Receipts | receipts.test.emali2.damplabs.com |
Production uses the corresponding *.pos.damplabs.com hosts only after
production database, object storage, identity clients, secrets, certificates,
and rollback evidence are provisioned.