API de SMS em massa v1
Uma pequena API REST para enviar SMS em massa a partir do seu próprio software: JSON de entrada e saída, chaves Bearer, os mesmos preços e verificações do console, e webhooks assinados para relatórios de entrega. URL base https://smsbulk.cc/rest/v1
Visão geral
Envie para até 8,000 números por solicitação, cada um com seus próprios valores de mesclagem, e a API precifica cada destinatário com seu próprio texto mesclado, exatamente como o console. Solicite uma cotação primeiro com o mesmo corpo para saber o custo; nada é enviado ou cobrado por uma cotação. Os horários são ISO 8601 em UTC, o dinheiro é uma string em dólares americanos com cinco casas decimais.
curl https://smsbulk.cc/rest/v1/send \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"recipients": [
{ "to": "+14155550142", "fields": { "name": "Maria" } },
{ "to": "+447400123456", "fields": { "name": "Tom" } }
],
"text": "Hi {{name}}, your order ships today.",
"client_ref": "order-batch-17"
}'const res = await fetch("https://smsbulk.cc/rest/v1/send", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
recipients: [
{ to: "+14155550142", fields: { name: "Maria" } },
{ to: "+447400123456", fields: { name: "Tom" } },
],
text: "Hi {{name}}, your order ships today.",
client_ref: "order-batch-17",
}),
});
const data = await res.json();
console.log(res.status, data.campaign?.id, data.campaign?.cost);import requests
res = requests.post(
"https://smsbulk.cc/rest/v1/send",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={
"recipients": [
{"to": "+14155550142", "fields": {"name": "Maria"}},
{"to": "+447400123456", "fields": {"name": "Tom"}},
],
"text": "Hi {{name}}, your order ships today.",
"client_ref": "order-batch-17",
},
timeout=30,
)
data = res.json()
print(res.status_code, data.get("campaign", {}).get("id"))<?php
$body = json_encode([
"recipients" => [
["to" => "+14155550142", "fields" => ["name" => "Maria"]],
["to" => "+447400123456", "fields" => ["name" => "Tom"]],
],
"text" => "Hi {{name}}, your order ships today.",
"client_ref" => "order-batch-17",
]);
$ch = curl_init("https://smsbulk.cc/rest/v1/send");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer YOUR_API_KEY", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$data = json_decode(curl_exec($ch), true);
echo curl_getinfo($ch, CURLINFO_HTTP_CODE), " ", $data["campaign"]["id"] ?? "", PHP_EOL;Autenticação
Crie uma chave no console em API e webhooks; as chaves são liberadas após a primeira recarga creditada. Envie-a em cada solicitação como um token Bearer. Uma chave é mostrada uma vez; você pode manter até 5 chaves e revogar qualquer uma delas de uma vez.
Authorization: Bearer smsb_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxTarifas
Público, sem necessidade de chave. O preço por segmento de SMS para cada destino, ou para um código ISO.
GEThttps://smsbulk.cc/rest/v1/rates
GEThttps://smsbulk.cc/rest/v1/rates/{ISO}
{
"country": "US",
"name": "United States",
"dial_code": "+1",
"rate": "0.00485",
"currency": "USD",
"per": "message segment"
}Crédito
GEThttps://smsbulk.cc/rest/v1/credit
{ "credit": "125.40000", "currency": "USD", "account": "A418820037715" }Cotação
Precisa um envio sem enviá-lo. Recebe o mesmo corpo que Enviar.
POSThttps://smsbulk.cc/rest/v1/quote
| Campo | Tipo | Significado |
|---|---|---|
| to | string or string[] | Um número ou uma lista. Formato internacional com + é mais seguro; números locais precisam de default_country. |
| recipients | object[] | Em vez de to: uma lista de { to, fields } onde fields preenche campos de mesclagem como {{name}}. |
| text | string | A mensagem, até 2,000 caracteres e 10 segmentos após a mesclagem. |
| default_country | string | Código ISO usado para números sem +, como GB. Usa o padrão salvo em Configurações. |
| opt_out | boolean | Adiciona seu texto de cancelamento salvo como última linha (salve-o primeiro em Configurações). |
{
"recipients": 2,
"messages": 2,
"segments": { "min": 1, "max": 1 },
"encoding": "GSM-7",
"total": "0.01240",
"currency": "USD",
"credit": "125.40000",
"credit_after": "125.38760",
"countries": [
{ "country": "US", "recipients": 1, "rate": "0.00485", "segments": 1, "subtotal": "0.00485" },
{ "country": "GB", "recipients": 1, "rate": "0.00755", "segments": 1, "subtotal": "0.00755" }
],
"left_out": []
}Enviar
Cria uma campanha, retira o total do seu crédito e coloca as mensagens na fila. Responde com 201 e a campanha. Números que não podem ser enviados são listados em left_out e nunca são cobrados.
POSThttps://smsbulk.cc/rest/v1/send
| Campo | Tipo | Significado |
|---|---|---|
| to / recipients / text / default_country / opt_out | Como em Quote. | |
| send_at | string | Horário ISO 8601 para início, de 5 minutos a 90 dias no futuro. Omita para enviar agora. |
| name | string | Seu rótulo para a campanha, até 80 caracteres. |
| client_ref | string | Sua própria referência, até 64 caracteres, retornada em cada mensagem e webhook. |
HTTP/1.1 201 Created
{
"campaign": {
"id": "C507318264409",
"name": null,
"status": "sending",
"source": "api",
"test": false,
"recipients": 2,
"cost": "0.01240",
"refunded": "0.00000",
"net_cost": "0.01240",
"currency": "USD",
"send_at": null,
"created_at": "2026-11-07T14:02:11.000Z",
"started_at": "2026-11-07T14:02:11.000Z",
"finished_at": null,
"client_ref": "order-batch-17"
},
"messages": 2,
"left_out": []
}Campanhas
GEThttps://smsbulk.cc/rest/v1/campaigns?limit=25&before={id}
GEThttps://smsbulk.cc/rest/v1/campaigns/{id}
GEThttps://smsbulk.cc/rest/v1/campaigns/{id}/messages?status=failed&limit=100&after={message id}
Uma única campanha inclui contagens por status: na fila, enviado, entregue, falhou e cancelado. Lista de mensagens com after e has_more; até 1.000 por página.
Mensagens
GEThttps://smsbulk.cc/rest/v1/messages/{id}
GEThttps://smsbulk.cc/rest/v1/messages?to=%2B14155550142&status=delivered&client_ref=order-batch-17&limit=50
{
"id": "M220947781305",
"campaign": "C507318264409",
"to": "+14155550142",
"country": "US",
"segments": 1,
"encoding": "GSM-7",
"price": "0.00485",
"status": "delivered",
"reason": null,
"reason_text": null,
"refunded": false,
"client_ref": "order-batch-17",
"created_at": "2026-11-07T14:02:11.000Z",
"sent_at": "2026-11-07T14:02:12.000Z",
"finished_at": "2026-11-07T14:02:16.000Z"
}Cancelar uma campanha agendada
POSThttps://smsbulk.cc/rest/v1/campaigns/{id}/cancel
Funciona até o horário de início. O custo total retorna ao seu crédito em uma linha do livro-razão e o status da campanha se torna cancelado.
Webhooks
Defina uma URL de endpoint no console. Enviamos JSON para message.delivered, message.failed e campaign.done, além de testar quando você pressiona Enviar evento de teste. Cada evento carrega quatro cabeçalhos, e qualquer resposta 2xx dentro de 10 segundos conta como recebida.
| Campo | Tipo | Significado |
|---|---|---|
| Smsbulk-Event | cabeçalho | Nome do evento, como message.delivered. |
| Smsbulk-Delivery | cabeçalho | ID de entrega, o mesmo em cada tentativa de um evento. |
| Smsbulk-Timestamp | cabeçalho | Segundos Unix quando esta tentativa foi assinada. |
| Smsbulk-Signature | cabeçalho | sha256= seguido do HMAC-SHA256 em base64 do timestamp, uma quebra de linha e o corpo bruto, chaveado com seu segredo sbw_. |
A entrega é tentada 8 vezes: imediatamente, depois de 1 minuto, 5 minutos, 15 minutos, 1 hora, 3 horas, 6 horas e 12 horas. Cada tentativa aparece no log de entrega do console com seu status HTTP e o início da sua resposta.
{
"id": "dlv_3f9a1c07b2e44d58a6c1",
"event": "message.failed",
"created": "2026-11-07T14:02:16.000Z",
"data": {
"message": "M220947781305",
"campaign": "C507318264409",
"to": "+447400123456",
"country": "GB",
"status": "failed",
"reason": "not_in_service",
"segments": 1,
"price": "0.00755",
"client_ref": "order-batch-17"
}
}Verificar a assinatura
import crypto from "node:crypto";
// raw = the request body exactly as received (string), secret = your sbw_ signing secret
export function verify(raw, headers, secret) {
const ts = headers["smsbulk-timestamp"];
const want = "sha256=" + crypto.createHmac("sha256", secret).update(ts + "\n" + raw).digest("base64");
const got = headers["smsbulk-signature"] || "";
const fresh = Math.abs(Date.now() / 1000 - Number(ts)) < 300;
return fresh && got.length === want.length && crypto.timingSafeEqual(Buffer.from(got), Buffer.from(want));
}import base64, hashlib, hmac, time
def verify(raw: bytes, headers, secret: str) -> bool:
ts = headers.get("Smsbulk-Timestamp", "")
mac = hmac.new(secret.encode(), ts.encode() + b"\n" + raw, hashlib.sha256).digest()
want = "sha256=" + base64.b64encode(mac).decode()
fresh = abs(time.time() - int(ts or 0)) < 300
return fresh and hmac.compare_digest(want, headers.get("Smsbulk-Signature", ""))Erros e limites
Erros usam o status HTTP e um corpo JSON: {"error":{"code":"insufficient_credit","message":"..."}}. Nada é cobrado quando uma requisição falha.
| Campo | Tipo | Significado |
|---|---|---|
| 400 bad_json | O corpo não é JSON. | |
| 401 unauthorized | Chave ausente, malformada ou revogada. | |
| 402 insufficient_credit | O total excede seu saldo. Adicione créditos no console. | |
| 404 not_found | Endpoint, campanha, mensagem ou código ISO não encontrado nesta conta. | |
| 409 not_scheduled | Cancelamento solicitado para uma campanha já iniciada. | |
| 422 text_missing, text_too_long, too_many_segments | Mensagem vazia, com mais de 2,000 caracteres ou mais de 10 segmentos para um destinatário. | |
| 422 no_recipients, too_many, recipients_missing | Nenhum número válido, mais de 8,000 números, ou nem 'to' nem 'recipients' especificados. | |
| 422 schedule_range, bad_send_at | send_at fora do intervalo de 5 minutos a 90 dias, ou não é um horário válido. | |
| 422 opt_out_unset | opt_out está ativado, mas nenhum texto de cancelamento foi salvo nas Configurações. | |
| 429 rate_limited | Mais de 300 solicitações por minuto para esta chave. |
Cada resposta inclui X-RateLimit-Limit, X-RateLimit-Remaining e X-RateLimit-Reset.
Códigos excluídos
Números removidos antes da precificação retornam em left_out com sua posição na lista, o valor enviado, um código e o motivo claro.
| Campo | Tipo | Significado |
|---|---|---|
| no_country_code | Sem '+' e sem país padrão definido. | |
| too_short / too_long | Menos de 8 ou mais de 15 dígitos. | |
| not_a_number | Letras ou símbolos que não fazem parte de um número. | |
| nanp_length | Um número +1 sem onze dígitos. | |
| not_covered | Um destino fora da nossa lista. | |
| duplicate | O mesmo número já apareceu anteriormente na solicitação. | |
| suppressed | O número está na sua lista de não envio. |
Status e motivos
| Campo | Tipo | Significado |
|---|---|---|
| queued | status | Aguardando para sair da nossa fila. |
| sent | status | Enviado para a operadora, aguardando relatório de entrega. A maioria chega em minutos; algumas operadoras podem levar até 24 horas. |
| delivered | status | A operadora confirmou que o telefone recebeu. |
| failed | status | Não entregue. O valor foi reembolsado ao seu saldo. |
| canceled | status | Não enviado devido ao cancelamento. Reembolsado. |
| Campo | Tipo | Significado |
|---|---|---|
| network_error | motivo | Erro de rede |
| switched_off | motivo | Telefone desligado ou fora de cobertura |
| not_in_service | motivo | Número não está em serviço |
| blocked | motivo | Bloqueado pela operadora ou pelo destinatário |
| landline | motivo | Linha fixa ou número que não pode receber SMS |
| content_filter | motivo | Rejeitado pelo filtro de conteúdo da operadora |
| expired | motivo | Expirado: nenhum relatório antes do prazo da operadora |
| unknown | motivo | Resposta da operadora desconhecida |
| canceled | motivo | Cancelado antes do envio |
Dúvidas sobre a API? Abra um ticket no console ou leia o FAQ.