Back to Developers

API Reference v1

Partner API

Mobile recharge and bill payments for India, Bangladesh and Singapore. One Bearer token, one corridor, no FX. Sandbox is free and instant.

Quickstart

From zero to a successful sandbox recharge in five steps.

1

Get a sandbox API key

Email developers@tachopay.com. We'll send a tpk_test_… key with 1,000 of test wallet credit in your chosen corridor (IN/BD/SG).
2

Confirm balance

curl
curl https://api.tachopay.com/v1/partner/balance \
  -H "Authorization: Bearer $API_KEY"
# → {"available":"1000.0000","currency":"BDT","corridor":"BD",...}
3

Pick an operator

curl
curl https://api.tachopay.com/v1/partner/operators \
  -H "Authorization: Bearer $API_KEY"
# → { "operators": [ { "id": 14, "name": "Grameenphone", ... } ] }
4

Send your first recharge (sandbox phone …0001 = SUCCESS)

curl
curl -X POST https://api.tachopay.com/v1/partner/topup \
  -H "Authorization: Bearer $API_KEY" \
  -H "Idempotency-Key: my-first-recharge-$(date +%s)" \
  -H "Content-Type: application/json" \
  -d '{"operator_id": 14, "phone": "8801712340001", "amount": 100}'

# → 200 OK in ~200ms:
# { "order_id":"TP202600007", "status":"PENDING", "balance_after":"900.0000", ... }
5

Check the final outcome

curl
curl https://api.tachopay.com/v1/partner/transactions/TP202600007 \
  -H "Authorization: Bearer $API_KEY"

# → { "status":"SUCCESS", "operator_ref":"MOCK-...", "settled_at":"..." }

You're integrated.

Production needs the same code with a tpk_live_… key (issued after KYC).

Authentication

Every request requires a Bearer token in the Authorization header. The token prefix decides the environment:

  • tpk_live_… — production. Real money moves.
  • tpk_test_… — sandbox. Mock providers, deterministic outcomes, no real money.
Authorization header
Authorization: Bearer tpk_live_<32 base64url chars>

Your token is shown ONCE.

We store only an HMAC-SHA256 hash. Lost token → regenerate (revokes the old one).

Optional security additions (set in the partner portal):

  • IP allowlist — reject requests not originating from configured IPs (403).
  • Rate limit — default 60 requests/minute, configurable.

Environments & sandbox

Each partner is assigned one corridor at onboarding. Your wallet is in that corridor's native currency. No FX conversion is applied to your transactions.

IN

India · INR

Direct operator · BBPS

BD

Bangladesh · BDT

Direct operator

SG

Singapore · SGD

Direct operator

Sandbox uses a deterministic mock provider. The last 4 digits of the recipient phone (or consumer number for bills) control the outcome:

SuffixOutcomeUse it to test
…0001SUCCESS (instant)Happy path
…0002FAILED + auto-refund (instant)Failure handling, wallet credit-back
…0003PENDING → SUCCESS after 30sAsync settle, status polling
…0004PENDING → FAILED + refund after 30sAsync failure handling
…0005Stuck PENDING (never settles)Webhook-failure / reconciliation

Wallet balance

GET/v1/partner/balance
200 OK
{
  "available": "8742.50",
  "currency": "BDT",
  "corridor": "BD",
  "updated_at": "2026-06-06T04:21:25.960Z",
  "environment": "sandbox"
}

List operators

GET/v1/partner/operators

Returns all active mobile operators in your corridor.

200 OK (excerpt)
{
  "corridor": "BD",
  "currency": "BDT",
  "count": 5,
  "operators": [
    {
      "id": 14,
      "api_operator_id": "GT-3",
      "name": "Grameenphone",
      "category": "MOBILE_PREPAID",
      "min_amount": "20.00",
      "max_amount": "1000.00",
      "local_currency": "BDT"
    },
    { "id": 15, "name": "Robi", ... },
    { "id": 16, "name": "Banglalink", ... }
  ]
}

Mobile prepaid recharge

POST/v1/partner/topup

Send a prepaid topup. Returns optimistic PENDING in 200-500ms; settlement runs async (single-step — no fetch required).

Required headers

Headers
Authorization: Bearer $API_KEY
Idempotency-Key: <unique-per-request>
Content-Type: application/json

Request body

operator_idintegerrequired
From GET /v1/partner/operators.
phonestringrequired
Recipient mobile. Country code optional if it matches your corridor.
amountnumberrequired
Amount in local currency. Must be within operator's min/max.
referencestring
Your internal reference. Echoed in the response and webhook.

Examples

curl
curl -X POST https://api.tachopay.com/v1/partner/topup \
  -H "Authorization: Bearer $API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "operator_id": 14,
    "phone": "8801712340001",
    "amount": 100,
    "reference": "your-internal-ref-789"
  }'
Node.js (fetch)
const res = await fetch("https://api.tachopay.com/v1/partner/topup", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.TACHOPAY_KEY}`,
    "Idempotency-Key": crypto.randomUUID(),
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    operator_id: 14,
    phone: "8801712340001",
    amount: 100,
    reference: "your-internal-ref-789",
  }),
});
const { order_id, status, balance_after } = await res.json();
Python (requests)
import os, uuid, requests
r = requests.post(
    "https://api.tachopay.com/v1/partner/topup",
    headers={
        "Authorization": f"Bearer {os.environ['TACHOPAY_KEY']}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "operator_id": 14,
        "phone": "8801712340001",
        "amount": 100,
        "reference": "your-internal-ref-789",
    },
    timeout=30,
)
data = r.json()
order_id = data["order_id"]
200 OK — initial response (always PENDING)
{
  "order_id": "TP202600007",
  "status": "PENDING",
  "operator_id": 14,
  "phone": "8801712340001",
  "amount_local": "100.0000",
  "currency": "BDT",
  "balance_after": "900.0000",
  "reference": "your-internal-ref-789",
  "created_at": "2026-06-06T05:01:12.330Z"
}

Bill payment — two-step flow

Postpaid mobile, BBPS, utility

BBPS / utility bill payment is a two-step flow. Fetch the outstanding amount first (so the customer sees the real bill), then submit the payment with the fetched amount.

POST /bill-infoPOST /pay-billGET /transactions/:order_id

Fetch outstanding → submit payment → confirm settlement

Step 1 — Fetch outstanding amount

POST/v1/partner/bill-info
Request
curl -X POST https://api.tachopay.com/v1/partner/bill-info \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "biller_id": 14,
    "consumer_number": "98765430001"
  }'
200 OK
{
  "biller_id": 14,
  "biller_name": "Airtel",
  "consumer_number": "98765430001",
  "customer_name": "Test Customer A",
  "amount": "500.00",
  "currency": "BDT",
  "due_date": "2026-06-13",
  "bill_period": "2026-06",
  "bill_reference": "BBPS-MOCK-1780730408152",
  "fetched_at": "2026-06-06T07:20:08.152Z",
  "instructions": "Submit this amount via POST /v1/partner/pay-bill ..."
}

Step 2 — Submit payment

POST/v1/partner/pay-bill

Submit the exact amount from step 1. BBPS requires ±1 unit match.

Request
curl -X POST https://api.tachopay.com/v1/partner/pay-bill \
  -H "Authorization: Bearer $API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "biller_id": 14,
    "consumer_number": "98765430001",
    "amount": 500.00,
    "reference": "bill-jun-2026"
  }'
200 OK
{
  "order_id": "TP202600121",
  "status": "PENDING",
  "biller_id": 14,
  "consumer_number": "98765430001",
  "amount_local": "500.0000",
  "currency": "BDT",
  "balance_after": "12101.00",
  "reference": "bill-jun-2026",
  "created_at": "2026-06-06T07:20:08.294Z"
}

Sandbox bill amounts

In sandbox, the consumer_number suffix decides the outstanding amount:…0001 → 500.00 · …0002 → 1500.00 · …0003 → 850.50 · …0004 → 2400.00 (overdue) · …0099 → 0.00

Transaction status

GET/v1/partner/transactions/:order_id

The authoritative source of truth for any transaction. Status field is masked to the three customer-facing values: PENDING · SUCCESS · FAILED.

200 OK — terminal SUCCESS
{
  "order_id": "TP202600007",
  "status": "SUCCESS",
  "operator_ref": "GT-9F2A1B7C",
  "amount_local": "100.0000",
  "currency": "BDT",
  "phone": "8801712340001",
  "created_at": "2026-06-06T05:01:12.330Z",
  "settled_at": "2026-06-06T05:01:14.880Z"
}
200 OK — FAILED (money already refunded)
{
  "order_id": "TP202600008",
  "status": "FAILED",
  "amount_local": "100.0000",
  "currency": "BDT",
  "phone": "8801712340002",
  "refunded_at": "2026-06-06T05:01:14.880Z"
}

Webhooks

Async settlement notifications

Configure a webhook URL in your partner portal to receive transaction.succeeded and transaction.failed events as they settle. 3 retries at 5-minute gaps; if all three fail, the delivery is marked FAILED and you should reconcile via GET /transactions/:order_id.

Signature header
X-TachoPay-Signature: t=1717658472,v1=4d9b7c2a...

# Verify with HMAC-SHA256:
#   signed_payload = t + "." + raw_request_body
#   expected      = hmac_sha256(your_webhook_secret, signed_payload)
#   compare       = timingSafeEqual(expected, v1)
POST body — transaction.succeeded
{
  "event": "transaction.succeeded",
  "order_id": "TP202600007",
  "operator_ref": "GT-9F2A1B7C",
  "amount_local": "100.0000",
  "currency": "BDT",
  "reference": "your-internal-ref-789",
  "settled_at": "2026-06-06T05:01:14.880Z"
}
POST body — transaction.failed
{
  "event": "transaction.failed",
  "order_id": "TP202600008",
  "amount_local": "100.0000",
  "currency": "BDT",
  "refunded_at": "2026-06-06T05:01:14.880Z"
}

Idempotency

Every value-moving call (/topup, /pay-bill) requires an Idempotency-Key header. This guarantees safe retries on network errors.

  • Same key + same body within 24h → cached response (header Idempotent-Replay: true).
  • Same key + different body → 409 idempotency_conflict.
  • No key → 400 idempotency_key_required.
Safe retry pattern (pseudocode)
const idempotencyKey = uuid();
let attempt = 0;
while (attempt < 5) {
  try {
    return await topup({ idempotencyKey, ... });
  } catch (networkError) {
    attempt++;
    await sleep(Math.pow(2, attempt) * 1000);  // exponential backoff
    // Retry with the SAME idempotency key — replay-safe.
  }
}

MCP server — for AI agents

POSThttps://api.tachopay.com/mcp

We expose the full Partner API as a Model Context Protocol (MCP) server. Any AI agent (Claude Desktop, OpenAI Agents SDK, custom orchestration) can connect with the same Bearer token and call our endpoints as tools — no glue code needed.

Claude Desktop config

~/Library/Application Support/Claude/claude_desktop_config.json
{
  "mcpServers": {
    "tachopay": {
      "url": "https://api.tachopay.com/mcp",
      "headers": {
        "Authorization": "Bearer tpk_test_REPLACE_ME"
      }
    }
  }
}

Available tools

ToolPurpose
tachopay_get_balanceWallet balance
tachopay_list_operatorsList operators / billers (filterable)
tachopay_topupMobile prepaid recharge
tachopay_pay_billPostpaid / BBPS bill payment
tachopay_get_transactionCheck transaction status

All tools enforce the same money rules and idempotency as the REST API. Sandbox keys give the AI safe, deterministic playground access.

Error codes

StatusCodeCause
400validation_failedMissing/malformed field
400idempotency_key_requiredMissing Idempotency-Key header
401invalid_api_keyToken revoked, malformed, or wrong
402insufficient_balanceWallet below required amount
403kyc_requiredLive key without KYC approval
403ip_not_allowedCaller IP not in allowlist
404operator_not_found_or_wrong_corridorOperator id invalid for your corridor
404not_foundorder_id doesn't exist or isn't yours
409idempotency_conflictSame Idempotency-Key, different body
422amount_out_of_rangeBelow operator min or above max
429rate_limit_exceededPer-key limit hit
500internal_errorOur problem — safe to retry with same Idempotency-Key

Money rules

Read once — your customer-support and reconciliation teams will thank you.

  • Debit-first. Your wallet is debited the moment you submit. The response includes balance_after.
  • Refund only on explicit FAILED. If upstream explicitly fails, we refund automatically. Stuck/disputed transactions are NOT auto-refunded — they hold until manually resolved.
  • Idempotency is required. Same key + same body = cached response. Different body = 409. Safe to retry on network errors.
  • Zero FX. Your wallet is in your corridor's currency; upstream settles in the same currency. No conversion loss.
  • Status mask. You only ever see PENDING, SUCCESS, or FAILED. Internal states (INITIATED, DISPUTED) are not exposed.
  • Single corridor per partner. Reduces ambiguity in your code and our routing.

Ready to integrate?

Email us with your use case + expected volume. We'll send sandbox credentials, a Postman workspace, and an onboarding call within one business day.