Documentation & API

Build on the neutral acceptance layer.

2Pay owns the acceptance experience and inherits authorization and settlement from Ethiopia's licensed rails. This guide covers how a tap resolves, how each rail connects, and how merchants, POS/ECR vendors and fleet systems integrate.

Preview documentation. Endpoints, field names and payloads below are indicative and show the intended integration shape. Final schemas, rail-specific callback specifications, base URLs and sandbox credentials are issued to onboarded integration partners, and each rail is connected under its operator's own integration agreement.

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.

Merchants & fleets

Deploy HCE nodes and passive stands, then read confirmations and reconciliation feeds.

POS & ECR vendors

Embed the request/confirm flow in the till so tender and fiscal receipt stay in sync.

Rail & wallet operators

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.

  1. 1
    Sale context

    The cashier (or the ECR) sets the amount; the merchant, node and optional attendant are attached.

  2. 2
    Tap event

    A single-use, time-bound request URL is read by the customer's phone from the HCE node or stand.

  3. 3
    Server resolution

    2Pay's resolution engine returns a wallet deep link — or a dynamic ETHQR payload — for that one request.

  4. 4
    Native authorization

    The customer approves inside their own wallet with PIN or biometric. 2Pay is never the credential.

  5. 5
    Independent confirmation

    The rail's callback confirms settlement to the merchant terminal and the payer — never a screenshot or SMS.

Request lifecycle

StatusMeaning
createdRequest issued; waiting for a tap.
resolvedTap read; wallet deep link or ETHQR payload returned.
authorizedCustomer approved inside their wallet.
confirmedRail-side settlement confirmed. Terminal state; the only one that releases goods.
expiredTTL elapsed before authorization. Terminal.
failedDeclined 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.

EthioPay-IPS
Domestic settlement rail (inland)
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.
ETHQR
National QR standard · fallback rungs
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.
Telebirr
Mobile-money wallet
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.
CBE Birr
Bank-backed wallet
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.
PAPSS
Continental settlement · cross-border
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.
Awash Birr & other wallets/banks
Additional rails
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.

MethodPathPurpose
POST/v1/requestsCreate a single-use, time-bound payment request.
GET/v1/requests/{id}Fetch a request and its current status.
POST/v1/requests/{id}/cancelCancel an unauthorized request.
GET/v1/transactionsReconciliation export by date range, node or attendant.
GET/v1/nodesList HCE and passive nodes, with health and last-seen.
Create a request · curl
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
  }'
Response
// 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.

EventWhen
payment.confirmedRail-side confirmation received. Safe to release goods and print the fiscal receipt.
payment.failedDeclined or rail error.
request.expiredTTL elapsed without authorization.
node.alertA watchdog rule fired (e.g. repeated failed validations at one stand).
payment.confirmed · payload
{
  "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 }
  }
}
Verify a signature · TypeScript
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.

  1. 1The till posts the sale total to 2Pay (create request) and shows the tap prompt.
  2. 2The customer taps and authorizes in their own wallet.
  3. 3On the payment.confirmed event, the middleware marks the tender as paid and triggers the fiscal receipt.
  4. 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.

PhaseDefinitionOwnerTarget
1 · InitiationTap to wallet open, pre-filled2PayUnder 2 s
2 · AuthorizationPIN or biometric in the wallet (CDCVM)Wallet issuerMeasured, not claimed
3 · SettlementClearance on EthioPay-IPS / wallet railsNational railsMeasured, not claimed
4 · ConfirmationRail callback to the merchant app2PayUnder 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.

Request a commercial pilot

Tell us where you operate. We'll deploy HCE nodes or passive stands at your busiest lanes and measure the difference.

Funding memorandum & pitch deck

The technical white paper, funding memorandum and deck are shared under NDA with prospective investors.