API documentation
Error codes
HTTP status tells you what kind of problem it is; the message tells you what to fix.
| Status | Meaning | Typical message | What to do |
|---|---|---|---|
400 | Bad request | Missing required fields: to, messageInsufficient balanceInvalid phone number | Fix the payload; top up if balance is low. |
401 | Unauthenticated | Missing API key · Invalid API keyMissing required headers: X-Timestamp, X-SignatureRequest timestamp expired or invalid · Invalid signature | Check the key, and for OTP endpoints re-sign with a fresh millisecond timestamp and the exact body string. |
403 | Forbidden | API key does not have the required scopeIP address not allowedDaily spend limit reached | Ask support to adjust scopes, whitelist or limits. |
404 | Not found | Message not found · Contact not found · Unknown endpoint | Check the id / path. |
405 | Method not allowed | Use the method listed in the reference. | |
429 | Rate limited | Too many requests, please try again later | Back off until RateLimit-Reset; batch with /sms/bulk. |
500 | Server error | Failed to … | Retry with the same Idempotency-Key; contact support if it persists. |
502 / 503 | Service unavailable | Messaging service temporarily unavailable | Retry with back-off (we are usually back within seconds). |
Handling pattern
retry.js
async function send(payload, attempt = 1) {
const res = await fetch('https://tiansms.com/api/v1/sms/send', {
method: 'POST',
headers: { 'x-api-key': KEY, 'Content-Type': 'application/json', 'Idempotency-Key': payload.idempotencyKey },
body: JSON.stringify(payload.body)
});
const json = await res.json();
if (res.ok && json.success) return json.data;
if ((res.status === 429 || res.status >= 500) && attempt < 4) {
await new Promise(r => setTimeout(r, 500 * 2 ** attempt));
return send(payload, attempt + 1);
}
throw new Error(json.message || ('HTTP ' + res.status));
}
Message-level failures
An accepted message can still fail later (handset off for days, number ported, DND). You will see status: "failed" with error_message on /sms/status/{id} or an sms.failed webhook. Failed messages are refunded automatically.