G-SMS.

Documentation

Send an SMS

One request, one message. The API validates the number, picks the operator from its prefix, counts the segments, charges your balance and queues the message — all inside a single transaction. Everything below is what the server actually does, in the order it does it.

Endpoint

POST https://api.sms.gworldship.com/v1/sms

Authenticated with Authorization: Bearer gsk_…. The account and the application are resolved from the key, never from the request body.

Parameters

FieldTypeRequiredDescription
tostringRequired Cameroonian mobile number. Accepted as +2376XXXXXXXX, 2376XXXXXXXX or 6XXXXXXXX; spaces are ignored. The response always returns it normalised to E.164.
textstringRequired Message body, 1 to 1600 characters. An empty or blank text is rejected with TEXT_EMPTY.
clientReferencestringOptional Your own identifier, up to 64 characters, unique inside the application. Sending it twice returns the first message instead of sending a second one. See idempotency.

Request

curl -X POST 'https://api.sms.gworldship.com/v1/sms' \
  -H 'Authorization: Bearer gsk_your_api_key' \
  -H 'Content-Type: application/json' \
  -d '{
        "to": "+237690000000",
        "text": "Your verification code is 123456",
        "clientReference": "otp-42-1757664063"
      }'

Response

HTTP/1.1 202 Accepted

{
  "messageId": "9f1c4b2e-5d3a-4c7e-8b21-6a0f7d9c1e45",
  "to": "+237690000000",
  "operator": "ORANGE",
  "segments": 1,
  "priceXaf": 5,
  "status": "QUEUED",
  "clientReference": "otp-42-1757664063",
  "balanceAfterXaf": 95,
  "createdAt": "2026-09-12T08:41:03.512Z"
}
FieldDescription
messageIdUUID of the message. Keep it: it is the key for status lookups and webhooks.
toThe number normalised to E.164, whatever format you sent.
operatorORANGE, MTN, CAMTEL, NEXTTEL or DEFAULT, resolved from the number prefix.
segmentsNumber of segments billed for this text. See the section below.
priceXafTotal charged, in XAF: operator price per segment × segments.
status Always QUEUED on a 202. Delivery comes later — statuses and webhooks.
balanceAfterXaf Your balance after the charge. Present only on a 202, because only a 202 moved money.
clientReferenceEchoed back when you sent one; absent otherwise.
createdAtAcceptance time, ISO-8601 UTC.

Null fields are omitted from the JSON. On a 202 you will not see failureReason, sentAt or deliveredAt: they appear on later reads, once they have a value.

Segments and price

SMS billing counts segments, not messages. Which alphabet your text uses decides how many characters fit in one segment.

EncodingSingle segmentPer segment when split
GSM-7 (plain text)160 characters153 characters
UCS-2 (rare accents, emoji)70 characters67 characters
  • A single character outside GSM-7 switches the whole message to UCS-2. One emoji in a 150-character text turns one segment into three. Write plain text in anything you send at volume.
  • These four sizes are settings, not constants: an administrator can change them. Treat them as the current values and rely on the segments field of the response for what you were actually charged, or on the segment calculator before sending.
  • Price per segment: XAF 5 to Orange and MTN, XAF 7 to Camtel and Nexttel, XAF 10 on the default route. Full pricing.

How the operator is chosen

The operator comes from the number prefix, following the ART numbering plan. A prefix that belongs to no operator in the plan is not rejected: the message goes out through the fallback route, recorded as DEFAULT and billed XAF 10 per segment. You can check any number with the carrier lookup tool before you send.

The order of checks

This order matters more than it looks: it tells you exactly when your balance is touched and when it is not. Nothing is charged before the last step.

#CheckStatusCode
1Key is valid and not revoked401INVALID_API_KEY
2Account is active403ACCOUNT_SUSPENDED
3Client reference not already used409the existing message, not an error object
4Text and number are valid, a price exists400 TEXT_EMPTY · TEXT_TOO_LONG · INVALID_NUMBER · PRICE_NOT_CONFIGURED
5Target operator is enabled503OPERATOR_DISABLED
6Daily quota not reached429QUOTA_EXCEEDED
7Balance is sufficient — charge and queue402INSUFFICIENT_BALANCE
  • OPERATOR_DISABLED is returned before any charge, with a Retry-After: 3600 header. A disabled network is never silently rerouted through another one.
  • The daily quota is disabled by default, so most accounts never see step 6. Quotas and limits.
  • Requests are also rate limited to ten per second per key, before any of this: 429 RATE_LIMITED with Retry-After: 1.

Node.js

// Node.js 18+ — no dependency, fetch is built in.
const res = await fetch('https://api.sms.gworldship.com/v1/sms', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.GSMS_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    to: '+237690000000',
    text: `Your verification code is ${code}`,
    clientReference: `otp-${userId}-${Date.now()}`,
  }),
});

const body = await res.json();

if (res.status === 202) {
  console.log('queued', body.messageId, 'balance', body.balanceAfterXaf);
} else if (res.status === 409) {
  // Duplicate client reference: body is the EXISTING message, not an error.
  console.log('already sent', body.messageId);
} else {
  throw new Error(`${res.status} ${body.code}`);
}

PHP

<?php
// PHP 8, cURL from the standard library.
$payload = json_encode([
    'to' => '+237690000000',
    'text' => "Your verification code is {$code}",
    'clientReference' => "otp-{$userId}-" . time(),
]);

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

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

$body = json_decode($response, true);

if ($status === 202 || $status === 409) {
    // 409 = same clientReference already used: the existing message is returned.
    $messageId = $body['messageId'];
} else {
    throw new RuntimeException($body['code'] ?? 'HTTP ' . $status);
}

Both examples treat 409 as a success: the message exists, it was charged once, and its identifier is in the body. That single branch is what makes a retry safe. Why 409 is not an error here.

Reading a message back

GET https://api.sms.gworldship.com/v1/sms/9f1c4b2e-5d3a-4c7e-8b21-6a0f7d9c1e45

Returns the same object, with the current status. A message that belongs to another application returns 404, never 403: the API does not confirm the existence of identifiers you are not allowed to see.

Ready to send your first SMS?

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

Create account