DocumentationAPI v1 · 1.3.0

Reliability

Errors

Error format, every API error code, 503 FEATURE_DISABLED maintenance and typed package exceptions.

All errors have the same shape. code is stable and meant for your code; message is in French and meant for humans; details is always an object ({} when empty), whose content depends on the code.

Format
// Every error has the same shape: branch on "code", show "message" to humans (French)
{ "error": { "code": "INSUFFICIENT_CREDITS", "message": "Solde de crédits insuffisant pour cet envoi. Aucun message envoyé.", "details": { "available": 3, "required": 4 } } }

// Maintenance: nothing was done (no SMS, no debit)
{ "error": { "code": "FEATURE_DISABLED", "message": "Maintenance de l'envoi de SMS jusqu'à 10 h.",
  "details": { "feature": "api.sms_send", "blocked_by": "api.sms_send", "message": "Maintenance de l'envoi de SMS jusqu'à 10 h.", "until": "2026-11-06T10:00:00+00:00" } } }

Branch your logic on code, never on message. The code list is open: an integration must accept an unknown code (handle it by HTTP status).

Error codes#

CodeHTTPMeaningWhat to doRetry?
UNAUTHENTICATED401API key missing, invalid or revoked.All operationsCheck the Authorization: Bearer … header and the key in the client area.No
FORBIDDEN403IP address not allowed for this key.All operationsAdd the server IP to the key's allowlist, or remove the restriction.No
ACCOUNT_SUSPENDED403Account suspended by the platform: the whole API is closed until reactivation.All operationsContact support.No
OTP_DISABLED_FOR_ACCOUNT403OTP service disabled for this account by the platform.POST /otp/sendContact support.No
NOT_FOUND404Unknown route ("Route introuvable.") or resource not found for this account (message, otp_id).All operationsCheck the path and the identifier.No
HTTP_ERROR405Method not allowed on this path (details.allowed_methods, Allow header).All operationsUse one of the methods in details.allowed_methods.No
INSUFFICIENT_CREDITS409The balance does not cover the send: nothing is sent (details.available, details.required). Never with a test key.POST /sms, POST /otp/sendTop up the account, then resend.No
IDEMPOTENCY_KEY_REUSED409Idempotency-Key already used for a request with different content.POST /sms, POST /otp/sendGenerate one key per business operation, never reused for other content.No
IDEMPOTENCY_KEY_UNRESOLVED409The original request with this key failed unexpectedly: its outcome is unknown (an SMS may have been sent).POST /sms, POST /otp/sendDo not retry automatically: check (GET /sms?reference=), then resend with a new key.No
IDEMPOTENCY_REQUEST_IN_PROGRESS409The original request with this key is still in progress (Retry-After: 5).POST /sms, POST /otp/sendRetry with the same key after Retry-After seconds.After a delay
VALIDATION_ERROR422Missing or invalid field; details lists messages per field.All operationsFix the request (messages are in French).No
RECIPIENT_NOT_SENDABLE422Invalid, blacklisted or opted-out number (details.reason, details.rejected). All or nothing: nothing is sent.POST /sms, POST /otp/sendRemove the rejected numbers and resend.No
SENDER_ID_NOT_APPROVED422sender (or the OTP settings' Sender ID) is not an approved Sender ID of the account.POST /sms, POST /otp/sendUse a value from GET /sender-ids, or omit sender.No
INVALID_PHONE422Unusable phone number.POST /otp/sendAsk the user to correct the number.No
INVALID_OTP_TEMPLATE422template without the {code} token.POST /otp/sendAdd {code} to the template.No
TOO_MANY_REQUESTS429Rate limit reached (details.retry_after_seconds).All operationsWait Retry-After seconds, then retry.After a delay
OTP_RESEND_TOO_SOON429Resend too soon to the same number while the last code is not verified.POST /otp/sendShow a Retry-After seconds countdown.After a delay
OTP_RATE_LIMITED429Hourly cap reached, per number or per account (details.scope, details.limit).POST /otp/sendDo not retry before Retry-After; inform the user.After a delay
FEATURE_DISABLED503Feature turned off by the platform (maintenance): nothing is done. details = feature, blocked_by, message, until.All operationsDo not retry in a loop: retry after Retry-After when present, otherwise queue or inform the user.After a delay
OTP_SERVICE_DISABLED503The whole OTP service is off (legacy code, same details as FEATURE_DISABLED). Codes already sent can still be verified.POST /otp/sendSame as FEATURE_DISABLED.After a delay

Maintenance: 503 FEATURE_DISABLED#

A platform administrator can temporarily turn off the whole API or one function. The call then gets 503 FEATURE_DISABLED and nothing is done: no send, no debit.

detailsContent
featureFeature requested by the call.
blocked_byFeature actually turned off: itself or a parent (api when the whole API is off).
messageAdministrator message, null otherwise (error.message repeats it or gives a generic text).
untilScheduled automatic reactivation (ISO 8601), null if unknown. When known, the Retry-After header gives the remaining seconds.
FeatureOperations affected
apiThe whole API, every route. The refusal comes before authentication and limits, even with an invalid key.
api.sms_sendPOST /sms
api.otp_sendPOST /otp/send
api.otp_verifyPOST /otp/verify
otpThe whole OTP service: POST /otp/send returns the legacy code 503 OTP_SERVICE_DISABLED, with the same details. Codes already sent can still be verified.

What to do?#

  • Do not retry in a loop.
  • If Retry-After is present, retry after that delay (packages do it on their own when it is short: 10 s by default, maxRetryWait).
  • Otherwise, treat the call as a temporary failure: queue the send, or show details.message to the user.

Handling errors over HTTP#

Over HTTP

curl -i -X POST "https://api.labyrinthe-rdc.com/api/public/v1/sms" \
  -H "Authorization: Bearer lsk_test_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{"recipient":"243812345678","body":"Bonjour"}'

# HTTP/1.1 503 Service Unavailable
# Retry-After: 1800            <- only when a return date is scheduled
# X-Request-Id: 5f0c2b9e-...   <- give it to support
# {"error":{"code":"FEATURE_DISABLED", ...}}

Package exceptions#

Every SDK turns errors into typed exceptions, with code, HTTP status, details and request ID. Maintenance has its own class, FeatureDisabledException / FeatureDisabledError, which covers FEATURE_DISABLED and OTP_SERVICE_DISABLED and exposes feature, blocked_by, the administrator message, until and Retry-After.

With the packages

<?php
use Labyrinthe\Connect\Client;
use Labyrinthe\Connect\Exception\ApiException;
use Labyrinthe\Connect\Exception\FeatureDisabledException;
use Labyrinthe\Connect\Exception\InsufficientCreditsException;
use Labyrinthe\Connect\Exception\RateLimitException;
use Labyrinthe\Connect\Exception\RecipientNotSendableException;

$client = new Client('lsk_test_xxxxxxxxxxxxxxxx'); // or Client::fromEnv() (LABYRINTHE_API_KEY)

try {
    $client->sms->send(to: '243812345678', body: 'Bonjour');
} catch (FeatureDisabledException $e) {
    // 503 maintenance: nothing was sent, no credit used
    $e->getFeature();      // 'api.sms_send'
    $e->getBlockedBy();    // 'api' when the whole API is off
    $e->getAdminMessage(); // platform message, or null
    $e->getUntil();        // ?DateTimeImmutable
    $e->getRetryAfter() !== null ? retryIn($e->getRetryAfter()) : queueForLater();
} catch (InsufficientCreditsException $e) {
    notifyTopUp($e->getAvailable(), $e->getRequired());
} catch (RecipientNotSendableException $e) {
    foreach ($e->getRejected() as $rejected) {
        echo $rejected->recipient, ' ', $rejected->reason->value, PHP_EOL;
    }
} catch (RateLimitException $e) {
    retryIn($e->getRetryAfter() ?? 60);
} catch (ApiException $e) {
    logger($e->getErrorCode(), $e->getStatusCode(), $e->getRequestId());
}
PackageBaseMaintenance
PHP / LaravelApiException : getErrorCode(), getStatusCode(), getDetails(), getRequestId()FeatureDisabledException : getFeature(), getBlockedBy(), getAdminMessage(), getUntil(), getRetryAfter()
Node.jsApiError : code, status, details, requestIdFeatureDisabledError : feature, blockedBy, adminMessage, until, retryAfter
PythonApiError : code, status, details, request_idFeatureDisabledError : feature, blocked_by, admin_message, until, retry_after
.NETApiException : Code, StatusCode, Details, RequestIdFeatureDisabledException : Feature, BlockedBy, AdminMessage, Until, RetryAfter
JavaApiException : code(), statusCode(), details(), requestId()FeatureDisabledException : feature(), blockedBy(), adminMessage(), until(), retryAfter()

Errors common to every route#

  • 404 NOT_FOUND, message "Route introuvable.": unknown path.
  • 405 HTTP_ERROR: method not allowed (e.g. DELETE /balance), with details.allowed_methods and the Allow header.

Always give the X-Request-Id header to support.