A small integration, with the bank in control.
Expose two adapter endpoints, receive signed events, and choose how customers see HathPay: our embedded module in your app, or your own screens on our APIs.
Embedded module
Your backend opens a short-lived session and your app shows the HathPay module in a web view. Activation, PIN, limits, pause, history and removal are ready-made and carry your colours.
- One server call opens a session
- Your theme on every screen
- Status changes pushed back to your app
Headless partner API
Build every screen yourself and call the partner API for customers, limits, pause and history. Every event arrives as a signed webhook for your app and records.
- The same capabilities as the module
- You own every pixel
- Native SDKs for a signed partner
Signed both ways. Safe to retry.
Every request in either direction carries a timestamp, a single-use nonce and an HMAC-SHA256 signature. A repeated request is refused as a replay. A retried payment is recognised by its idempotency key and never charged twice.
- Open a sessionAfter your own login, one call returns a token for the in-app module.
- Decide every paymentYour adapter receives authorise-debit and returns approved or declined.
- Verify every eventCheck the signature, the window and the nonce before you act on it.
# Your backend, after your own login import hashlib, hmac, json, secrets, time, httpx def signed(method, path, payload): body = json.dumps(payload).encode() ts, nonce = str(int(time.time())), secrets.token_hex(12) msg = f"{ts}\n{nonce}\n{method}\n{path}\n" + hashlib.sha256(body).hexdigest() sig = hmac.new(PARTNER_SECRET, msg.encode(), hashlib.sha256).hexdigest() return httpx.request(method, HATHPAY + path, content=body, headers={ "X-HathPay-Bank": BANK_ID, "X-HathPay-Timestamp": ts, "X-HathPay-Nonce": nonce, "X-HathPay-Signature": sig}) r = signed("POST", "/v1/partner/sessions", { "customer_ref": customer.ref, "accounts": [{"account_token": a.token, "label": a.label} for a in customer.accounts], "purpose": "activate", }) session_token = r.json()["session_token"] # hand to the in-app module
# Your adapter: HathPay asks, the bank decides @app.post("/hathpay/authorize-debit") def authorize_debit(req): verify_hathpay(req, ADAPTER_SECRET) # signature + window + nonce p = req.json() if seen(p["idempotency_key"]): return previous_result(p["idempotency_key"]) # safe retries account = accounts.by_token(p["account_token"]) if account.balance < p["amount_minor"]: return {"status": "declined", "reason": "insufficient_funds"} ref = ledger.debit(account, p["amount_minor"], merchant=p["merchant_id"]) return {"status": "approved", "bank_txn_ref": ref} @app.post("/hathpay/reverse") def reverse(req): ...
# Every event is signed. Check it, and refuse a nonce you have seen. def verify_hathpay(req, secret): ts = req.headers["X-HathPay-Timestamp"] nonce = req.headers["X-HathPay-Nonce"] sig = req.headers["X-HathPay-Signature"] if abs(time.time() - int(ts)) > 300: raise Unauthorized("stale") msg = f"{ts}\n{nonce}\n{req.method}\n{req.path}\n" + sha256_hex(req.body) expected = hmac.new(secret, msg.encode(), hashlib.sha256).hexdigest() if not hmac.compare_digest(expected, sig): raise Unauthorized("bad signature") if nonce_cache.seen(nonce): # a replayed event raise Unauthorized("replayed") nonce_cache.add(nonce, ttl=305)
Every call between you and HathPay.
Short on purpose. The money path is two endpoints on your side.
| Direction | Endpoint | What it does |
|---|---|---|
| Bank → HathPay | POST /v1/partner/sessions | Open a module session for a customer you have signed in. |
| Bank → HathPay | GET /v1/partner/customers/{ref} | HathPay status, linked account and limits for one customer. |
| Bank → HathPay | POST …/customers/{ref}/pause · /resume | Stop or restart palm payments for a customer. |
| Bank → HathPay | PUT …/customers/{ref}/limits | Set per-payment and daily limits within your caps. |
| Bank → HathPay | GET …/customers/{ref}/transactions | Palm payment history for statements and support. |
| Bank → HathPay | DELETE /v1/partner/customers/{ref} | Remove HathPay for a customer and delete their templates. |
| HathPay → Bank | POST {adapter}/authorize-debit | Approve or decline one palm payment. You move the money. |
| HathPay → Bank | POST {adapter}/reverse | Undo an approved debit when a later step fails. |
| HathPay → Bank | POST {webhook} | Signed events: enrollment.completed, payment.authorized, payment.declined, customer.paused and more. |
Every surface is signed. Every decision is logged.
Three kinds of client talk to HathPay, each with its own credential. HathPay talks to the bank through two narrow contracts: the adapter the bank exposes, and the events it receives.
Scroll sideways to see the whole diagram.
API gateway
Four separate surfaces: partner, embedded, device and operations. A credential for one opens nothing on another.
rate-limited per terminalBiometric service
Checks each capture, normalises the palm region, extracts features and searches the bank's gallery.
stores templates, never imagesRisk engine
Bank and customer limits, velocity rules, PIN step-up and lockout, and a cooldown for terminals presenting unknown palms.
every rule outcome recordedDevice trust
Terminals generate their own keys and need approval before they work. Enrolment and merchant terminals cannot stand in for each other.
ECDSA P-256 · revocableOrchestrator
Turns an identified palm into exactly one authorisation request. Idempotency keys stop double charges; failures trigger reversal.
2-minute payment windowAudit and events
A hash-chained log shows any altered row. Events reach the bank as signed webhooks, retried until delivered.
verifiable from the consoleSee it working, end to end, in thirty minutes.
Activation in a bank app, enrolment at a branch terminal and a palm payment settled through a simulated bank. Then the architecture and the pilot plan.