Référence de l'API SMS
JSON sur HTTPS. Créez une clé sur la page Clé API après votre première recharge, puis envoyez-la comme jeton Bearer.
Adresse de base et clés
Tous les appels se font à l'adresse ci-dessous. Envoyez votre clé dans l'en-tête Authorization. Toute personne disposant de la clé peut dépenser votre solde : conservez-la uniquement sur votre serveur.
https://portsms.com/api/v1Authorization: Bearer psm_your_api_key Content-Type: application/json
Limites : 300 appels par minute et par compte, 1 000 destinataires par requête, 50,000 messages par compte sur 24 heures.
Envoyer un message
Envoie un texte à un ou plusieurs numéros. Les numéros doivent être au format international (+ et l'indicatif du pays). Le coût total est débité de votre solde dès que la requête est acceptée ; les numéros que nous ne pouvons pas desservir sont renvoyés dans rejected et ne sont pas facturés.
POST/api/v1/messages
| to | chaîne ou tableau | Numéros des destinataires, jusqu'à 1 000. Chaque élément peut aussi être un objet { "to": "+1415...", "vars": ["Alice"] } dont les vars remplissent {{1}}, {{2}} dans le texte |
| text | chaîne | Texte du message, jusqu'à 10 parties par destinataire |
| name | chaîne, facultatif | Un nom pour l'envoi, affiché dans votre historique (80 caractères) |
| schedule_at | chaîne, facultatif | Heure de démarrage au format ISO 8601, entre 5 minutes et 29 jours à l'avance. Débité tout de suite ; annulez avant le démarrage pour un remboursement intégral |
| opt_out | booléen, facultatif | true ajoute "Reply STOP to opt out" sur une nouvelle ligne à la fin |
| ref | chaîne, facultatif | Votre propre référence, renvoyée dans les webhooks (64 caractères) |
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())Estimer avant d'envoyer
Même corps que pour l'envoi. Renvoie l'encodage, le nombre de parties, le coût et les numéros qui seraient refusés, sans rien envoyer.
POST/api/v1/estimate
{ "encoding": "gsm7", "characters": 19, "parts": 1, "recipients": 2,
"cost": "0.0440", "balance": "120.00",
"rejected": [ { "to": "4155550123", "reason": "format" } ] }Consulter un message
Statut d'un destinataire, à partir de l'id renvoyé lors de l'envoi.
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", ... } }Campagnes
Chaque requête d'envoi constitue une campagne, avec le statut scheduled, sending, done ou canceled. Listez vos 100 dernières, ouvrez-en une pour voir le nombre de messages par statut et chaque destinataire (500 par page, ?page=2 pour la suite), ou annulez une campagne programmée avec POST /api/v1/campaigns/{id}/cancel pour un remboursement intégral.
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 }Solde et tarifs
Votre solde en dollars américains et le prix par partie pour chaque pays disponible.
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" }, ... ] }Statuts des messages
| queued | Accepté et facturé, en attente de transmission à la route opérateur |
| sent | Transmis à la route opérateur, en attente d'un accusé de livraison |
| delivered | L'opérateur a confirmé la livraison sur le téléphone |
| failed | Refusé ou impossible à livrer. Le prix a été remboursé sur votre solde |
Webhooks de livraison
Indiquez une adresse https sur la page Clé API. Lorsqu'un message passe à delivered ou failed, nous l'envoyons en POST au format JSON. Nous réessayons jusqu'à 8 fois sur environ deux heures, jusqu'à ce que votre serveur réponde avec un statut 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", ... } }Vérifiez l'en-tête X-PortSMS-Signature : t est un horodatage Unix et v1 est le HMAC-SHA256 de "t.body" calculé avec votre secret de signature. Rejetez les requêtes de plus de cinq minutes.
// 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]));
}Erreurs
Les erreurs sont renvoyées en JSON avec un code d'erreur et un statut HTTP.
HTTP/1.1 402 Payment Required
{ "error": "insufficient_balance", "cost": "44.0000" }| bad_key | 401 | La clé est absente ou incorrecte |
| no_deposit | 403 | L'API s'ouvre une fois votre première recharge créditée |
| insufficient_balance | 402 | Le solde est inférieur au coût (le coût est indiqué) |
| no_recipients | 400 | Aucun numéro n'a pu être accepté (rejected indique pourquoi) |
| too_many_recipients | 413 | Plus de 1 000 numéros dans une même requête |
| empty_text | 400 | Le texte est vide |
| text_too_long | 400 | Le texte nécessite plus de 10 parties |
| bad_schedule | 400 | schedule_at n'est pas une heure comprise entre 5 minutes et 29 jours à l'avance |
| daily_limit | 429 | La limite de messages sur 24 heures du compte est atteinte |
| rate_limited | 429 | Plus de 300 appels en une minute |
| not_found | 404 | Message, campagne ou adresse introuvable |
| not_scheduled | 409 | Seule une campagne programmée qui n'a pas encore démarré peut être annulée |
Pourquoi un numéro est refusé
| format | Pas au format international, ou trop court ou trop long |
| country | Un pays que nous ne desservons pas |
| price | Un pays qui n'est pas disponible actuellement |
| duplicate | Le même numéro apparaît deux fois dans une même requête |
| too_long | Une fois ses vars remplies, le texte dépasse 10 parties |