Powering SA prepaid since 2003 / No monthly fees, ever
SIMcloud bulk airtime and data recharge platform

SIMcloud API

How to Integrate the SIMcloud API with PHP and cURL

A server-side PHP foundation with explicit HTTP handling, safe token storage, persisted order identifiers and production error controls.

Author: SIMcloud Published 15 July 2026
Secure PHP API gateway connecting several prepaid service routes

The SIMcloud API lets a PHP application send SMS messages, recharge airtime and data, buy digital vouchers and prepaid electricity, check wallet balance and look up the current network for a South African mobile number.

This tutorial builds a small procedural PHP client with cURL, explicit HTTP handling and no framework dependency. It is suitable as a starting point for an existing PHP/MySQL application.

Before you start

  • A funded SIMcloud account with API access.
  • An API token stored outside the public web root.
  • PHP with the cURL extension enabled.
  • A local database table for your own request and SIMcloud identifiers.
  • Application logging that never records bearer tokens or completed voucher codes.

All documented requests use:

Authorization: Bearer YOUR_API_TOKEN

Store the token in an environment variable or private configuration file. Never place it in browser JavaScript, a repository, a URL query string or an error response.

Create a small PHP cURL client

<?php

function simcloudRequest(string $method, string $path, ?array $payload = null): array
{
    $token = getenv('SIMCLOUD_API_TOKEN');
    if (!is_string($token) || $token === '') {
        throw new RuntimeException('SIMCLOUD_API_TOKEN is not configured');
    }

    $url = 'https://simcloud.co.za' . $path;
    $ch = curl_init($url);
    if ($ch === false) {
        throw new RuntimeException('Could not initialise cURL');
    }

    $headers = [
        'Authorization: Bearer ' . $token,
        'Accept: application/json',
    ];

    $options = [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_CUSTOMREQUEST => $method,
        CURLOPT_HTTPHEADER => $headers,
        CURLOPT_CONNECTTIMEOUT => 10,
        CURLOPT_TIMEOUT => 30,
    ];

    if ($payload !== null) {
        $body = json_encode($payload, JSON_THROW_ON_ERROR);
        $headers[] = 'Content-Type: application/json';
        $options[CURLOPT_HTTPHEADER] = $headers;
        $options[CURLOPT_POSTFIELDS] = $body;
    }

    curl_setopt_array($ch, $options);
    $responseBody = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    $error = curl_error($ch);
    curl_close($ch);

    if ($responseBody === false) {
        throw new RuntimeException('SIMcloud transport error: ' . $error);
    }

    $decoded = json_decode($responseBody, true, 512, JSON_THROW_ON_ERROR);
    return ['http_status' => $status, 'body' => $decoded];
}

This function returns the HTTP status separately from the JSON body. Your application needs both. A JSON message that says “error” and an HTTP 409 duplicate response require different action from a transport timeout.

Test authentication with the Balance API

try {
    $result = simcloudRequest('GET', '/api/balance.php');

    if ($result['http_status'] !== 200) {
        throw new RuntimeException('Balance request failed');
    }

    $balance = (float) ($result['body']['balance'] ?? 0);
    echo 'Current last known balance: R' . number_format($balance, 2);
} catch (Throwable $e) {
    error_log('SIMcloud balance check failed: ' . $e->getMessage());
}

The balance endpoint returns the account’s stored last known balance. Treat it as a pre-check, not a reservation. Another order can spend wallet value before your next request is processed.

Call the different SIMcloud services

Send one SMS

$sms = simcloudRequest('POST', '/api/sms.php', [
    'recipient' => '+27821234567',
    'message' => 'Your service request has been received.',
]);

if ($sms['http_status'] === 200) {
    $smsId = (int) $sms['body']['sms_id'];
    // Persist $smsId before checking delivery status.
}

Queue airtime

$airtime = simcloudRequest('POST', '/api/airtime.php', [
    'msisdn' => '0821234567',
    'network' => 'mtn',
    'amount' => 10.00,
    'reference' => 'LOCAL-ORDER-10045',
]);

if ($airtime['http_status'] === 201) {
    $requestId = (int) $airtime['body']['request_id'];
    // Persist $requestId and queue_status.
}

Fetch the live data-bundle catalogue

$catalogue = simcloudRequest('GET', '/api/data.php?products=1&network=mtn');

Use the returned network and sell value when creating a data order. Do not maintain a permanent copy of bundle prices without a refresh process.

Fetch the live voucher catalogue

$vouchers = simcloudRequest('GET', '/api/vas.php');

Resolve the requested brand from the response and validate the amount against available_amounts before posting the order.

Persist identifiers before polling

SIMcloud recharge, voucher and electricity workflows can be asynchronous. The POST response is the handoff point between your local transaction and the remote order.

  1. Create a local pending order with a unique local reference.
  2. Send the SIMcloud request.
  3. Store the HTTP status, SIMcloud request/order ID, transaction ID or order reference returned.
  4. Commit the local record before scheduling a poll.
  5. Poll with the documented identifier.
  6. Update the existing record to a final status.
  7. Do not create a new purchase merely because a poll is pending or a transport request timed out.

Handle errors explicitly

HTTP resultMeaningApplication action
400Validation, product, denomination or balance problem.Do not retry unchanged. Show or log the safe validation reason.
401Missing or invalid bearer token.Stop processing and alert operations. Do not ask an end user to change purchase data.
404Requested order or number result not found.Check ownership and identifier; do not create a replacement automatically.
409Airtime or data duplicate inside the five-minute same-number-and-amount window.Load the original local request and reconcile it.
5xxSIMcloud or downstream service problem.Use a bounded retry for safe reads. For purchases, investigate the existing reference before resubmitting.
Transport timeoutYour client did not receive a response.Outcome may be unknown. Search or poll using your persisted reference rather than assuming failure.

Never log the Authorization header. Also exclude electricity PINs and completed digital-voucher codes from general logs. Log endpoint, local ID, SIMcloud ID, HTTP status and a safe failure reason.

Production checklist

  • Token is private and rotated through configuration.
  • Inputs are validated before the API call.
  • Local references are unique and traceable.
  • Returned identifiers are persisted before polling.
  • HTTP 401 and 409 have dedicated handling.
  • Retries are bounded and do not duplicate purchases.
  • Logs exclude tokens, PINs and voucher codes.
  • Final status is reconciled to billing and delivery.

Frequently asked questions

Does the SIMcloud API use OAuth?

The current documented API uses a bearer API token in the Authorization header.

Can I call the API from browser JavaScript?

Do not expose the bearer token in a browser. Call SIMcloud from your server.

Why must I store the request ID?

Queued services return identifiers used to query the original order and prevent duplicate resubmission.

Can I retry a 409 response?

Not unchanged. It signals a recent same-number-and-amount airtime or data request. Reconcile the original order.

Where is the full contract?

Use the SIMcloud API documentation and machine-readable OpenAPI document.

Put this guide into practice

Build against the documented SIMcloud API contract

Use server-side bearer authentication, persist returned identifiers and handle each HTTP outcome explicitly.