Payments
Initiate mobile money payments, check status, and handle webhooks. Supports M-Pesa, Airtel Money, Mixx by Yas, and Halotel.
Payment Flow
Here's how a typical payment works end-to-end:
- Your backend (using your API key) creates a payment page for this order
- Customer visits the hosted checkout link (camelpay.in/pay/your-slug) or your own checkout UI
- Customer enters their phone number and taps pay
- Your frontend (or the customer's browser) calls POST /v1/payments/checkout — no API key needed here
- CamelPay initiates a mobile money charge; the customer gets a USSD prompt on their phone
- Customer enters their PIN to authorize the payment
- CamelPay confirms the charge and credits your wallet
- You find out either by polling GET /v1/payments/{reference}/status, polling GET /v1/transactions with your key, or via your registered webhook; the API also reconciles recent unresolved records as a provider-status safety net
Create a Payment Page With Your API Key
This is the integration point for software you build — it requires the pages:write scope and a KYC-verified account:
curl -X POST https://api.camelpay.in/v1/payment-pages \
-H "X-API-Key: cp_live_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"type": "wallet_topup",
"title": "Campus Delivery Service",
"description": "Pay for your campus delivery",
"amount": 2000,
"allow_custom_amount": false
}'Your customer-facing payment URL remains https://camelpay.in/pay/{returned-slug}. First-party dashboard creation uses a JWT via POST /v1/merchant/payment-pages, while the developer API-key creation route remains POST /v1/payment-pages.
Initiate a Payment (Checkout)
curl -X POST https://api.camelpay.in/v1/payments/checkout \
-H "Content-Type: application/json" \
-d '{
"slug": "campus-delivery-service-a1b2c",
"phone_number": "+255712345678",
"customer_name": "John Doe",
"customer_email": "john@example.com",
"idempotency_key": "order_12345_67890"
}'Request Fields
| Field | Type | Required | Description |
|---|---|---|---|
slug | string | Yes | Payment page slug |
phone_number | string | Yes | Customer's mobile money number (E.164 format) |
customer_name | string | Yes | Customer's name |
customer_email | string | Yes | Customer's email |
idempotency_key | string | Yes | Unique identifier for this transaction |
amount | integer | Conditional | Required if the page allows custom amounts |
Check Payment Status
Also public — anyone with the reference can poll this, same as the checkout call itself:
curl -X GET https://api.camelpay.in/v1/payments/{reference}/statusAs the page owner, you can instead list everything that's landed in your wallet with your API key (transactions:read scope) — useful for reconciliation without tracking individual references:
curl -X GET https://api.camelpay.in/v1/transactions \
-H "X-API-Key: cp_live_your_api_key_here"Status Values
| Status | Description |
|---|---|
pending | Awaiting customer action |
processing | Payment being processed |
completed | Payment successful, wallet credited |
failed | Payment failed or declined |
voided | Customer cancelled before completion |
expired | Payment session timed out unattended (1 hour default) |
Polling Strategy
For real-time UI updates, poll the public status endpoint:
async function pollPaymentStatus(reference) {
const maxAttempts = 60; // 3 minutes with 3-second intervals
const interval = 3000;
for (let i = 0; i < maxAttempts; i++) {
const response = await fetch(
`https://api.camelpay.in/v1/payments/${reference}/status`
);
const { data } = await response.json();
if (data.status === "completed") return { success: true, data };
if (["failed", "voided", "expired"].includes(data.status)) {
return { success: false, error: data.status };
}
await new Promise(resolve => setTimeout(resolve, interval));
}
return { success: false, error: "timeout" };
}Currency and Amounts
- Currency: TZS (Tanzanian Shilling) only
- Amount format: Integer (no decimals)
- Minimum amount: 500 TZS
- Example: 5000 TZS =
5000(not5000.00)
