InfinityPay

Partner Integration

Direct Wallet Push

A complete integration guide for a billing platform, ecommerce site, or app collecting payments on behalf of InfinityPay merchants — from the first API call through to handling a failed payment.

How it fits together

text
Your system ──API key──▶ InfinityPay ──▶ Mobile money ──▶ Customer's wallet
      ▲                        │
      └──────  webhook  ───────┘

You integrate with InfinityPay and nothing else. InfinityPay holds the provider relationships and reaches them from its own approved infrastructure. You never hold, see, or configure provider credentials.

A merchant's IP allowlist governs access to the InfinityPay API. It is unrelated to any provider-side allowlist, which covers InfinityPay's own servers and is not your concern.

What the merchant sets up first

  1. Creates an InfinityPay account and completes verification.
  2. Generates API credentials at /portal/api-credentials. Sandbox keys work immediately; live keys become available once the account is approved, verified, and priced — there is no per-key approval step.
  3. Gives you the secret key.
  4. Sets a webhook URL and generates a signing secret at /portal/webhooks.

The webhook URL is not optional in practice

With no webhook URL configured, no events are sent at all — silently, with no error. If you plan to react to payment outcomes rather than poll for them, confirm the merchant has set one before going live.

Authentication

Send the secret key as a bearer token, or as X-API-Key — both are accepted.

bash
Authorization: Bearer sk_live_YOUR_SECRET_KEY_HERE
  • sk_live_… moves real money. sk_test_… is sandbox — no provider call, no money.
  • pk_… is a public identifier, not a credential. It cannot authenticate and is rejected if sent as a token.
  • The secret is shown once at creation and stored only as a hash. It cannot be recovered — rotate if lost.
  • Server-side only. Never ship a secret key in a browser or mobile app.

There is no request signing today

Requests are authenticated by the bearer key over HTTPS, optionally narrowed by an IP allowlist. InfinityPay does not currently verify an HMAC signature on inbound API requests — do not build one expecting it to be checked. Outbound webhooks are signed; that is separate and covered below.

Sending a push

POST/v1/collections/wallet-push

Send a payment prompt to the customer's phone.

API key (collections:write)
bash
curl -X POST https://api.infinitypay.me/v1/collections/wallet-push \
  -H "Authorization: Bearer sk_live_YOUR_SECRET_KEY_HERE" \
  -H "Idempotency-Key: 8f1c2d3e-4b5a-6c7d-8e9f-0a1b2c3d4e5f" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": "5000.00",
    "currency": "TZS",
    "phone": "+255712345678",
    "customer_name": "Asha Mushi",
    "reference": "INV-2026-000412",
    "description": "March broadband subscription"
  }'
FieldRequiredNotes
amountYesDecimal string, e.g. "5000.00".
phoneYesThe customer's mobile money number.
currencyNoDefaults to TZS.
customer_nameNoShown on the merchant's records.
referenceNoYour order or invoice id. Echoed back on every response and webhook.
descriptionNoFree text.
merchant_idNoYou do not need this. Your API key already identifies the merchant. Accepted if sent, and rejected if it is not the key's own merchant.

Idempotency-Key is a required header, not a body field.

json
{
  "success": true,
  "data": {
    "collection_id": "3f2a8c1e-...",
    "reference": "INV-2026-000412",
    "status": "processing",
    "message": "Payment prompt sent. Please approve on your phone."
  }
}

202 means the prompt was sent, not that payment succeeded

Never mark an order paid from this response. Wait for the collection.success webhook, or poll GET /v1/collections/{collection_id}.

Idempotency

Retry with the same Idempotency-Key and you get the original result back — no second prompt to the customer, no second payment attempt. Reusing a key with a different body is rejected rather than silently returning the wrong result.

Generate one key per payment attempt and reuse it for every retry of that attempt. Do not reuse it for a genuinely new payment.

Webhooks

Events relevant to this flow: collection.success, collection.failed, and collection.pending_review. Full details, signature verification examples and the retry schedule are on the Webhooks page.

json
{
  "event": "collection.failed",
  "merchant_code": "27048391",
  "collection_id": "3f2a8c1e-...",
  "reference": "INV-2026-000412",
  "merchant_reference": "INV-2026-000412",
  "amount": "5000.00",
  "currency": "TZS",
  "status": "failed",
  "failure_reason_code": "user_cancelled",
  "failure_reason_message": "The customer cancelled the payment.",
  "failed_at": "2026-09-27T09:14:22Z",
  "timestamp": "2026-09-27T09:14:22Z"
}

Headers on every delivery

HeaderPurpose
X-Infinity-SignatureHMAC-SHA256 hex digest of the exact raw body, keyed with the webhook secret.
X-Infinity-EventThe event name, e.g. collection.success.
X-Infinity-DeliveryUnique per event. Deduplicate on this — deliveries are at-least-once.
X-Infinity-TimestampUnix seconds at send time, for rejecting very old replays. Not part of the signed material — verify the signature over the body alone.

Verifying the signature

python
import hashlib
import hmac
import json

def handle_webhook(request):
    raw = request.get_data()                      # raw BYTES, before any JSON parse
    sent = request.headers.get("X-Infinity-Signature", "")
    expected = hmac.new(SECRET.encode(), raw, hashlib.sha256).hexdigest()

    if not hmac.compare_digest(expected, sent):   # constant-time
        return "", 401

    payload = json.loads(raw)
    if payload.get("test") or payload.get("sandbox"):
        return "", 200                            # acknowledge, do not fulfil

    # Store it, return 200, then process. Do not process before responding.
    enqueue_for_processing(payload)
    return "", 200

Sign the raw bytes you received. Parsing the JSON and re-serialising it changes the bytes and the digest will not match — this is the single most common integration mistake.

Retries

Any 2xx counts as delivered. Anything else — a non-2xx, a timeout, or an unreachable host — is retried up to 5 attempts, spaced immediately, then after 1, 5, 15 and 30 minutes. After the fifth the event is marked failed and is not retried again.

The timeout is 8 seconds, so return 200 as soon as you have stored the event and do your processing afterwards — a slow handler burns a retry. If your endpoint was down long enough to exhaust them, poll Transaction Status to catch up.

Three kinds of delivery — only one to act on

SourceMarkerFulfil?
Send Test Webhook"test": trueNo
Sandbox push"sandbox": trueNo
Live pushNeither field presentYes

A test delivery deliberately looks like a real success

Its body reads event: collection.success so that your real handler is what gets exercised. A handler keying only on that field would fulfil a fake order — check test and sandbox explicitly.

Testing without spending money

A sandbox key (sk_test_…) queues the same events a live push would, so you can exercise your real handler end to end for free. Use simulate_status on the create call to choose the outcome — successful, failed, pending_clearance or reversed.

json
{
  "amount": "1000.00",
  "phone": "+255712345678",
  "reference": "TEST-001",
  "simulate_status": "failed"
}

Statuses

StatusMeaningWhat to do
processingPrompt sent, customer has not acted yet.Wait for the webhook.
successfulPaid, and the merchant wallet is credited.Fulfil the order.
failedTerminal failure.Read failure_reason_code.
pending_clearanceHeld for review.Wait. Do not fulfil.
reversedReversed after completing.Reverse your fulfilment.

Failure reason codes

failure_reason_code is a closed set, safe to switch on. failure_reason_message is a sentence you may show a user. Handle unknown codes gracefully — the set can grow.

CodeMeaning
user_cancelledThe customer cancelled the prompt.
provider_declinedDeclined by the payment provider.
reversedReversed after it had completed.
expiredExpired before the customer approved it.
provider_unavailableProvider unreachable. Retryable.
unknown_provider_errorThe cause could not be identified.
insufficient_balanceNot enough balance in the wallet.
wrong_pinWrong PIN, or authorization failed.
timeoutAuthorization timed out.

IP allowlist (optional)

Off by default — a valid key works from any address. Enabled per key in the portal, it restricts that key to specific addresses; a request from anywhere else is rejected with a generic error that reveals nothing about the allowlist, and the rejection is recorded in the audit log. If you use it, add every egress address your servers can present, and update it when your infrastructure changes.

Go-live checklist

  1. Merchant account created, approved and verified.
  2. Live API key generated. Secret stored server-side only.
  3. Webhook URL set and signing secret generated.
  4. Send Test Webhook from the portal. Confirm your verifier accepts the signature.
  5. Sandbox run with sk_test_…, including a simulate_status: "failed" case.
  6. Optional: enable the IP allowlist and confirm a request still succeeds.
  7. One live push at the smallest workable amount, to a phone you control.
  8. Confirm the whole chain: 202 → webhook delivered → collection successful → merchant wallet credited.
  9. Confirm a deliberate failure — decline the prompt — arrives as collection.failed with failure_reason_code: "user_cancelled".