Riferimento API SMS
JSON su HTTPS. Crea una chiave nella pagina della chiave API dopo la tua prima ricarica, poi inviala come token Bearer.
Indirizzo di base e chiavi
Tutte le chiamate vanno all'indirizzo qui sotto. Invia la tua chiave nell'header Authorization. Chiunque abbia la chiave può spendere il tuo saldo, quindi conservala solo sul tuo server.
https://portsms.com/api/v1Authorization: Bearer psm_your_api_key Content-Type: application/json
Limiti: 300 chiamate al minuto per account, 1.000 destinatari per richiesta, 50,000 messaggi per account nelle 24 ore.
Inviare un messaggio
Invia un testo a uno o più numeri. I numeri devono essere in formato internazionale (+ e prefisso del paese). L'intero costo viene addebitato sul saldo quando la richiesta viene accettata; i numeri che non possiamo servire vengono restituiti in rejected e non sono addebitati.
POST/api/v1/messages
| to | stringa o array | Numeri dei destinatari, fino a 1.000. Ogni elemento può anche essere un oggetto { "to": "+1415...", "vars": ["Alice"] } i cui vars compilano {{1}}, {{2}} nel testo |
| text | stringa | Testo del messaggio, fino a 10 parti per destinatario |
| name | stringa, facoltativo | Un nome per l'invio, mostrato nella tua cronologia (80 caratteri) |
| schedule_at | stringa, facoltativo | Orario di inizio ISO 8601, da 5 minuti a 29 giorni nel futuro. Addebitato subito; annulla prima che inizi per un rimborso completo |
| opt_out | booleano, facoltativo | true aggiunge "Reply STOP to opt out" su una nuova riga alla fine |
| ref | stringa, facoltativo | Un tuo riferimento, restituito nei webhook (64 caratteri) |
curl https://portsms.com/api/v1/messages \
-H "Authorization: Bearer $PORTSMS_KEY" \
-H "Content-Type: application/json" \
-d '{"to": ["+14155550123", "+447700900123"],
"text": "Your code is 482913",
"name": "Login codes",
"ref": "order-1042"}'// One text, filled in per recipient, sent tomorrow at 09:00 UTC with an opt-out line
{"to": [{"to": "+14155550123", "vars": ["Alice", "A-1042"]},
{"to": "+447700900123", "vars": ["Ben", "A-1043"]}],
"text": "Hi {{1}}, your order {{2}} has shipped.",
"schedule_at": "2026-10-14T09:00:00Z",
"opt_out": true}HTTP/1.1 201 Created
{
"campaign": {
"id": "c_7kq2m9xd4tpa", "name": "Login codes", "source": "api",
"text": "Your code is 482913", "encoding": "gsm7", "parts": 1,
"recipients": 2, "cost": "0.0440", "refunded": "0.0000",
"status": "sending", "ref": "order-1042", "scheduled_at": null,
"created_at": "2026-10-13T09:30:00.000Z", "done_at": null
},
"messages": [
{ "id": "m_3hx8q2vkd7mrtc", "campaign_id": "c_7kq2m9xd4tpa", "to": "+14155550123",
"country": "US", "status": "queued", "error": null, "parts": 1, "price": "0.0090",
"created_at": "2026-10-13T09:30:00.000Z", "sent_at": null, "done_at": null }
],
"rejected": []
}// Node.js 18+
const res = await fetch("https://portsms.com/api/v1/messages", {
method: "POST",
headers: { Authorization: "Bearer " + process.env.PORTSMS_KEY, "Content-Type": "application/json" },
body: JSON.stringify({ to: ["+14155550123"], text: "Your code is 482913" }),
});
const data = await res.json();# Python 3 with requests
import os, requests
r = requests.post("https://portsms.com/api/v1/messages",
headers={"Authorization": "Bearer " + os.environ["PORTSMS_KEY"]},
json={"to": ["+14155550123"], "text": "Your code is 482913"})
print(r.status_code, r.json())Stima prima dell'invio
Stesso corpo dell'invio. Restituisce codifica, parti, costo e i numeri che verrebbero rifiutati, senza inviare nulla.
POST/api/v1/estimate
{ "encoding": "gsm7", "characters": 19, "parts": 1, "recipients": 2,
"cost": "0.0440", "balance": "120.00",
"rejected": [ { "to": "4155550123", "reason": "format" } ] }Controllare un messaggio
Stato di un singolo destinatario, tramite l'id restituito al momento dell'invio.
GET/api/v1/messages/{id}
{ "message": { "id": "m_3hx8q2vkd7mrtc", "to": "+14155550123", "status": "delivered",
"error": null, "price": "0.0090", "done_at": "2026-10-13T09:30:06.000Z", ... } }Campagne
Ogni richiesta di invio è una campagna con stato scheduled, sending, done o canceled. Elenca le ultime 100, aprine una per vedere i conteggi per stato e ogni destinatario (500 per pagina, ?page=2 per gli altri), oppure annulla una campagna programmata con POST /api/v1/campaigns/{id}/cancel per un rimborso completo.
GET/api/v1/campaigns
GET/api/v1/campaigns/{id}?page=1
POST/api/v1/campaigns/{id}/cancel
{ "campaign": { "id": "c_7kq2m9xd4tpa", "status": "done", "recipients": 2,
"counts": { "queued": 0, "sent": 0, "delivered": 1, "failed": 1 }, ... },
"messages": [ ... ], "page": 1, "page_size": 500 }Saldo e prezzi
Il tuo saldo in dollari USA e il prezzo per parte di ogni paese in vendita.
GET/api/v1/balance
GET/api/v1/prices
{ "balance": "120.00", "currency": "USD", "plan": "basic", "discount_percent": 3 }
{ "prices": [ { "country": "US", "name": "United States", "dial_code": "+1", "price_per_part": "0.0090" }, ... ] }Stati dei messaggi
| queued | Accettato e addebitato, in attesa di essere passato alla rotta dell'operatore |
| sent | Passato alla rotta dell'operatore, in attesa del rapporto di consegna |
| delivered | L'operatore ha confermato la consegna al telefono |
| failed | Rifiutato o non recapitabile. Il prezzo è stato rimborsato sul tuo saldo |
Webhook di consegna
Imposta un indirizzo https nella pagina della chiave API. Quando un messaggio diventa delivered o failed, lo inviamo in POST come JSON. Riproviamo fino a 8 volte in circa due ore, finché il tuo server non risponde con uno stato 2xx.
POST https://your-server.example/portsms
X-PortSMS-Signature: t=1791883806,v1=5f2c...e91a
Content-Type: application/json
{ "event": "sms.status",
"data": { "id": "m_3hx8q2vkd7mrtc", "campaign_id": "c_7kq2m9xd4tpa", "to": "+14155550123",
"country": "US", "status": "delivered", "error": null, "parts": 1, "price": "0.0090",
"ref": "order-1042", "done_at": "2026-10-13T09:30:06.000Z", ... } }Verifica l'header X-PortSMS-Signature: t è un orario Unix e v1 è l'HMAC-SHA256 di "t.body" con il tuo segreto di firma. Rifiuta le richieste più vecchie di cinque minuti.
// Node.js: verify before trusting the body
import crypto from "node:crypto";
function verify(raw, 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] + "." + raw).digest("hex");
return crypto.timingSafeEqual(Buffer.from(want), Buffer.from(m[2]));
}Errori
Gli errori vengono restituiti come JSON con un codice error e uno stato HTTP.
HTTP/1.1 402 Payment Required
{ "error": "insufficient_balance", "cost": "44.0000" }| bad_key | 401 | La chiave manca o è errata |
| no_deposit | 403 | L'API si attiva dopo l'accredito della prima ricarica |
| insufficient_balance | 402 | Il saldo è inferiore al costo (il costo è incluso) |
| no_recipients | 400 | Nessun numero è stato accettato (rejected indica il motivo) |
| too_many_recipients | 413 | Più di 1.000 numeri in una sola richiesta |
| empty_text | 400 | Il testo è vuoto |
| text_too_long | 400 | Il testo richiede più di 10 parti |
| bad_schedule | 400 | schedule_at non è un orario compreso tra 5 minuti e 29 giorni nel futuro |
| daily_limit | 429 | È stato raggiunto il limite di messaggi nelle 24 ore per l'account |
| rate_limited | 429 | Più di 300 chiamate in un minuto |
| not_found | 404 | Messaggio, campagna o indirizzo inesistente |
| not_scheduled | 409 | Si può annullare solo una campagna programmata che non è ancora iniziata |
Perché un numero viene rifiutato
| format | Non è in formato internazionale, oppure è troppo corto o troppo lungo |
| country | Un paese che non serviamo |
| price | Un paese che al momento non è in vendita |
| duplicate | Lo stesso numero compare due volte nella stessa richiesta |
| too_long | Con i suoi vars compilati, il testo supera le 10 parti |