Panoramica
Usa l'API quando i messaggi sono attivati dai tuoi sistemi: aggiornamenti ordini da un negozio, codici da un modulo di accesso, promemoria da uno strumento di prenotazione. Una richiesta a POST /v1/messages con fino a 10,000 numeri crea una campagna, addebita il saldo e accoda ogni messaggio. Le campagne create tramite API appaiono anche nella console con i loro rapporti di consegna.
- Tutti gli importi sono in dollari USA come stringhe con cinque decimali, ad esempio "0.00534".
- I numeri di telefono sono internazionali, con o senza il prefisso +; spazi e trattini vengono rimossi.
- Gli orari sono in formato ISO 8601 in UTC, ad esempio 2026-12-01T09:00:00Z.
- Gli ID messaggio e campagna hanno un aspetto simile a MSG-XXXX-XXXX-XXXX e CMP-XXXX-XXXX-XXXX.
Autenticazione
Crea una chiave API nella sezione API della console (le chiavi si sbloccano dopo il primo ricarico). Inviarla nell'header Authorization di ogni richiesta. Una chiave ha un aspetto simile a bso_api_ seguito da 36 caratteri; viene mostrata una sola volta, quindi conservala come segreto sul tuo server. Creare una nuova chiave sostituisce immediatamente la precedente.
Authorization: Bearer bso_api_0123456789abcdef0123456789abcdef0123 Content-Type: application/json
Limiti
| Richieste | 300 al minuto per chiave; di più risponde 429 rate_limited |
|---|---|
| Numeri per richiesta | 10,000 |
| Testo messaggio | 2,000 caratteri così come scritti, fino a 10 segmenti |
| Pianificazione | send_at da 5 minuti a 60 giorni nel futuro |
| Velocità di invio | 400 messaggi al minuto per coda |
| Tentativi webhook | 9 tentativi in circa 22 ore |
Invia messaggi
POST/v1/messages
Invia un testo a uno o più numeri. Il costo totale viene prelevato dal saldo quando la richiesta è accettata; ogni messaggio non consegnabile viene rimborsato successivamente. La risposta ha stato 201 e la campagna con ogni messaggio.
| Campo | Tipo | Descrizione |
|---|---|---|
toobbligatorio | string | string[] | Un numero o un array fino a 10,000 numeri in formato internazionale. I numeri ripetuti vengono inviati una volta; quelli illeggibili sono elencati in skipped. |
textobbligatorio | string | Il messaggio, fino a 2,000 caratteri e 10 segmenti. Il testo GSM-7 contiene 160 caratteri per SMS, Unicode 70. |
send_atopzionale | string | Ora ISO 8601 per programmare l'invio, da 5 minuti a 60 giorni nel futuro. Ometti per inviare ora. |
opt_outopzionale | boolean | true aggiunge la tua riga di opt-out dalle impostazioni della console su una nuova riga alla fine. |
client_refopzionale | string | Il tuo riferimento personale, fino a 64 lettere, cifre e _ . : -. Viene restituito nei report e nei webhook. |
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" }
]
}Preventivo invio
POST/v1/quote
Prende lo stesso corpo di /v1/messages e restituisce il costo esatto, i segmenti e la codifica senza inviare o addebitare nulla. Usalo per mostrare un prezzo ai tuoi utenti prima che confermino.
{
"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" }Tariffe
GET/v1/rates
GET/v1/rates/{country}
Il prezzo di un segmento SMS per ogni destinazione, o per un codice paese ISO di due lettere. Questi due endpoint sono pubblici e non richiedono una chiave.
{ "country": "DE", "name": "Germany", "calling_code": "+49", "price": "0.01544", "currency": "USD", "per": "segment" }Stato messaggio
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"
}Campagne
GET/v1/campaigns/{id}
GET/v1/campaigns/{id}/messages?offset=0&limit=100
Il primo restituisce la campagna con i suoi conteggi per stato; il secondo sfoglia i suoi messaggi, fino a 1.000 per pagina. Le campagne inviate dalla console possono essere lette allo stesso modo con l'id mostrato sulla loro pagina.
Annulla invio programmato
POST/v1/campaigns/{id}/cancel
Interrompe una campagna creata con send_at e non ancora iniziata. Ogni messaggio viene annullato e il costo totale torna al saldo. Una campagna già in invio risponde 409 not_scheduled.
Webhook di consegna
Aggiungi un URL HTTPS sotto API nella console per ricevere ogni stato finale (consegnato o fallito) come richiesta POST. Rispondi con qualsiasi stato 2xx entro 8 secondi. Altre risposte e timeout vengono ritentati dopo 20 secondi, 1, 5 e 15 minuti, poi 1, 3, 6 e 12 ore, 9 tentativi in totale. Lo stesso evento può arrivare due volte, quindi usa event_id per elaborarlo una sola volta.
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"
}
}Verifica la firma
L'header contiene il tempo Unix t e la firma v1 = HMAC-SHA256 della stringa "t.raw_body" con il tuo segreto webhook, in esadecimale. Calcolalo sul corpo grezzo prima di analizzare il JSON, confronta in tempo costante e rifiuta eventi più vecchi di cinque minuti.
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]);
}Stati
| queued | Accettato e in attesa di uscire dalla coda |
|---|---|
| submitted | Consegnato alla rete di destinazione |
| delivered | Confermato dal dispositivo (finale) |
| failed | Impossibile consegnare; il prezzo viene rimborsato (finale) |
| canceled | Parte di una campagna programmata annullata prima dell'invio (finale) |
Lo stato della campagna è scheduled, sending, complete o canceled.
Errori
Gli errori rispondono con uno stato HTTP e un corpo JSON: { "error": { "code": "insufficient_balance", "message": "..." } }. Niente viene addebitato quando una richiesta risponde con un errore.
| HTTP | Codice | Significato |
|---|---|---|
| 400 | invalid_request | Un campo è mancante o non valido; il messaggio indica quale. |
| 400 | invalid_json | Il corpo non è JSON. |
| 400 | no_valid_numbers | Nessuno dei numeri può essere letto. |
| 401 | unauthorized | La chiave è mancante, malformata o sostituita. |
| 402 | insufficient_balance | Il saldo non copre il costo; ricarica e invia di nuovo. |
| 404 | not_found | ID o endpoint sconosciuto. |
| 409 | not_scheduled | Solo una campagna programmata che non è ancora iniziata può essere annullata. |
| 413 | too_many_recipients | Più di 10,000 numeri in una richiesta. |
| 429 | rate_limited | Più di 300 richieste in un minuto. |
| 500 | server_error | Riprova la richiesta; non è stato addebitato nulla. |