G-SMS.

G-SMS · Use case

Transactional SMS notifications in Cameroon

Confirming an order, acknowledging a Mobile Money payment, announcing a delivery: the messages customers are waiting for. Here is how to send them, what they cost per order, and why a transactional SMS is not a marketing SMS.

The problem: the customer pays, then hears nothing

A Mobile Money payment clears in seconds, but nothing on the customer’s side confirms it apart from the operator’s own SMS, which says nothing about what was bought. The confirmation email lands in an inbox checked once a week, when it is checked at all. A WhatsApp message assumes an active data connection, which is far from guaranteed outside the main cities.

An SMS reaches every handset, including models with no touchscreen, no data plan and no app installed. It is the only channel you can assume works for all of your customers.

What silence costs

The customer who gets no confirmation calls. On 300 orders a month, if a third of buyers make a three-minute call to ask where their order stands, that is five hours on the phone every month for your team — plus all the calls that never get through because the line is busy.

Then comes the classic dispute: “I paid and received nothing.” Without a record sent to the customer at the moment of payment, that conversation ends in your disfavour, sometimes with a refund that was never owed.

The solution in three steps

1. Pick the events that deserve a message

One SMS per state change that alters something for the customer, and not one more. For an online store: order confirmed, payment received, parcel handed to the courier. Everything else belongs in your interface, not on the buyer’s phone.

Two families of messages follow different rules and have their own page: verification codes (OTP), which need a lifetime and a resend cap, and appointment reminders, where the sending time is what matters.

2. Send, with a reference derived from the event

<?php

$payload = [
    'to'              => '+237690000000',
    'text'            => 'Order CM-10432 confirmed. Amount: XAF 24,500. '
                       . 'Delivery on 14/09. Akwa Store.',
    // Derived from the business event: replayable, with no risk of a double send.
    'clientReference' => 'order-10432-confirmation',
];

$ch = curl_init('https://api.sms.gworldship.com/v1/sms');
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 10,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer gsk_votre_cle_api',
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode($payload, JSON_UNESCAPED_UNICODE),
]);

$body   = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

$sms = json_decode($body, true);

if ($status === 202) {
    recordSend($sms['messageId'], $sms['priceXaf'], $sms['balanceAfterXaf']);
} elseif ($status === 409) {
    // Reference already used: $sms IS the message sent the first time, not an
    // error object. Nothing was sent, and nothing was charged, a second time.
    recordSend($sms['messageId'], 0, null);
} else {
    error_log('G-SMS ' . $status . ' ' . ($sms['code'] ?? 'INTERNAL_ERROR'));
}

The 202 response carries messageId, segments, priceXaf and balanceAfterXaf. Null fields are absent from the response: test for their presence rather than their value.

3. Track delivery through the webhook

<?php
// POST https://your-store.cm/webhooks/gsms

$raw       = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_GSMS_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_GSMS_SIGNATURE'] ?? '';

// Tolerance window: 300 seconds either side.
if (abs(time() - (int) $timestamp) > 300) { http_response_code(400); exit; }

$expected = 'sha256=' . hash_hmac('sha256', $raw . '.' . $timestamp, $secret);

// hash_equals: constant-time comparison, mandatory here.
if (!hash_equals($expected, $signature)) { http_response_code(401); exit; }

$event = json_decode($raw, true);

// At-least-once delivery: the same eventId can come back. Deduplicate.
if (alreadyHandled($event['eventId'])) { http_response_code(200); exit; }
markHandled($event['eventId']);

updateNotificationStatus(
    $event['data']['clientReference'],   // order-10432-confirmation
    $event['data']['status'],            // SENT, DELIVERED or FAILED
    $event['data']['failureReason'] ?? null
);

http_response_code(200);   // any 2xx counts as an acknowledgement

Body received on each status change:

POST /webhooks/gsms
X-GSms-Event: sms.sent
X-GSms-Timestamp: 1789203247
X-GSms-Signature: sha256=9f1c…
X-GSms-Delivery: 2c81a0f4-1d33-4f7e-a0d6-6b5d9a2e7c14

{
  "eventId": "0f3a6b18-72c4-4d0e-8f2b-1a9c4e5d7b60",
  "type": "sms.sent",
  "occurredAt": "2026-09-12T09:16:31Z",
  "applicationId": "akwa-store",
  "data": {
    "messageId": "b7d1f0c2-5a3e-4c19-9f0a-2d6e8c41b3aa",
    "clientReference": "order-10432-confirmation",
    "to": "+237690000000",
    "operator": "ORANGE",
    "segments": 1,
    "priceXaf": 5,
    "status": "DELIVERED",
    "failureReason": null
  }
}
Despite its name, applicationId carries the application slug, not its technical identifier. And retries run at 1 minute, 5 minutes, 30 minutes, 2 hours, 12 hours, then every 12 hours until the event is abandoned after 48 hours: an endpoint that answers slowly will be called again. Statuses and webhooks in detail →

The real cost, in CFA francs

Assumption: an online store, 300 orders a month, three messages per order. Weighted average price of XAF 5.30, from 85% of sends to Orange and MTN at XAF 5 and 15% to Camtel and Nexttel at XAF 7.

MessageMonthly volumeMonthly cost
Order confirmed300XAF 1,590
Payment received300XAF 1,590
Parcel handed to the courier300XAF 1,590
Total900 SMSXAF 4,770

That is XAF 15.90 per order kept informed end to end — against the five hours of phone calls those messages replace.

Writing a message that fits in one segment

Billing is per segment: 160 characters in the standard alphabet (GSM-7), 153 per segment beyond that. A single character outside that alphabet switches the whole message to UCS-2, capped at 70 characters — and the price doubles from the 71st.

TextLengthEncodingSegments
Your order CM-10432 is confirmed. We’ll deliver it on 14/09. Akwa Store.72UCS-22
Your order CM-10432 is confirmed. We'll deliver it on 14/09. Akwa Store.72GSM-71

The two texts differ by one invisible character: the first uses the curly apostrophe ’ (U+2019), which word processors insert automatically and which is not part of GSM-7; the second uses the straight apostrophe ' (U+0027), which is. Same sentence, twice the price. Test a message in the segment calculator →

Those four segment sizes are service settings rather than frozen constants: the API always returns the number of segments actually billed.

Good practice

  • Name yourself in the text. The sender shown is a phone number: without your name the message is unreadable.
  • Use a client reference derived from the business event, such as order-10432-confirmation, never a random identifier: that is what makes a replay harmless.
  • Never send from inside the checkout flow. Queue the send in your application; a payment confirmation page must not wait on an external HTTP call.
  • Respect the hours — nothing between 9 pm and 7 am, unless the event was triggered by the customer: a payment made at 11 pm deserves its immediate confirmation.
  • Keep the messageId next to the order: that is what lets you answer “I received nothing” with a fact rather than a guess.
  • Watch balanceAfterXaf on every send and alert well before zero: an empty balance answers 402 and the notification is simply lost.
  • A timeout is not a failure. Retry with the same reference: if the first call did go through, the API answers 409 with the original message instead of sending a second one.

What G-SMS does not do

  • No marketing campaigns. There is no custom sender name: a promotion sent from an unknown number reads as spam, and you have no way to identify yourself at a glance. The product is built for the expected message.
  • No inbound SMS. The customer can neither reply nor text “STOP”. This is a settled design decision — and one more reason to stick to notifications tied to a customer action.
  • No batch sending. One HTTP call per recipient, within a limit of 10 requests per second per key. Beyond that the API answers 429 with a Retry-After header.
  • No server-side templates. The text is composed by your application: it is the one that knows the customer’s language and the contents of their order.
📵 The sender seen by the recipient is the number of the sending SIM, not a brand name: G-SMS is designed for transactional SMS (OTP codes, notifications), not for marketing campaigns.

Frequently asked questions

How much does a notification SMS cost in Cameroon?

XAF 5 per segment to Orange and MTN, XAF 7 to Camtel and Nexttel. A short order confirmation fits in one segment, so budget 5 to 7 francs per message, with no subscription.

Can I send promotional offers with G-SMS?

No. The recipient sees the number of the sending SIM rather than a brand name, so a promotion arriving from an unknown number reads as spam. G-SMS is built for messages the customer is expecting.

How do I know the customer received the message?

Every status change is pushed to your server through a signed webhook, and the status stays available on GET /v1/sms/{messageId}. DELIVERED means the operator confirmed handset delivery.

Ready to send your first SMS?

Create an account, get 100 FCFA of credit and an API key straight away.

Create an account