Desarrolladores
Referencia de la API
Un endpoint para enviar, otro para leer el estado, webhooks firmados con HMAC para los recibos. Las claves se emiten en el momento en que existe la cuenta — no hay nada que aprobar ni nadie a quien escribir.
Primeros pasos
Tres cosas separan una cuenta nueva de un mensaje entregado: un token, algo de saldo y una clave. Ninguna de ellas involucra a una persona.
- Crea una cuenta: su navegador genera el token, y ese token es la cuenta.
- Recárguela. $40 mínimo, en cualquiera de las monedas en la página de pagos.
- Cree una clave de API en el panel. Se muestra una sola vez; solo almacenamos su hash, igual que el token.
Una clave con saldo vacío no puede enviar
La creación de claves está deliberadamente bloqueada hasta que la cuenta tenga saldo. Una credencial sin usar dando vueltas es un riesgo sin ninguna ventaja, así que el panel no generará una hasta que haya algo que gastar.
Autenticación
Token portador en cada solicitud. La clave identifica la cuenta; no hay segundo factor, no hay firma en la solicitud y no hay lista blanca de IP — la clave es toda la credencial, así que trátela como tal.
Authorization: Bearer ws_live_9f2c…
| Transporte | Solo HTTPS. Una solicitud por HTTP simple se rechaza, no se redirige — una redirección ya habría filtrado la clave. |
|---|---|
| Alcance | Una clave gasta el saldo de una sola cuenta. Cree una clave por cada sistema que envíe, así una filtración se revoca de forma acotada. |
| Revocación | Inmediato. Una clave revocada devuelve 401 en la siguiente solicitud; los lotes en curso ya aceptados igualmente se completan. |
| Recuperación | Ninguno. Guardamos un hash. Una clave perdida se reemplaza, nunca se recupera. |
Enviar mensajes
POST/v1/messages
Una llamada, uno o varios destinatarios, destinos mixtos. Cada destinatario se factura a la tarifa de su propio destino y se cobra por segmento. La llamada se acepta o se rechaza en conjunto; nunca le cobra parcialmente.
Cuerpo de la solicitud
| Campo | Tipo | Notas |
|---|---|---|
to | string[] | Obligatorio. E.164, hasta 5000 por llamada. Los números que no correspondan a un destino con tarifa se rechazan antes de facturar. |
text | string | Obligatorio. UTF-8. El recuento de segmentos sigue el estándar GSM 03.38 — consulta segmentos. |
from | string | ID de remitente alfanumérico, de hasta 11 caracteres. Se conserva donde la ruta lo permite; consulta ID de remitente. |
vars | object[] | Valores de combinación opcionales, un objeto por destinatario, sustituidos en los marcadores {{name}} antes de que se cuenten los segmentos. |
callback_url | string | Anulación opcional por lote del webhook de la cuenta. |
reference | string | Opcional. Se repite en cada recibo para que pueda vincularlo con sus propios registros. |
Ejemplo
# one call, many recipients, mixed destinations
curl -X POST https://api.worldsms.io/v1/messages \
-H "Authorization: Bearer $WORLDSMS_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "ACME",
"to": ["+447700900123", "+33612345678"],
"text": "Your code is 480913"
}'
{
"batch_id": "b_7Kq2xR9wLm",
"accepted": 2,
"rejected": 0,
"segments": 1,
"cost_usd": 0.0353
}
0,0137 $ hacia el Reino Unido más 0,0216 $ hacia Francia — la tarifa aplicada por destino, no un promedio combinado.
Agrupación
Un lote se acepta de inmediato y se procesa a 200 mensajes por minuto por cuenta, así que 50.000 destinatarios tardan aprox. 4 h 10 min. El tráfico urgente no debería hacer cola detrás de un envío de marketing: use una clave distinta para eso.
Estado del mensaje
GET/v1/messages/{batch_id}
El sondeo (polling) es compatible y tiene límite de tasa; los webhooks son la vía prevista. La respuesta consolida el lote y lista el estado por destinatario.
{
"batch_id": "b_7Kq2xR9wLm",
"status": "done",
"sending": 0,
"delivered": 1,
"undelivered": 1,
"refund_usd": 0.0216
}
refund_usd se liquida una sola vez, cuando el lote termina. Ya está de vuelta en su saldo para cuando puede leerlo.
Saldo disponible
GET/v1/balance
Compruébelo antes de un envío grande. Los saldos disponibles se mantienen internamente en microdólares enteros — las tarifas llegan a cuatro decimales, y un número de coma flotante no tiene cabida en un registro de movimientos — y se devuelven aquí redondeados a seis.
{ "balance_usd": 412.905400, "currency": "USD" }
Confirmaciones de entrega
Se emite un recibo por destinatario a medida que el operador lo reporta. Estos son los únicos cuatro estados; no existe un cajón de “desconocido” que signifique en silencio “lo perdimos”.
| Estado | Significado | Facturado |
|---|---|---|
queued | Aceptado y esperando su turno en el lote. | Sí |
sent | Entregado al operador, aún sin confirmación. | Sí |
delivered | El teléfono lo confirmó. | Sí |
undelivered | Muerto, bloqueado, o el remitente fue rechazado. | Reembolsado |
Los números que nunca resuelven a un destino con precio se rechazan en el envío y jamás entran en este ciclo de vida: no se facturan para luego reembolsarse, simplemente no se facturan.
Webhooks
Configure un endpoint en el panel o por lote con callback_url. Enviamos JSON por POST y esperamos un 2xx en 5 segundos. Los que no sean 2xx se reintentan seis veces con retroceso exponencial durante aproximadamente una hora, y después se descartan.
Carga útil
{
"event": "message.delivered",
"batch_id": "b_7Kq2xR9wLm",
"to": "+447700900123",
"destination": "gb",
"segments": 1,
"cost_usd": 0.0137,
"reference": "order-4471",
"ts": 1785312041
}
Verificando la firma
Cada entrega lleva X-WorldSMS-Signature y X-WorldSMS-Timestamp. La firma es HMAC-SHA256(timestamp + "." + body) usando su secreto de webhook. Compare en tiempo constante, y rechace una marca de tiempo con más de cinco minutos de antigüedad, para que una entrega capturada no pueda repetirse contra usted más tarde.
// $secret is the webhook secret from the dashboard
$raw = file_get_contents('php://input');
$ts = $_SERVER['HTTP_X_WORLDSMS_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_X_WORLDSMS_SIGNATURE'] ?? '';
// A stale timestamp is a replay, however well it is signed.
if (abs(time() - (int) $ts) > 300) { http_response_code(408); exit; }
$mine = hash_hmac('sha256', $ts . '.' . $raw, $secret);
if (!hash_equals($mine, $sig)) { http_response_code(401); exit; }
http_response_code(204);
Ser idempotente
Un reintento tras el tiempo de espera agotado de su endpoint es idéntico a una primera entrega. Use como clave batch_id + to + event y trate una repetición como una operación nula.
Errores
Cada error devuelve un cuerpo JSON con un slug error estable. Compare con el slug, nunca con el texto — el texto puede mejorar.
| HTTP | Slug | Qué hacer |
|---|---|---|
| 400 | invalid_body | JSON mal formado o falta un campo requerido. |
| 401 | key_rejected | Clave ausente, revocada o mal escrita. No reintentable. |
| 402 | insufficient_credit | El lote cuesta más que el saldo disponible. No se envió nada; recargue y vuelva a enviarlo. |
| 422 | no_valid_recipients | Ningún número pudo resolverse a un destino con tarifa. |
| 422 | sender_id_invalid | from tiene más de 11 caracteres o contiene una cadena solo de dígitos que se leería como un número. |
| 429 | rate_limited | Reduzca la frecuencia. Retry-After indica el número de segundos. |
| 503 | route_unavailable | Un destino no tiene ruta temporalmente. Reintentable. |
{
"error": "insufficient_credit",
"message": "Batch costs $84.20, balance is $12.05.",
"required_usd": 84.20,
"balance_usd": 12.05
}
Límites
| Solicitudes | 60 por minuto por clave, con picos de 20. 429 con Retry-After a partir de ahí. |
|---|---|
| Destinatarios por llamada | 5,000. Divida las listas más grandes en varias llamadas, o suba el archivo en el panel. |
| Tamaño del cuerpo | 2 MB. |
| Rendimiento | 200 mensajes por minuto por cuenta, sin importar cuántas claves los envíen. |
| Claves | Sin límite. Uno por sistema de envío es el patrón previsto. |
Los límites de tasa se aplican a las llamadas a la API, no a los mensajes: una llamada con 5.000 destinatarios cuenta como una sola solicitud contra el límite.
Las claves se emiten al instante. No hay paso de aprobación.
Financie una cuenta y el panel genera una al instante.