The most common failure in SMS integrations is not a failed request — it is a request that looks successful while no message is sent. You get HTTP 200, your code is happy, the phone is quiet. Nobody notices until someone reads the code in the response body.
The NetGSM REST endpoint returns HTTP 200 to say it received your request; whether the message was actually accepted is a code inside the response body. 00 is success, 30 means bad credentials or an IP restriction, 40 an unapproved sender name, 20 a length or character problem. An integration that checks only the HTTP status makes every one of these invisible. On top of that the number must be in 5XXXXXXXXX form (no leading zero, no country code), msgheader must be an approved sender, and commercial messages are subject to Turkey's consent registry.
The REST endpoint takes a JSON body with basic authentication. The smallest working example:
<?php
$payload = [
'msgheader' => 'APPROVED_SENDER',
'encoding' => 'TR',
'messages' => [
['msg' => 'Your registration has been received.', 'no' => '5301234567'],
],
];
$ch = curl_init('https://api.netgsm.com.tr/sms/rest/v2/send');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_USERPWD => NETGSM_USER . ':' . NETGSM_PASS,
CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE),
CURLOPT_TIMEOUT => 15,
]);
$body = curl_exec($ch);
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
The JSON_UNESCAPED_UNICODE flag matters: sending Turkish characters as \u011f-style escapes causes trouble in some setups.
The critical point: $http is almost always 200. The real outcome is in the body.
00 | Success — the message was queued |
20 | Message text invalid or the character limit was exceeded |
30 | Wrong credentials or API access restricted by IP |
40 | The sender header is not approved |
50 | Blocked by the consent registry — recipient refuses commercial messages |
70 | Invalid parameter; the body shape differs from what is expected |
Code 30 wastes the most time, because it appears even when the credentials are correct. A NetGSM account can restrict API access to specific IP addresses; code that works from your development machine stops the moment it is deployed. Testing before adding the server's outbound IP in the panel is wasted effort.
The service expects 5XXXXXXXXX: ten digits, no leading zero, no country code. Real numbers in your database are never that tidy — users type spaces, parentheses, dashes, +90 and leading zeros.
A single normalisation step before sending removes most failures:
function netgsm_number(string $raw): ?string {
$d = preg_replace('/\D+/', '', $raw); // drop non-digits
if (str_starts_with($d, '90')) $d = substr($d, 2);
if (str_starts_with($d, '0')) $d = substr($d, 1);
return preg_match('/^5\d{9}$/', $d) ? $d : null;
}
Returning null and recording it, rather than sending an invalid number, avoids both wasted credits and failures you cannot diagnose later.
There is a trade-off here. Setting encoding to TR renders Turkish letters correctly, but the message is encoded as Unicode and a single SMS drops from 160 characters to 70. Longer text is split across multiple credits.
The alternative is to fold the text down to ASCII: ş→s, ğ→g, ı→i, ö→o, ü→u, ç→c. You keep 160 characters at the cost of how the text reads. The right choice depends on content: ASCII is fine for verification codes and appointment reminders, while corporate announcements usually deserve correct spelling.
Whichever you pick, count characters before sending and record how many credits the message will consume. Otherwise the monthly invoice becomes a surprise.
In Turkey, sending commercial electronic messages requires consent registered in a national system. Informational messages — order status, appointment reminders, verification codes — fall outside that scope; campaign and promotional content falls inside it.
Make that distinction in code as well: apply the consent check to commercial sends and skip it for transactional ones. Routing both through a single function leads either to transactional messages being blocked needlessly, or to commercial messages going out where they should not.
The minimum an SMS layer should satisfy before it goes to production:
CURLOPT_TIMEOUT) so a slow SMS provider cannot stall your checkoutRecording matters most. When a "the SMS never arrived" complaint comes in, being able to see the returned code separates the cases in seconds: 00 means it was handed to the operator and the problem is downstream; 40 means the sender approval lapsed; no record at all means the send was never attempted.
SMS integrations look simple, which is exactly why they get written carelessly. The habit that matters fits in one sentence: the carrier's HTTP response does not tell you the business outcome. Read the code in the body, log every send, normalise numbers, and keep sending off the main request path. Those four points eliminate almost all SMS-related debugging.
POST https://api.netgsm.com.tr/sms/rest/v2/send, basic auth00 is success5XXXXXXXXX — no leading zero, no country codeencoding: TR renders correctly but drops the limit from 160 to 70Because the HTTP status only tells you the request arrived; whether the send was accepted is a code in the response body. 00 is success, 30 is credentials or IP restriction, 40 is an unapproved sender header, 20 is a message or character problem. If your integration does not read the body, all of these stay invisible.
That code covers IP restriction as well as bad credentials. A NetGSM account can limit API access to specific IP addresses, which is why code that works on your development machine stops after deployment. Add the server's outbound IP to the allowed list in the panel.
It depends on the content. Setting encoding to TR renders letters correctly but switches the message to Unicode, dropping the single-SMS limit from 160 to 70 characters; longer text is split across several credits. For short transactional messages such as verification codes, folding to ASCII noticeably reduces cost.
No. The obligation applies to commercial electronic messages. Transactional messages such as order status, appointment reminders and verification codes are out of scope. If you do not separate the two in code, you will either block transactional messages needlessly or send commercial ones where you should not.