PortSMS se trouve à l'adresse portsms.com. Votre adresse de recharge personnelle vous attend sur la page Solde une fois connecté.

PortSMS

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/v1
Authorization: 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

tochaîne ou tableauNumé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
textchaîneTexte du message, jusqu'à 10 parties par destinataire
namechaîne, facultatifUn nom pour l'envoi, affiché dans votre historique (80 caractères)
schedule_atchaîne, facultatifHeure 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_outbooléen, facultatiftrue ajoute "Reply STOP to opt out" sur une nouvelle ligne à la fin
refchaîne, facultatifVotre 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

queuedAccepté et facturé, en attente de transmission à la route opérateur
sentTransmis à la route opérateur, en attente d'un accusé de livraison
deliveredL'opérateur a confirmé la livraison sur le téléphone
failedRefusé 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_key401La clé est absente ou incorrecte
no_deposit403L'API s'ouvre une fois votre première recharge créditée
insufficient_balance402Le solde est inférieur au coût (le coût est indiqué)
no_recipients400Aucun numéro n'a pu être accepté (rejected indique pourquoi)
too_many_recipients413Plus de 1 000 numéros dans une même requête
empty_text400Le texte est vide
text_too_long400Le texte nécessite plus de 10 parties
bad_schedule400schedule_at n'est pas une heure comprise entre 5 minutes et 29 jours à l'avance
daily_limit429La limite de messages sur 24 heures du compte est atteinte
rate_limited429Plus de 300 appels en une minute
not_found404Message, campagne ou adresse introuvable
not_scheduled409Seule une campagne programmée qui n'a pas encore démarré peut être annulée

Pourquoi un numéro est refusé

formatPas au format international, ou trop court ou trop long
countryUn pays que nous ne desservons pas
priceUn pays qui n'est pas disponible actuellement
duplicateLe même numéro apparaît deux fois dans une même requête
too_longUne fois ses vars remplies, le texte dépasse 10 parties