Visão geral
Use a API quando as mensagens forem disparadas pelos seus próprios sistemas: atualizações de pedidos de uma loja, códigos de um formulário de login, lembretes de uma ferramenta de reservas. Uma requisição a POST /v1/messages com até 10,000 números cria uma campanha, desconta o saldo e coloca todas as mensagens na fila. As campanhas criadas pela API também aparecem no console com seus relatórios de entrega.
- Todos os valores são em dólares americanos, como strings com cinco casas decimais, por exemplo "0.00534".
- Os números de telefone são internacionais, com ou sem o + inicial; espaços e traços são removidos.
- Os horários seguem ISO 8601 em UTC, por exemplo 2026-12-01T09:00:00Z.
- Os ids de mensagens e campanhas têm a forma MSG-XXXX-XXXX-XXXX e CMP-XXXX-XXXX-XXXX.
Autenticação
Crie uma chave de API em API no console (as chaves são liberadas depois que a sua primeira recarga é adicionada). Envie-a no cabeçalho Authorization de cada requisição. Uma chave começa com bso_api_ seguido de 36 caracteres; ela é exibida uma única vez, então guarde-a como segredo no seu servidor. Criar uma nova chave substitui a anterior imediatamente.
Authorization: Bearer bso_api_0123456789abcdef0123456789abcdef0123 Content-Type: application/json
Limites
| Requisições | 300 por minuto por chave; acima disso, resposta 429 rate_limited |
|---|---|
| Números por requisição | 10,000 |
| Texto da mensagem | 2,000 caracteres como escritos, até 10 segmentos |
| Agendamento | send_at de 5 minutos a 60 dias à frente |
| Ritmo de envio | 400 mensagens por minuto por fila |
| Novas tentativas do webhook | 9 tentativas em cerca de 22 horas |
Enviar mensagens
POST/v1/messages
Envia um texto para um ou mais números. O custo total é descontado do saldo quando a requisição é aceita; cada mensagem que não pode ser entregue é reembolsada depois. A resposta tem status 201 e traz a campanha com todas as mensagens.
| Campo | Tipo | Descrição |
|---|---|---|
toobrigatório | string | string[] | Um número ou uma lista de até 10,000 números em formato internacional. Números repetidos são enviados uma vez; os ilegíveis são listados em skipped. |
textobrigatório | string | A mensagem, com até 2,000 caracteres e 10 segmentos. O texto GSM-7 comporta 160 caracteres por SMS, o Unicode 70. |
send_atopcional | string | Horário ISO 8601 para agendar o envio, de 5 minutos a 60 dias à frente. Omita para enviar agora. |
opt_outopcional | boolean | true adiciona a sua linha de cancelamento das configurações do console em uma nova linha no final. |
client_refopcional | string | Sua própria referência, com até 64 letras, dígitos e _ . : -. Ela é retornada em relatórios e webhooks. |
curl -X POST https://bulksmsonline.cc/api/v1/messages \
-H "Authorization: Bearer $BSO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"to":["+447700900412","+33612345678"],"text":"Order 4521 has shipped. Track it at shop.example/t/4521","client_ref":"order-4521"}'const res = await fetch("https://bulksmsonline.cc/api/v1/messages", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.BSO_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ to: ["+447700900412", "+33612345678"], text: "Order 4521 has shipped.", client_ref: "order-4521" }),
});
if (!res.ok) throw new Error((await res.json()).error.message);
const campaign = await res.json();import os, requests
res = requests.post("https://bulksmsonline.cc/api/v1/messages",
headers={"Authorization": f"Bearer {os.environ['BSO_API_KEY']}"},
json={"to": ["+447700900412", "+33612345678"], "text": "Order 4521 has shipped.", "client_ref": "order-4521"},
timeout=30)
res.raise_for_status()
campaign = res.json()<?php
$ch = curl_init("https://bulksmsonline.cc/api/v1/messages");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("BSO_API_KEY"), "Content-Type: application/json"],
CURLOPT_POSTFIELDS => json_encode(["to" => ["+447700900412", "+33612345678"], "text" => "Order 4521 has shipped.", "client_ref" => "order-4521"])]);
$campaign = json_decode(curl_exec($ch), true);HTTP/1.1 201 Created
{
"id": "CMP-7KQ2-M9XA-4HTD",
"status": "sending",
"channel": "api",
"recipients": 2,
"segments": 1,
"encoding": "GSM-7",
"cost": "0.02158",
"refunded": "0.00000",
"client_ref": "order-4521",
"send_at": null,
"counts": { "queued": 2, "submitted": 0, "delivered": 0, "failed": 0, "canceled": 0 },
"skipped": [],
"balance": "118.43120",
"messages": [
{ "id": "MSG-3RTW-81KD-ZQ0P", "to": "+447700900412", "country": "GB", "segments": 1, "price": "0.00834", "status": "queued" },
{ "id": "MSG-5HNA-4C2E-7YVB", "to": "+33612345678", "country": "FR", "segments": 1, "price": "0.01324", "status": "queued" }
]
}Cotar um envio
POST/v1/quote
Recebe o mesmo corpo de /v1/messages e retorna o custo exato, os segmentos e a codificação sem enviar nem cobrar nada. Use para mostrar um preço aos seus próprios usuários antes de confirmarem.
{
"recipients": 2,
"skipped": [],
"segments": 1,
"encoding": "GSM-7",
"cost": "0.02158",
"balance": "118.45278",
"countries": [ { "country": "GB", "recipients": 1, "cost": "0.00834" }, { "country": "FR", "recipients": 1, "cost": "0.01324" } ]
}Saldo
GET/v1/balance
{ "balance": "118.43120", "currency": "USD" }Tarifas
GET/v1/rates
GET/v1/rates/{country}
O preço de um segmento de SMS para cada destino, ou para um código de país ISO de duas letras. Estes dois endpoints são públicos e não precisam de chave.
{ "country": "DE", "name": "Germany", "calling_code": "+49", "price": "0.01544", "currency": "USD", "per": "segment" }Status da mensagem
GET/v1/messages/{id}
{
"id": "MSG-3RTW-81KD-ZQ0P",
"campaign_id": "CMP-7KQ2-M9XA-4HTD",
"to": "+447700900412",
"country": "GB",
"segments": 1,
"price": "0.00834",
"status": "delivered",
"error": null,
"created_at": "2026-11-07T09:00:02Z",
"submitted_at": "2026-11-07T09:00:03Z",
"final_at": "2026-11-07T09:00:08Z"
}Campanhas
GET/v1/campaigns/{id}
GET/v1/campaigns/{id}/messages?offset=0&limit=100
O primeiro retorna a campanha com suas contagens por status; o segundo percorre suas mensagens, com até 1.000 por página. As campanhas enviadas pelo console podem ser lidas da mesma forma com o id mostrado na página delas.
Cancelar um envio agendado
POST/v1/campaigns/{id}/cancel
Interrompe uma campanha criada com send_at que ainda não começou. Todas as mensagens são canceladas e o custo total volta para o saldo. Uma campanha que já está sendo enviada responde 409 not_scheduled.
Webhooks de entrega
Adicione uma URL HTTPS em API no console para receber cada status final (delivered ou failed) como uma requisição POST. Responda com qualquer status 2xx em até 8 segundos. Outras respostas e timeouts são repetidos após 20 segundos, 1, 5 e 15 minutos, depois 1, 3, 6 e 12 horas, no total de 9 tentativas. O mesmo evento pode chegar duas vezes, então use event_id para processá-lo uma só vez.
POST https://your-server.example/sms-status
Content-Type: application/json
X-BulkSMSOnline-Signature: t=1794042008,v1=5f0c2a...e91b
{
"type": "message.status",
"event_id": "ev_8f1c22d0a4b97e63c1f0",
"sent_at": "2026-11-07T09:00:09Z",
"data": {
"message_id": "MSG-3RTW-81KD-ZQ0P",
"campaign_id": "CMP-7KQ2-M9XA-4HTD",
"to": "+447700900412",
"country": "GB",
"status": "delivered",
"error": null,
"segments": 1,
"price": "0.00834",
"currency": "USD",
"client_ref": "order-4521",
"final_at": "2026-11-07T09:00:08Z"
}
}Verifique a assinatura
O cabeçalho contém o horário Unix t e a assinatura v1 = HMAC-SHA256 da string "t.raw_body" com o segredo do seu webhook, em hexadecimal. Calcule-a sobre o corpo bruto antes de interpretar o JSON, compare em tempo constante e rejeite eventos com mais de cinco minutos.
import crypto from "node:crypto";
export function verify(rawBody, header, secret) {
const m = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header || "");
if (!m || Math.abs(Date.now() / 1000 - Number(m[1])) > 300) return false;
const want = crypto.createHmac("sha256", secret).update(m[1] + "." + rawBody).digest("hex");
return crypto.timingSafeEqual(Buffer.from(want), Buffer.from(m[2]));
}import hmac, hashlib, re, time
def verify(raw_body: bytes, header: str, secret: str) -> bool:
m = re.fullmatch(r"t=(\d+),v1=([0-9a-f]{64})", header or "")
if not m or abs(time.time() - int(m.group(1))) > 300:
return False
want = hmac.new(secret.encode(), m.group(1).encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(want, m.group(2))<?php
function verify(string $raw, string $header, string $secret): bool {
if (!preg_match('/^t=(\d+),v1=([0-9a-f]{64})$/', $header, $m)) return false;
if (abs(time() - (int)$m[1]) > 300) return false;
$want = hash_hmac('sha256', $m[1] . '.' . $raw, $secret);
return hash_equals($want, $m[2]);
}Status
| queued | Aceita e aguardando para sair da fila |
|---|---|
| submitted | Entregue à rede de destino |
| delivered | Confirmada pelo aparelho (final) |
| failed | Não foi possível entregar; o valor é reembolsado (final) |
| canceled | Parte de uma campanha agendada cancelada antes do envio (final) |
O status da campanha é scheduled, sending, complete ou canceled.
Erros
Os erros respondem com um status HTTP e um corpo JSON: { "error": { "code": "insufficient_balance", "message": "..." } }. Nada é cobrado quando uma requisição responde com erro.
| HTTP | Código | Significado |
|---|---|---|
| 400 | invalid_request | Um campo está ausente ou não é válido; a mensagem indica qual. |
| 400 | invalid_json | O corpo não é JSON. |
| 400 | no_valid_numbers | Nenhum dos números pôde ser lido. |
| 401 | unauthorized | A chave está ausente, malformada ou foi substituída. |
| 402 | insufficient_balance | O saldo não cobre o custo; recarregue e envie novamente. |
| 404 | not_found | Id ou endpoint desconhecido. |
| 409 | not_scheduled | Somente uma campanha agendada que ainda não começou pode ser cancelada. |
| 413 | too_many_recipients | Mais de 10,000 números em uma só requisição. |
| 429 | rate_limited | Mais de 300 requisições em um minuto. |
| 500 | server_error | Repita a requisição; nada foi cobrado. |