Core

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:

  1. Your backend (using your API key) creates a payment page for this order
  2. Customer visits the hosted checkout link (camelpay.in/pay/your-slug) or your own checkout UI
  3. Customer enters their phone number and taps pay
  4. Your frontend (or the customer's browser) calls POST /v1/payments/checkout — no API key needed here
  5. CamelPay initiates a mobile money charge; the customer gets a USSD prompt on their phone
  6. Customer enters their PIN to authorize the payment
  7. CamelPay confirms the charge and credits your wallet
  8. 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:

bash
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)

Checkout is public — no X-API-Key or JWT. It's scoped entirely to the page's slug, the same way a hosted checkout link doesn't need your credentials to be completed by a buyer.
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

FieldTypeRequiredDescription
slugstringYesPayment page slug
phone_numberstringYesCustomer's mobile money number (E.164 format)
customer_namestringYesCustomer's name
customer_emailstringYesCustomer's email
idempotency_keystringYesUnique identifier for this transaction
amountintegerConditionalRequired if the page allows custom amounts

Check Payment Status

Also public — anyone with the reference can poll this, same as the checkout call itself:

bash
curl -X GET https://api.camelpay.in/v1/payments/{reference}/status

As 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:

bash
curl -X GET https://api.camelpay.in/v1/transactions \
  -H "X-API-Key: cp_live_your_api_key_here"

Status Values

StatusDescription
pendingAwaiting customer action
processingPayment being processed
completedPayment successful, wallet credited
failedPayment failed or declined
voidedCustomer cancelled before completion
expiredPayment 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" };
}
Webhooks are the preferred notification path. CamelPay also runs a bounded reconciliation loop every five minutes in production for recent pending or processing records. Treat GET /v1/payments/{reference}/status and your wallet transaction list as recovery views, not as a reason to create a second payment.

Currency and Amounts

  • Currency: TZS (Tanzanian Shilling) only
  • Amount format: Integer (no decimals)
  • Minimum amount: 500 TZS
  • Example: 5000 TZS = 5000 (not 5000.00)