Overview
2Pay is a neutral B2B payment orchestration and tap-to-pay layer. It resolves each tap at a physical checkout into a single-use request, opens the customer's own wallet pre-filled, and confirms settlement to the merchant from the rail — without holding float, custodying customer funds or issuing wallets.
Deploy HCE nodes and passive stands, then read confirmations and reconciliation feeds.
Embed the request/confirm flow in the till so tender and fiscal receipt stay in sync.
Connect a rail once; every 2Pay merchant can then accept it at the counter.
How a payment resolves
Five steps from sale to confirmation. The same request ID follows the payment through every rung of the fallback ladder.
- 1Sale context
The cashier (or the ECR) sets the amount; the merchant, node and optional attendant are attached.
- 2Tap event
A single-use, time-bound request URL is read by the customer's phone from the HCE node or stand.
- 3Server resolution
2Pay's resolution engine returns a wallet deep link — or a dynamic ETHQR payload — for that one request.
- 4Native authorization
The customer approves inside their own wallet with PIN or biometric. 2Pay is never the credential.
- 5Independent confirmation
The rail's callback confirms settlement to the merchant terminal and the payer — never a screenshot or SMS.
Request lifecycle
| Status | Meaning |
|---|---|
| created | Request issued; waiting for a tap. |
| resolved | Tap read; wallet deep link or ETHQR payload returned. |
| authorized | Customer approved inside their wallet. |
| confirmed | Rail-side settlement confirmed. Terminal state; the only one that releases goods. |
| expired | TTL elapsed before authorization. Terminal. |
| failed | Declined or rail error. Terminal; a new request is needed. |
Rail integration
2Pay sits above the rails. For each tap it decides how the payer's own app is launched, then listens for the rail's settlement confirmation. Authorization (PIN, CDCVM biometrics) always happens inside the wallet, so upgrades the wallet ships are inherited immediately.
The three-rung ladder — native NFC tap, dynamic on-screen ETHQR, passive stand QR — uses the same request, confirmation and ledger at every rung.
- 2Pay provides
- Single-use request resolved to a payer-to-merchant instant payment instruction.
- Customer launch
- Via the payer's bank or wallet app that is an IPS participant.
- Confirmation
- Rail-side settlement callback; reconciled daily against IPS records.
- 2Pay provides
- A dynamic, server-generated ETHQR payload (single-use) for the on-screen rung; a printed ETHQR on passive stands.
- Customer launch
- Customer scans with any ETHQR-compatible wallet when NFC isn't available.
- Confirmation
- Same request ID, same rail-side confirmation as the NFC rung.
- 2Pay provides
- Merchant, amount and reference as a wallet deep link.
- Customer launch
- Android App Links / iOS Universal Links open the wallet pre-filled; the customer confirms in-wallet.
- Confirmation
- Wallet/rail callback mapped to the 2Pay request and pushed to the merchant.
- 2Pay provides
- Merchant, amount and reference as a wallet deep link.
- Customer launch
- Same App Links / Universal Links flow; PIN or biometric upgrades are inherited automatically.
- Confirmation
- Wallet/rail callback mapped to the 2Pay request and pushed to the merchant.
- 2Pay provides
- The route and quote display for the cross-border leg; the payment instruction is passed to a licensed participant. 2Pay is never a PAPSS participant itself.
- Customer launch
- The payer's own bank or wallet app, at an institution connected to PAPSS.
- Confirmation
- Confirmation comes back through the licensed participant. Cross-border confirmation time is measured per corridor, not promised.
- 2Pay provides
- Deep link where the wallet supports it, otherwise a dynamic ETHQR payload.
- Customer launch
- Customer's own app; 2Pay stays wallet-neutral across every rail.
- Confirmation
- Callback or IPS settlement record, depending on the operator.
Rail availability depends on each operator's onboarding. Contact us for current coverage and for the callback specification of a specific rail.
API preview
A small REST surface over HTTPS. All requests use a per-merchant bearer key; create calls accept an Idempotency-Key so retries never create a second request. Amounts are decimal strings in ETB.
| Method | Path | Purpose |
|---|---|---|
| POST | /v1/requests | Create a single-use, time-bound payment request. |
| GET | /v1/requests/{id} | Fetch a request and its current status. |
| POST | /v1/requests/{id}/cancel | Cancel an unauthorized request. |
| GET | /v1/transactions | Reconciliation export by date range, node or attendant. |
| GET | /v1/nodes | List HCE and passive nodes, with health and last-seen. |
curl -X POST "$BASE_URL/v1/requests" \
-H "Authorization: Bearer $TWOPAY_API_KEY" \
-H "Idempotency-Key: sale-8214-attempt-1" \
-H "Content-Type: application/json" \
-d '{
"merchant_id": "mer_bole_fuel",
"node_id": "node_pump_03",
"amount": "4000.00",
"currency": "ETB",
"reference": "TRIP-8214",
"ttl_seconds": 90
}'// 201 Created
{
"id": "req_8H3K04",
"status": "created",
"amount": "4000.00",
"currency": "ETB",
"expires_at": "2026-10-14T09:31:30Z",
"tap_url": "https://<resolver-host>/r/8H3K04",
"qr_payload": "<dynamic ETHQR payload>"
}Webhooks & signatures
Confirmation is pushed from the rail-side callback — never inferred from the customer's screen. Every delivery is signed with HMAC-SHA256 over the timestamp and raw body. Verify the signature and reject anything older than five minutes before acting on it.
| Event | When |
|---|---|
| payment.confirmed | Rail-side confirmation received. Safe to release goods and print the fiscal receipt. |
| payment.failed | Declined or rail error. |
| request.expired | TTL elapsed without authorization. |
| node.alert | A watchdog rule fired (e.g. repeated failed validations at one stand). |
{
"id": "evt_01JA7Q4M",
"type": "payment.confirmed",
"created_at": "2026-10-14T09:30:41Z",
"data": {
"request_id": "req_6T5D33",
"merchant_id": "mer_habesha_grill",
"node_id": "node_table_12",
"attendant": { "id": "att_03", "name": "Dawit" },
"bill": "750.00",
"tip": "50.00",
"total": "800.00",
"currency": "ETB",
"rail": "ethiopay-ips",
"rail_reference": "<rail transaction reference>",
"latency_ms": { "initiation": 1180, "confirmation": 640 }
}
}import { createHmac, timingSafeEqual } from "node:crypto";
// Header: 2Pay-Signature: t=1760434241,v1=<hex hmac>
export function verify(rawBody: string, header: string, secret: string) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const age = Math.abs(Date.now() / 1000 - Number(parts.t));
if (!parts.t || !parts.v1 || age > 300) return false; // reject stale or malformed
const expected = createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1);
return a.length === b.length && timingSafeEqual(a, b);
}Team nodes & tips
For hospitality and high-throughput retail, one master settlement account provisions a sub-node for every attendant. Pass attendant_id and an optional tip when creating a request. The customer taps once and approves one total; the confirmation event returns the bill, tip and attendant separately, so tips post to the right ledger and every sale is attributed to a named person. Nothing moves through personal bank or wallet accounts.
ECR & POS middleware
With the merchant's opt-in, 2Pay embeds in the electronic cash register so tender, confirmation and fiscal receipt stay in step, in line with the Ministry of Revenues fiscal protocol.
- 1The till posts the sale total to 2Pay (create request) and shows the tap prompt.
- 2The customer taps and authorizes in their own wallet.
- 3On the payment.confirmed event, the middleware marks the tender as paid and triggers the fiscal receipt.
- 4A per-shift feed of ECR receipts vs. confirmed taps flags paid-but-unreceipted sales for the owner.
Latency & reconciliation
We measure four phases and commit only to the two 2Pay controls. Median and 95th-percentile times are logged per transaction and reconciled daily against rail records; each confirmation event carries its own measured latencies.
| Phase | Definition | Owner | Target |
|---|---|---|---|
| 1 · Initiation | Tap to wallet open, pre-filled | 2Pay | Under 2 s |
| 2 · Authorization | PIN or biometric in the wallet (CDCVM) | Wallet issuer | Measured, not claimed |
| 3 · Settlement | Clearance on EthioPay-IPS / wallet rails | National rails | Measured, not claimed |
| 4 · Confirmation | Rail callback to the merchant app | 2Pay | Under 1 s |
Security model
- Merchant HCE nodes are gated by Google Play Integrity before any request is issued.
- Requests are single-use and time-bound; a replayed or copied request is rejected.
- Stand tags are write-locked; repeated failed validations at one stand raise a swap alert.
- No PINs, passwords or card data are stored. Authorization stays in the wallet.
- Webhooks are HMAC-signed with timestamp tolerance; confirmation comes from the rail.
- Row-level security isolates every merchant's data (tenant isolation).
Get sandbox access
Tell us about your rail, POS/ECR product or fleet system and we'll share the current schemas, a sandbox base URL and test credentials, and walk through onboarding.