# Ultraner API Documentation

> One API for accepting and moving money across Africa and the world: mobile money, cards (Stripe), PayPal, bank rails, cross-border transfers, recurring subscriptions, escrow, and webhooks. This file is a complete, plain-text mirror of https://ultraner.com/docs, written for humans, AI coding assistants, and LLM agents to read directly.

Base URL: `https://api.ultraner.com`

Related machine-readable resources:
- OpenAPI spec: https://ultraner.com/openapi.json
- Site-wide LLM index: https://ultraner.com/llms.txt
- Live exchange rates (JSON + page): https://ultraner.com/fx
- Adaptive Pricing explainer: https://ultraner.com/help/adaptive-pricing
- Help center: https://ultraner.com/help

---

## Authentication

Pass your API key on every request:

```
curl https://api.ultraner.com/v1/wallet \
  -H "X-API-Key: uk_live_your_key_here"
```

Keys are environment-prefixed:
- `uk_live_...` — production, charges real money.
- `uk_test_...` — sandbox, simulated transactions, no real gateway is ever called.

Every rail has a sandbox counterpart: add `-sandbox` to the version segment of the URL (`/v1-sandbox/...`, `/v2-sandbox/...`, `/v0-sandbox/...`) and use a `uk_test_` key. The URL and key must agree, a live key on a sandbox URL (or vice versa) is rejected with 401. Going live later is just deleting `-sandbox` from the URL and swapping the key.

All errors share one shape:

```json
{ "success": false, "error": { "code": "VALIDATION_ERROR", "message": "account_number is required" } }
```

---

## How currency conversion works

Ultraner moves money across three different currency situations:

1. **Mobile money, same currency.** A Tanzanian phone pays into a TZS wallet, a Rwandan phone pays into an RWF wallet. No conversion, payer and recipient are already in the same currency.
2. **Card & PayPal — Adaptive Pricing.** A payer anywhere in the world pays in their own currency (or USD, PayPal's only settlement currency); the African recipient is always credited the exact local amount their link/charge was for. Full explanation: https://ultraner.com/help/adaptive-pricing
3. **Cross-border (IMT).** A sender outside Tanzania, in USD, KES, UGX, GBP, EUR and more, transfers directly into a TZS wallet at a live rate.

Every rate Ultraner uses is public and live at **https://ultraner.com/fx**, grouped by region with buy/sell prices for every market. Every converted transaction has its own page at **`https://ultraner.com/fx/{transactionId}`** showing exactly what the payer paid, the rate applied, the gateway fee, and the net credited — use the `transactionId` you already have from any charge response or `payment.success` webhook.

---

## Universal Checkout — one endpoint for all three payment methods

For businesses with a wallet and API key, `POST /v0/payments/charge` is the single endpoint for mobile money, card, and PayPal. Ultraner picks the gateway internally based on `method`.

```
POST /v0/payments/charge
X-API-Key: uk_live_...

// mobile money — USSD push, no redirect
{ "method": "mobile_money", "amount": 50000, "phone": "255712345678", "provider": "Airtel" }

// card (Stripe) — needs return_url/cancel_url
{ "method": "card", "amount": 50000, "currency": "TZS", "description": "Order #42",
  "return_url": "https://your-app.com/success", "cancel_url": "https://your-app.com/cancel" }

// paypal — same shape
{ "method": "paypal", "amount": 50000, "currency": "TZS", "description": "Order #42",
  "return_url": "https://your-app.com/success", "cancel_url": "https://your-app.com/cancel" }
```

Response (note: request body is snake_case, response is camelCase — that's the real shape, not a typo):

```json
{
  "success": true,
  "data": {
    "transactionId": "txn_...",
    "method": "card",
    "gateway": "stripe",
    "status": "pending",
    "redirectUrl": "https://checkout.stripe.com/pay/cs_..."
  }
}
```

`mobile_money` sends a USSD push and never returns a redirect (the customer approves on their phone). `card`/`paypal` return `redirectUrl` — send the payer there; the gateway's own hosted page collects payment details, Ultraner never sees a card number or PayPal login.

Sandbox: `POST /v0-sandbox/payments/charge` with a `uk_test_` key exercises every method. `card`/`paypal` redirect to an Ultraner-hosted confirm/fail page instead of a real gateway (no card number is ever needed to test).

### Payment links (a different pattern)

Payment links (`pl_...` tokens) are the opposite flow: the payer lands on an **Ultraner-hosted checkout page** (`/pay/{token}`) and picks their own method. Use `POST /stripe/sessions` and `POST /paypal/orders` with the same `token`, or `POST /v0/pay/checkout` for mobile money. This is the right choice when you want Ultraner's own hosted UI rather than redirecting straight to the gateway from your own server-driven flow.

---

## Real-life examples (full walkthroughs at https://ultraner.com/docs#ex-streaming, #ex-game, #ex-saas)

- **Streaming/subscription app (e.g. Bingehub):** subscriber picks a plan + payment method → `POST /v1/recurring/plans` with a matching `channel` → webhook (`recurring.activated`, `recurring.success`) unlocks/renews access.
- **Game app (e.g. Vuma Legends):** one-off in-app purchase via `/v0/payments/charge` → never grant the reward until `payment.success` or a status poll confirms it → credit idempotently by `transactionId`.
- **SaaS platform (e.g. Taskflo):** "Upgrade to Pro" click → `POST /v1/recurring/plans` (channel: card) → webhook provisions the plan; renewals are automatic, no re-prompting.

---

## Recurring & Subscriptions

`POST /v1/recurring/plans` creates a recurring plan on any of the three payment methods via `channel` (`mobile_money` default, `card`, `paypal`). Card and PayPal are real gateway subscriptions — Stripe Billing / PayPal Subscriptions own the billing schedule and auto-charge every interval. Mobile money has no stored-credential concept on any African network, so Ultraner's own engine sends a fresh USSD push every interval instead (the payer still approves each one).

```
POST /v1/recurring/plans
X-API-Key: uk_live_...

// mobile money
{ "name": "Premium plan", "mno_provider": "Vodacom", "account_number": "255712345678",
  "amount": 15000, "currency": "TZS", "interval_type": "monthly" }

// card — Stripe owns the schedule from here
{ "name": "Premium plan", "channel": "card", "amount": 15000, "currency": "TZS", "interval_type": "monthly",
  "return_url": "https://you.app/billing/return", "cancel_url": "https://you.app/billing/cancel",
  "payer_email": "subscriber@example.com" }
```

Response includes `redirect_url` (card/paypal, send the payer there once) and `manage_url` — every plan, on every channel, gets a **public, no-account** page (`https://ultraner.com/manage/{token}`) where the payer can view or cancel their own subscription anytime, mirroring Stripe's own customer portal.

Confirm after approval (idempotent alongside the webhook): `POST /v0/subscriptions/confirm/stripe/{sessionId}` or `POST /v0/subscriptions/confirm/paypal/{subscriptionId}`.

Webhook events: `recurring.activated`, `recurring.success`, `recurring.past_due`, `recurring.failed`, `recurring.cancelled`.

---

## Mobile Money (AzamPay — Tanzania & Rwanda)

```
POST /v1/payments/express/mno
{ "account_number": "255712345678", "amount": 50000, "provider": "Airtel", "currency": "TZS" }
```

Sends a USSD push; the customer approves on their phone. Same endpoint auto-routes Rwanda (`250...` prefix, providers `MTN`/`Airtel`) vs Tanzania (`255...` prefix, providers `Vodacom`/`Airtel`/`Tigo`/`Halopesa`/`Azampesa`/`Mpesa`) from the phone prefix, your integration code never changes.

Bank checkout (CRDB/NMB, via a customer-generated OTP): `POST /v1/payments/express/bank`.
Disbursements (send money out): `POST /v1/disbursements`, with a `POST /v1/disbursements/lookup` name-check first.
Cross-border transfers into TZS: `POST /v1/crossborder`, rates at `GET /v1/crossborder/rates`.

## Selcom (Tanzania) — MNO, bank & card

`POST /v2/payments/express/mno` — mobile money collection via Selcom (Vodacom, Airtel, Tigo, Halotel). Also disbursements (`POST /v2/disbursements`) and bill payments (`POST /v2/bills/lookup`, `POST /v2/bills/pay`).

---

## PayPal

Ultraner holds a US PayPal merchant account and **always settles PayPal in USD** — unlike Stripe, PayPal has no way to present a charge in the payer's own currency. Point any payment link at it regardless of currency: Ultraner converts local→USD at a live rate and bills the payer that USD figure, then credits the recipient the exact local amount the link is for.

```
POST /paypal/orders
{ "token": "pl_live_xxxx", "amount": 5000, "currency": "USD", "email": "payer@acme.com" }
```

`currency: "USD"` (optional) treats `amount` as already USD, charged directly with no conversion; omit it and `amount` is read in the link's own currency and converted. Capture with `POST /paypal/orders/{orderId}/capture`. Minimum is $1.00 USD.

## Stripe

Anyone with a card (Visa, Mastercard, Amex) pays via Stripe-hosted checkout. This is Ultraner's Adaptive Pricing (https://ultraner.com/help/adaptive-pricing): for most currencies Stripe presents and charges the card **directly in the payer's own currency** (`charged_in_local: true`), no conversion at all. Where Stripe can't present a currency, Ultraner falls back to USD at a live rate. Either way the recipient is credited the exact local amount.

```
POST /stripe/sessions
{ "token": "pl_live_xxxx", "amount": 50000, "email": "payer@acme.com" }
```

Confirm: `POST /stripe/sessions/{sessionId}/confirm`.

---

## Wallet & Transfers

```
GET /v1/wallet                      // balance
POST /v1/transfer                   // wallet -> wallet, instant, real-time WebSocket event
GET /v1/transactions?page=1&limit=20
```

## Escrow

```
POST /v1/escrow                              // { seller_phone, amount, condition }
POST /v1/escrow/{escrow_code}/release        // buyer releases to seller
POST /v1/escrow/{escrow_code}/dispute        // buyer disputes
```

---

## Webhooks

Created from your Developer Console (**Console → Webhooks**), no API call needed — enter your HTTPS URL, click the events you want (grouped by category), save, and copy the signing secret immediately (shown once). Edit the URL/events or pause/delete anytime from the same page.

Every event is HMAC-SHA256 signed:

```js
const crypto = require('crypto');
app.post('/webhooks/ultraner', (req, res) => {
  const sig = req.headers['x-ultraner-signature'];
  const body = JSON.stringify(req.body);
  const expected = 'sha256=' + crypto.createHmac('sha256', process.env.WH_SECRET).update(body).digest('hex');
  if (sig !== expected) return res.status(401).end();
  const { event, data } = req.body;
  // handle event...
  res.status(200).end();
});
```

Real events (only these are ever fired, kept in sync with the codebase):
`payment.success`, `payment.failed`, `recurring.activated`, `recurring.success`, `recurring.past_due`, `recurring.failed`, `recurring.cancelled`, `crossborder.success`, `crossborder.failed`, `disbursement.success`, `disbursement.failed`, `billpay.received`.

---

## Sandbox testing

Every rail has a `-sandbox` counterpart with a `uk_test_` key and no real gateway is ever touched. Mobile money/bank outcomes are keyed off the phone number you send:

| Value | Outcome |
|---|---|
| ends in `700000001` | Instant success |
| ends in `700000002` | Insufficient funds (fails) |
| ends in `700000003` | Times out, stays pending |
| amount 1–100 (smallest unit) | Simulated provider outage (5xx) |
| any other number | Succeeds after a random 3–8s delay |

Card/PayPal sandbox redirects to an Ultraner-hosted confirm/fail page (`/sandbox-simulate/stripe/{sessionId}`, `/sandbox-simulate/paypal/{orderId}`) instead of a real checkout, no card number needed.

---

## For AI agents & coding assistants

This file, plus https://ultraner.com/llms.txt and https://ultraner.com/openapi.json, are written specifically so an AI agent can integrate Ultraner without a human reading every page first. If you are an LLM or coding agent building an integration: the Universal Checkout endpoint (`/v0/payments/charge`) is almost always the right starting point for a new integration since it works identically across mobile money, card, and PayPal.
