Wysyłanie SMS w PHP, Pythonie i Node.js: gotowe przykłady z własnym numerem
Jak wysłać SMS w PHP, Pythonie i Node.js przez API z własnego numeru: gotowy kod, obsługa błędów, ponawianie i sprawdzanie statusu dostarczenia.

Jeśli szukasz sposobu na wysyłanie SMS w PHP, Pythonie lub Node.js bez płacenia za każdą wiadomość, API smsportal przyjmuje zwykłe żądanie HTTP i wysyła SMS z Twojego numeru przez podłączony telefon z Androidem. Poniżej gotowe funkcje w trzech językach: wysyłka, obsługa błędów, ponawianie i sprawdzanie statusu. Konfigurację telefonu i klucza opisuje pełny przewodnik po SMS API, tu skupiamy się na kodzie.
Co jest potrzebne do wysłania SMS z kodu?
Potrzebujesz klucza API i podłączonego telefonu. Cała reszta to jedno żądanie POST. Adres bazowy to https://smsportal.app/api/v1, a klucz idzie w nagłówku x-api-key.
- Załóż konto w smsportal i połącz telefon z Androidem (kod QR).
- W panelu wygeneruj klucz API.
- Zapisz go w zmiennej środowiskowej:
export SMSPORTAL_API_KEY="wklej_tutaj_klucz"
Uwaga: nie ma oficjalnej paczki SDK na npm, Packagist ani PyPI. Używamy zwykłego HTTP. Dzięki temu nie dokładasz zależności, a przykłady działają w każdej wersji języka.
Jak wygląda minimalne żądanie w cURL?
Najkrótsza wersja to jedno polecenie curl. Pozwala sprawdzić klucz i telefon, zanim napiszesz kod.
curl -X POST "https://smsportal.app/api/v1/gateway/send-sms" \
-H "x-api-key: $SMSPORTAL_API_KEY" \
-H "Content-Type: application/json" \
-d '{"recipients": ["+48600100200"], "message": "Test z terminala"}'Odpowiedź 200 zawiera data.smsBatchId. Ten identyfikator posłuży do sprawdzenia statusu. Numery podawaj w formacie międzynarodowym (+48...).
Jak wysłać SMS w PHP?
W PHP użyj rozszerzenia cURL. Poniższa funkcja wysyła SMS, sprawdza kod HTTP i zwraca smsBatchId albo rzuca wyjątek z treścią błędu.
<?php
const SMSPORTAL_API = 'https://smsportal.app/api/v1';
function sendSms(array $recipients, string $message, ?string $scheduledAt = null): string
{
$payload = ['recipients' => $recipients, 'message' => $message];
if ($scheduledAt !== null) {
$payload['scheduledAt'] = $scheduledAt; // ISO 8601, w przyszłości
}
$ch = curl_init(SMSPORTAL_API . '/gateway/send-sms');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
CURLOPT_HTTPHEADER => [
'x-api-key: ' . getenv('SMSPORTAL_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode($payload),
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($body === false) {
throw new RuntimeException("Błąd sieci: $error");
}
if ($status !== 200) {
throw new RuntimeException("HTTP $status: $body", $status);
}
return json_decode($body, true)['data']['smsBatchId'];
}
$batchId = sendSms(['+48600100200'], 'Samochod gotowy do odbioru. Warsztat Kowalski.');
echo $batchId;Dla PHP nie musisz instalować Guzzle. Jeśli jednak już go używasz, ten sam schemat sprowadza się do $client->post($url, ['headers' => [...], 'json' => [...]]).
Jak wysłać SMS w Pythonie?
W Pythonie najwygodniejsza jest biblioteka requests (pip install requests). Funkcja niżej ma timeout, kontrolę kodu odpowiedzi i ponawianie błędów chwilowych.
import os
import time
import requests
API = "https://smsportal.app/api/v1"
HEADERS = {"x-api-key": os.environ["SMSPORTAL_API_KEY"]}
def send_sms(recipients, message, scheduled_at=None, retries=3):
payload = {"recipients": recipients, "message": message}
if scheduled_at:
payload["scheduledAt"] = scheduled_at # ISO 8601, w przyszłości
for attempt in range(retries):
try:
res = requests.post(
f"{API}/gateway/send-sms",
headers=HEADERS, json=payload, timeout=15,
)
except requests.RequestException:
time.sleep(2 ** attempt) # błąd sieci: ponów
continue
if res.status_code == 200:
return res.json()["data"]["smsBatchId"]
if res.status_code >= 500:
time.sleep(2 ** attempt) # błąd serwera: ponów
continue
# 400, 401, 429: ponawianie nic nie zmieni
raise RuntimeError(f"HTTP {res.status_code}: {res.text}")
raise RuntimeError("Nie udało się wysłać SMS po kilku próbach")
batch_id = send_sms(["+48600100200"], "Wizyta jutro o 10:00. Odpisz TAK.")
print(batch_id)Dla FastAPI czy Django wywołanie wrzuć do zadania w tle (Celery, RQ), żeby żądanie użytkownika nie czekało na sieć.
Jak wysłać SMS w Node.js?
W Node.js 18 i nowszym masz wbudowany fetch, więc nie instalujesz niczego. Poniższa funkcja ma ten sam kontrakt co wersje powyżej.
const API = 'https://smsportal.app/api/v1'
const headers = {
'x-api-key': process.env.SMSPORTAL_API_KEY,
'Content-Type': 'application/json',
}
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
export async function sendSms(recipients, message, scheduledAt, retries = 3) {
const payload = { recipients, message, ...(scheduledAt && { scheduledAt }) }
for (let attempt = 0; attempt < retries; attempt++) {
let res
try {
res = await fetch(`${API}/gateway/send-sms`, {
method: 'POST',
headers,
body: JSON.stringify(payload),
signal: AbortSignal.timeout(15000),
})
} catch {
await sleep(2 ** attempt * 1000) // błąd sieci: ponów
continue
}
if (res.ok) return (await res.json()).data.smsBatchId
if (res.status >= 500) {
await sleep(2 ** attempt * 1000) // błąd serwera: ponów
continue
}
// 400, 401, 429: ponawianie nic nie zmieni
throw new Error(`HTTP ${res.status}: ${await res.text()}`)
}
throw new Error('Nie udało się wysłać SMS po kilku próbach')
}
console.log(await sendSms(['+48600100200'], 'Zamowienie 1042 wyslane.'))Które błędy ponawiać, a których nie?
Ponawiaj tylko błędy chwilowe: brak połączenia, timeout i kody 5xx. Pozostałe wymagają poprawki po Twojej stronie. Ślepe ponawianie 400 lub 429 nic nie da, a przy timeoucie grozi duplikatem SMS-a.
| Kod | Znaczenie | Ponawiać? |
|---|---|---|
| 200 | Zlecenie przyjęte | Nie, zapisz smsBatchId |
| 400 | Zły numer lub treść, brak włączonego urządzenia, zły scheduledAt | Nie, popraw dane |
| 401 | Błędny lub unieważniony klucz | Nie, sprawdź x-api-key |
| 429 | Wyczerpany limit planu (dzienny, miesięczny lub jednorazowy) | Nie od razu |
| 5xx, timeout | Błąd serwera lub sieci | Tak, z rosnącą przerwą |
Wskazówka: po timeoucie żądanie mogło jednak dojść do serwera. Zanim ponowisz, sprawdź
GET /gateway/messages?search=...albo listę z ostatnich minut, żeby klient nie dostał dwóch identycznych SMS-ów.
Jak sprawdzić status SMS-a w kodzie?
Użyj GET /gateway/messages z smsBatchId zwróconym przy wysyłce. Pole status przechodzi z pending przez dispatched i sent do delivered albo failed.
import os
import requests
res = requests.get(
"https://smsportal.app/api/v1/gateway/messages",
headers={"x-api-key": os.environ["SMSPORTAL_API_KEY"]},
params={"smsBatchId": batch_id},
timeout=15,
)
for m in res.json()["data"]:
print(m["recipient"], m["status"])To samo w PHP i Node.js to zwykły GET z parametrem w adresie:
const url = new URL('https://smsportal.app/api/v1/gateway/messages')
url.searchParams.set('smsBatchId', batchId)
const res = await fetch(url, { headers: { 'x-api-key': process.env.SMSPORTAL_API_KEY } })
const { data } = await res.json()
data.forEach((m) => console.log(m.recipient, m.status))W panelu te wartości widać jako etykiety: w kolejce (pending i dispatched), wysłana (sent), dostarczona (delivered) i nieudana (failed). Dodanie status=failed zwraca tylko wiadomości nieudane z danej partii. Status sent oznacza, że telefon przekazał SMS do sieci, a delivered dopiero potwierdzenie operatora. Nie wszystkie sieci zwracają potwierdzenia, więc brak delivered nie zawsze znaczy błąd.
W produkcji nie odpytuj API w pętli. Skonfiguruj webhook na zdarzenia MESSAGE_SENT, MESSAGE_DELIVERED i MESSAGE_FAILED, a serwer dostanie powiadomienie od razu. Weryfikację podpisu X-Signature pokazuje artykuł odbieranie SMS przez API i webhook.
Jak wysłać wiele wiadomości i zaplanować wysyłkę?
Dla wielu odbiorców z różnym tekstem użyj POST /gateway/send-bulk-sms, a dla późniejszej godziny dodaj scheduledAt. W obu przypadkach schemat żądania zostaje ten sam.
const res = await fetch('https://smsportal.app/api/v1/gateway/send-bulk-sms', {
method: 'POST',
headers,
body: JSON.stringify({
messages: [
{ recipients: ['+48600100200'], message: 'Anna, wizyta jutro o 10:00.' },
{ recipients: ['+48601200300'], message: 'Piotr, wizyta jutro o 11:30.' },
],
}),
})W telefonie z dwiema kartami SIM dodaj simSubscriptionId, by wybrać kartę. Bez niego używana jest domyślna karta telefonu. Wysyłkę z pliku CSV ze zmiennymi opisuje artykuł o masowej wysyłce SMS z CSV lub Excela.
Ile SMS-ów na minutę wyśle skrypt?
Skrypt może wysłać żądanie szybko, ale telefon wysyła wiadomości z przerwą, domyślnie 5 sekund. To około 12 SMS-ów na minutę z jednego telefonu. Serwer ustawia większe partie w kolejce i wypuszcza je falami dopasowanymi do tej przerwy.
Nie ma sensu odpalać tysięcy żądań w równoległych wątkach. Dla większego wolumenu podłącz kilka telefonów. Do kampanii masowych na tysiące odbiorców lepsza jest hurtowa bramka SMS. Polskie litery skracają segment z 160 do 70 znaków (kodowanie UCS-2), więc w kodzie warto pilnować długości tekstu.
Jak wpiąć wysyłkę w Laravel, Django i Express?
Wywołanie API wydziel do jednej klasy lub funkcji i uruchamiaj z kolejki zadań, nie z kontrolera. Dzięki temu timeout sieci nie blokuje odpowiedzi użytkownikowi, a ponowienia robi worker.
Laravel (klient HTTP wbudowany we framework, zadanie w kolejce):
<?php
use Illuminate\Support\Facades\Http;
class SendSmsJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable;
public int $tries = 3;
public function backoff(): array { return [5, 30, 120]; }
public function __construct(private array $to, private string $text) {}
public function handle(): void
{
$res = Http::withHeaders(['x-api-key' => config('services.smsportal.key')])
->timeout(15)
->post('https://smsportal.app/api/v1/gateway/send-sms', [
'recipients' => $this->to,
'message' => $this->text,
]);
if ($res->serverError()) { $res->throw(); } // ponów
if ($res->failed()) { $this->fail(new Exception($res->body())); } // nie ponawiaj
}
}Django (zadanie Celery z funkcją send_sms z wcześniejszego przykładu):
from celery import shared_task
@shared_task(bind=True, max_retries=3)
def send_sms_task(self, recipients, message):
try:
return send_sms(recipients, message)
except RuntimeError as exc:
raise self.retry(exc=exc, countdown=30)Express (endpoint, który odpowiada od razu, a wysyła w tle):
app.post('/orders/:id/notify', async (req, res) => {
res.status(202).json({ queued: true })
sendSms([req.body.phone], `Zamowienie ${req.params.id} wyslane.`)
.catch((err) => console.error('SMS nie wyszedł:', err.message))
})Uwaga: przy produkcyjnych wysyłkach zapisuj
smsBatchIdw bazie przy zamówieniu lub wizycie. Gdy klient zapyta „czy dostałem SMS?”, odpowiesz jednym zapytaniem.
Jak przetestować integrację przed wdrożeniem?
Testuj w trzech krokach: pierwsze żądanie na własny numer, test błędów i test z wyłączonym telefonem. Dzięki temu zobaczysz każdy status, zanim zobaczy go klient.
- Na własny numer. Wyślij wiadomość do siebie i sprawdź w aplikacji oś czasu: zlecona, w kolejce, wysłana, dostarczona.
- Zły klucz i pusta treść. Podmień
x-api-keyna losowy ciąg (oczekuj 401), a potem wyślij pustymessage(oczekuj 400). Upewnij się, że kod nie ponawia tych błędów. - Telefon offline. Wyłącz internet w telefonie, wyślij SMS i obserwuj status
pending. Po włączeniu internetu powinien przejść dosent.
W testach automatycznych nie wysyłaj prawdziwych SMS-ów. Podmień funkcję sendSms na atrapę (mock) i sprawdzaj, że dostaje poprawny numer i treść, a w testach „wyjątków” zwracaj kody 401 i 500.
Co robić, gdy wiadomość nie dochodzi?
Zacznij od statusu z API, bo wskazuje, na którym etapie utknęła wiadomość. Większość problemów to telefon, nie kod.
| Status utknął na | Prawdopodobna przyczyna | Co zrobić |
|---|---|---|
pending | Telefon offline lub uśpiona aplikacja | Włącz internet, wyłącz optymalizację baterii |
sent, brak delivered | Sieć nie zwraca raportów doręczenia | Zwykle brak błędu, wiadomość mogła dojść |
failed | Zły numer, brak środków lub zasięgu | Sprawdź format +48... i kartę SIM |
| HTTP 400 | Brak włączonego urządzenia | Włącz telefon w panelu |
Co dalej: webhooki, automatyzacje i agenci AI
Gdy wysyłka działa, dodaj odbieranie odpowiedzi klientów. Odpowiedź („TAK” na przypomnienie) trafia na webhook jako MESSAGE_RECEIVED.
- Odbiór odpowiedzi: SMS przez API i webhook.
- Bez kodu: SMS w n8n.
- Asystent AI wysyła SMS-y sam: serwer MCP, a konfigurację znajdziesz w integracji MCP.
Pełne wymagania pól sprawdzisz w dokumentacji API. Aby zacząć, załóż bezpłatne konto i wyślij pierwszy SMS ze swojego telefonu na własny numer.
Najczęściej zadawane pytania
Jak wysłać SMS w PHP?
Użyj cURL wbudowanego w PHP: wyślij POST na /gateway/send-sms z nagłówkami x-api-key i Content-Type: application/json oraz ciałem z json_encode zawierającym recipients i message. Sprawdź kod HTTP przez curl_getinfo, bo 200 oznacza przyjęcie zlecenia.
Jak wysłać SMS w Pythonie za darmo?
Wiadomość wysyłasz biblioteką requests przez API smsportal, a koszt SMS-a to koszt w Twoim abonamencie komórkowym, często zerowy przy nielimitowanych SMS-ach. Dostęp do API ma też bezpłatny plan na start. Nie ma w pełni darmowych bramek, bo za dostarczenie zawsze ktoś płaci.
Czy jest oficjalna biblioteka SMS dla Node.js, PHP lub Pythona?
Nie, smsportal nie ma oficjalnej paczki na npm, Packagist ani PyPI. API to zwykły HTTP z kluczem w nagłówku, więc wystarczą fetch, requests lub cURL. Dwadzieścia linii własnej funkcji zastępuje zewnętrzną zależność.
Jak ponowić wysyłkę SMS po błędzie?
Ponawiaj tylko błędy chwilowe: timeout, błędy sieci i kody 5xx, z rosnącą przerwą. Nie ponawiaj kodów 400 i 401, bo to błąd danych lub klucza, ani 429, który oznacza wyczerpany limit planu. Przed ponowieniem sprawdź listę wiadomości, żeby nie wysłać SMS-a dwa razy.
Jak sprawdzić status SMS-a w kodzie?
Wyślij GET na /gateway/messages z parametrem smsBatchId zwróconym przy wysyłce. Pole status ma wartości pending, dispatched, sent, delivered lub failed. Zamiast odpytywać w pętli, możesz odbierać zdarzenia MESSAGE_DELIVERED i MESSAGE_FAILED webhookiem.


