Wysylka SMS przez proste API HTTP. Ten dokument opisuje wszystko, czego potrzebujesz, zeby zintegrowac swoja aplikacje z bramka.
https://sms.inonet.plDo korzystania z bramki potrzebujesz klucza API. Klucz wystawia administrator bramki i przekazuje go Tobie. Wyglada tak:
smsb_PrZyKlAdOwYkLuCz_nIe_DzIaLa_ZaStAp_wLaSnYm
Klucz jest pokazywany tylko raz, w chwili wygenerowania - bramka przechowuje wylacznie jego skrot i nie potrafi go odtworzyc. Jesli go zgubisz, administrator musi wystawic nowy. Nigdy nie umieszczaj klucza w kodzie strony po stronie przegladarki ani w publicznym repozytorium - trzymaj go w zmiennej srodowiskowej albo w pliku konfiguracyjnym serwera.
Kazde zapytanie do API wysylasz z naglowkiem X-API-Key. Najszybszy test,
ktory sprawdza, czy klucz dziala (nie wysyla zadnego SMS-a):
curl -s https://sms.inonet.pl/api/sms/history?limit=1 \
-H "X-API-Key: smsb_TWOJ_KLUCZ_API"
Poprawna odpowiedz (nawet gdy nie wyslales jeszcze nic) wyglada tak:
{"items": [], "total": 0, "limit": 1, "offset": 0}
Jesli zamiast tego dostajesz 401, klucz jest zly, odwolany albo wygasl.
Jesli 403 - klucz jest poprawny, ale Twoje konto zostalo wylaczone.
Wszystkie endpointy klienckie (/api/sms/*) uwierzytelnia sie
wylacznie naglowkiem:
X-API-Key: smsb_TWOJ_KLUCZ_API
Authorization: Bearer - ten naglowek obsluguje panel
administracyjny, nie API klienckie.x-api-key zadziala
tak samo), ale sama wartosc klucza owszem.Content-Type: application/json.To, z ktorej karty SIM wyjdzie SMS, ustala administrator bramki przy Twoim koncie. W zadaniu nie da sie wskazac routera ani numeru nadawcy - ewentualne dodatkowe pola w JSON-ie sa po prostu ignorowane. Jesli potrzebujesz zmiany numeru nadawcy, zglos to administratorowi.
Endpoint dodaje wiadomosc do kolejki i natychmiast zwraca
odpowiedz. Nie czeka na faktyczne nadanie SMS-a - tym zajmuje sie proces w tle,
zwykle w ciagu kilku sekund. Odpowiedz 201 znaczy
"przyjeto do kolejki", a nie "dostarczono".
| Pole | Typ | Wymagane | Opis |
|---|---|---|---|
recipient |
string | tak | Numer odbiorcy. Format E.164 z prefiksem kraju, np. +48123456789.
Spacje, myslniki, nawiasy, kropki i ukosniki sa usuwane automatycznie, wiec
+48 123-456-789 jest rownowazne +48123456789.
Pole przyjmuje tez nazwy zamienne: phone, number,
to - dziala dokladnie tak samo. |
message |
string | tak | Tresc wiadomosci, od 1 do 1600 znakow. Nie moze skladac sie z samych bialych znakow. Dluzsza tresc rozbija sie u operatora na kilka SMS-ow. |
priority |
integer | nie | Od 1 do 9, domyslnie 5.
1 = najwyzszy priorytet, 9 = najnizszy. Wiadomosci o nizszej
liczbie sa zabierane z kolejki wczesniej. Wartosc spoza zakresu to blad 422. |
scheduled_at |
string (ISO 8601) | nie | Termin wysylki. Pominiecie pola (albo null) = wyslij natychmiast.
Szczegoly nizej. |
scheduled_at - strefa czasowa i zakres2026-08-01T09:00:00+02:00 albo
2026-08-01T07:00:00Z) jest brana doslownie.2026-08-01T09:00:00) jest
interpretowana jako czas lokalny Europe/Warsaw - nie jako UTC.curl -s -X POST https://sms.inonet.pl/api/sms/send \
-H "X-API-Key: smsb_TWOJ_KLUCZ_API" \
-H "Content-Type: application/json" \
-d '{
"recipient": "+48123456789",
"message": "Przypominamy o wizycie jutro o 10:00.",
"priority": 5
}'
{
"id": 140,
"status": "pending",
"recipient": "+48123456789",
"priority": 5,
"scheduled_at": "2026-07-25T10:46:45.271058+02:00",
"created_at": "2026-07-25T10:46:45.271058+02:00"
}
id
Pole id to jedyny sposob, zeby pozniej sprawdzic los tej konkretnej
wiadomosci przez GET /api/sms/status/{id}. Zapisz je u siebie razem
z rekordem, ktorego wiadomosc dotyczy.
Zadanie z terminem wysylki - odpowiedz pokazuje date juz ze strefa:
curl -s -X POST https://sms.inonet.pl/api/sms/send \
-H "X-API-Key: smsb_TWOJ_KLUCZ_API" \
-H "Content-Type: application/json" \
-d '{
"recipient": "+48123456789",
"message": "Termin przegladu uplywa za tydzien.",
"priority": 7,
"scheduled_at": "2026-10-01T09:00:00"
}'
# odpowiedz:
# {"id":141,"status":"pending","recipient":"+48123456789","priority":7,
# "scheduled_at":"2026-10-01T09:00:00+02:00",
# "created_at":"2026-07-25T10:47:07.746519+02:00"}
Zwraca aktualny stan wiadomosci o podanym id (tym z odpowiedzi
POST /api/sms/send). Widzisz wylacznie wlasne
wiadomosci - cudze id daje 404, tak samo jak nieistniejace.
curl -s https://sms.inonet.pl/api/sms/status/140 \
-H "X-API-Key: smsb_TWOJ_KLUCZ_API"
{
"id": 140,
"recipient": "+48123456789",
"message": "Przypominamy o wizycie jutro o 10:00.",
"status": "pending",
"priority": 5,
"attempts": 0,
"max_attempts": 3,
"last_error": null,
"scheduled_at": "2026-07-25T10:46:45.271058+02:00",
"sent_at": null,
"created_at": "2026-07-25T10:46:45.271058+02:00",
"updated_at": "2026-07-25T10:46:45.271058+02:00",
"router_id": 1,
"router_name": "RUT241 glowny"
}
| Status | Znaczenie | Co robisz |
|---|---|---|
| pending | Wiadomosc czeka w kolejce. Jesli ma ustawione scheduled_at
w przyszlosci, bedzie czekac az do tego terminu. |
Czekasz. Sprawdzisz pozniej. |
| processing | Wiadomosc jest wlasnie obslugiwana - trwa proba nadania. | Czekasz. To stan przejsciowy, trwa sekundy. |
| sent | Stan koncowy. Bramka potwierdzila nadanie SMS-a.
Pole sent_at zawiera moment nadania. |
Nic. Gotowe. |
| failed | Stan koncowy. Wysylka nie powiodla sie i to jest pewne -
SMS nie zostal nadany. Powod znajdziesz w
last_error, a liczbe wykorzystanych prob w
attempts / max_attempts. |
Mozesz bezpiecznie zlecic wysylke ponownie. |
| unknown | Stan koncowy, wynik NIEROZSTRZYGNIETY. Patrz osobna sekcja nizej - to najwazniejsza rzecz w tej dokumentacji. | Nie ponawiaj automatycznie. |
Pola attempts i max_attempts opisuja wewnetrzne
proby bramki, nie Twoje. Bramka sama powtarza wysylke po bledzie rozstrzygnietym,
do max_attempts razy. Dopiero po ich wyczerpaniu wiadomosc dostaje
status failed.
Rekordy zrealizowanych wysylek sa okresowo usuwane z kolejki. Po takim
sprzataniu GET /api/sms/status/{id} zwroci 404, mimo ze
wiadomosc zostala normalnie wyslana. Trwaly slad zostaje w
historii. Jesli potrzebujesz statusu na dluzej, odczytaj
go w ciagu kilku minut od wyslania i zapisz u siebie.
unknown - przeczytaj koniecznieunknown nie znaczy "blad". Znaczy:
bramka wywolala urzadzenie nadawcze, ale nie doczekala sie jednoznacznej
odpowiedzi - przekroczony czas oczekiwania, zerwane polaczenie, restart
procesu. SMS mogl zostac nadany i najczesciej faktycznie zostal.
Bo nie da sie tego zrobic bezpiecznie. Skoro nie wiadomo, czy SMS poszedl, automatyczna powtorka oznaczalaby ryzyko, ze odbiorca dostanie te sama wiadomosc dwa razy. Przy powiadomieniach o platnosciach, kodach jednorazowych czy potwierdzeniach zamowien to gorszy blad niz brak wiadomosci. Dlatego bramka celowo zatrzymuje sie i oddaje decyzje czlowiekowi.
{
"id": 143,
"recipient": "+48123456789",
"message": "przykladowa tresc",
"status": "unknown",
"priority": 5,
"attempts": 1,
"max_attempts": 3,
"last_error": "Wynik wysylki nierozstrzygniety - router nie odpowiedzial, SMS mogl zostac nadany. Rekord nie jest ponawiany automatycznie. Komunikat routera: Przekroczony czas oczekiwania na odpowiedz routera",
"scheduled_at": "2026-09-01T09:00:00+02:00",
"sent_at": null,
"created_at": "2026-07-25T10:47:36.834684+02:00",
"updated_at": "2026-07-25T10:47:55.748969+02:00",
"router_id": 1,
"router_name": "RUT241 glowny"
}
Zwroc uwage: sent_at jest null, a last_error
zawiera wyjasnienie doklejone przez bramke. To nie jest komunikat bledu w zwyklym
sensie - to opis niepewnosci.
unknown jako osobny przypadek, nie wrzucaj go
do jednego worka z failed. Kod w rodzaju
if status != "sent": wyslij_ponownie() to blad -
wygeneruje duplikaty.unknown
liczy sie do Twojego limitu dziennego tak samo jak wyslana -
bo najprawdopodobniej kosztowala. Patrz Limity dzienne.sent - poszlo. failed - nie poszlo, mozesz powtorzyc.
unknown - nie wiadomo, nie powtarzaj sam, zapytaj czlowieka.
Trwala historia zakonczonych wysylek - wylacznie Twoich.
W przeciwienstwie do /status/{id} wpisy w historii nie znikaja przy
sprzataniu kolejki. Wyniki sa posortowane malejaco po dacie
(najnowsze pierwsze).
| Parametr | Typ | Domyslnie | Opis |
|---|---|---|---|
limit | integer | 50 |
Ile pozycji zwrocic. Zakres 1-200. Wieksza wartosc daje blad 422. |
offset | integer | 0 |
Ile pozycji pominac (paginacja). Minimum 0. |
status | string | brak | Filtr statusu. Dozwolone tylko: sent,
failed, unknown. Wartosci pending
i processing nie sa dopuszczalne - historia
zawiera wylacznie wysylki zakonczone. Inna wartosc daje blad 400. |
date_from | string (ISO 8601) | brak | Od tej daty wlacznie. |
date_to | string (ISO 8601) | brak | Do tej daty wlacznie. date_from pozniejsze niz
date_to daje blad 400. |
curl -s -G https://sms.inonet.pl/api/sms/history \
-H "X-API-Key: smsb_TWOJ_KLUCZ_API" \
--data-urlencode "limit=10" \
--data-urlencode "offset=0" \
--data-urlencode "status=unknown" \
--data-urlencode "date_from=2026-07-01T00:00:00" \
--data-urlencode "date_to=2026-07-31T23:59:59"
{
"items": [
{
"id": 73,
"queue_id": null,
"recipient": "+48123456789",
"message": "przykladowa tresc",
"status": "unknown",
"attempts": 1,
"error": "Timeout routera",
"created_at": "2026-07-25T10:47:55.751554+02:00",
"router_id": 1,
"router_name": "RUT241 glowny"
}
],
"total": 1,
"limit": 10,
"offset": 0
}
Pole total to liczba wszystkich pozycji pasujacych do
filtrow, niezaleznie od limit. Kolejne strony pobierasz zwiekszajac
offset o limit, az offset >= total.
Uwaga: pole w historii nazywa sie error (a nie last_error,
jak w statusie), a queue_id bywa null, gdy rekord kolejki
zostal juz posprzatany.
| Kod | Znaczenie | Co zrobic |
|---|---|---|
201 |
Wiadomosc przyjeta do kolejki (to nie jest blad). | Zapisz id z odpowiedzi. |
400 |
Nieprawidlowy parametr filtra w /history - zly
status albo date_from pozniejsze niz
date_to. |
Popraw zapytanie. Nie ponawiaj bez zmiany. |
401 |
Brak naglowka X-API-Key, albo klucz jest nieprawidlowy,
odwolany lub wygasl. |
Sprawdz naglowek i klucz. Ponawianie nic nie da. |
403 |
Klucz jest poprawny, ale konto zostalo wylaczone. | Skontaktuj sie z administratorem bramki. |
404 |
Nie ma wiadomosci o podanym id - nie istnieje, nalezy do innego
konta albo rekord kolejki zostal posprzatany. |
Sprawdz historie zamiast statusu. |
422 |
Blad walidacji danych: zly numer, pusta lub za dluga tresc, priorytet spoza
1-9, scheduled_at w przeszlosci albo dalej niz 90 dni,
limit powyzej 200. |
Popraw dane. Szczegol jest w detail[].loc i
detail[].msg. |
429 |
Wyczerpany limit dzienny. Odpowiedz zawiera naglowek
Retry-After z liczba sekund do polnocy. |
Poczekaj tyle sekund, ile mowi Retry-After. |
500 |
Blad wewnetrzny bramki. Odpowiedz zawiera incident -
krotki identyfikator zdarzenia. |
Sprobuj ponownie za chwile. Przy powtorzeniu podaj administratorowi
wartosc incident. |
503 |
Urzadzenie nadawcze przypisane do konta jest niedostepne albo bramka nie ma skonfigurowanego zadnego aktywnego urzadzenia. Wiadomosc NIE zostala przyjeta i nie ma jej w kolejce. | Sprobuj ponownie pozniej. Jesli sie powtarza - zglos administratorowi. |
Bledy 400 / 401 / 403 / 404 maja proste, tekstowe detail:
{"detail": "Brak naglowka X-API-Key"}
Blad 422 zwraca liste problemow - jeden wpis na pole:
{
"detail": [
{
"type": "value_error",
"loc": ["body", "recipient"],
"msg": "Value error, nieprawidlowy numer telefonu (oczekiwane np. +48123456789)",
"input": "abc"
}
]
}
Bledy 429 i 503 maja detail w postaci obiektu z dodatkowymi danymi:
Retry-After: 47555
{
"detail": {
"message": "Dzienny limit wysylki zostal wyczerpany",
"daily_limit": 100,
"sent_today": 87,
"queued_today": 13,
"remaining_today": 0
}
}
{
"detail": {
"message": "Router przypisany do konta jest niedostepny, skontaktuj sie z administratorem",
"reason": "router_konta_niedostepny"
}
}
Pole reason przy 503 przyjmuje wartosci
router_konta_niedostepny, brak_routera_domyslnego albo
brak_aktywnego_routera. Rozpoznawaj przypadki po nim, a nie po tresci
message - tekst moze sie zmienic.
Kazde konto ma dzienny limit wiadomosci ustawiany przez administratora bramki. Konto moze tez nie miec limitu w ogole.
| Pozycja | Liczy sie? | Dlaczego |
|---|---|---|
| Wiadomosci ze statusem sent | TAK | Zostaly nadane. |
| Wiadomosci ze statusem unknown | TAK | Mogly zostac nadane, wiec mogly kosztowac. Pominiecie ich pozwalaloby przekroczyc oplacony limit. |
| Wiadomosci ze statusem failed | nie | Na pewno nie poszly. |
Wiadomosci czekajace w kolejce (pending, processing)
z terminem na dzis albo zaleglym |
TAK | Rezerwuja limit z gory - inaczej dalby sie obejsc jednym wrzutem tysiaca zadan do kolejki. |
Wiadomosci z scheduled_at na kolejna dobe lub pozniej |
nie (dzis) | Rozlicza sie z limitu tego dnia, w ktorym maja wyjsc. |
O polnocy czasu polskiego (strefa Europe/Warsaw,
z uwzglednieniem zmiany czasu letni/zimowy). Doba liczy sie wg zegara bramki,
nie wg zegara Twojego serwera - jesli Twoja aplikacja chodzi w UTC, granica doby
wypada dla Ciebie o 22:00 (czas letni) albo 23:00 (czas zimowy) UTC.
Retry-After
Odpowiedz 429 zawiera naglowek Retry-After z dokladna
liczba sekund do polnocy, wyliczona przez bramke. To jedyna wartosc, ktora nie
rozjedzie sie na zmianie czasu ani na rozjezdzie zegarow. Nie licz polnocy
samodzielnie.
daily_limit - Twoj limit dzienny,sent_today - ile juz poszlo dzisiaj (razem z unknown),queued_today - ile czeka w kolejce z terminem na dzisiaj,remaining_today - ile zostalo (przy 429 zawsze 0).Gdy limit jest wyczerpany, wiadomosc nie trafia do kolejki - nie zostanie wyslana pozniej sama z siebie. Musisz powtorzyc zadanie po polnocy.
Wiadomosc z scheduled_at na kolejna dobe nie jest sprawdzana
wzgledem limitu przy przyjmowaniu - dostaniesz 201 nawet wtedy,
gdy dzisiejszy limit jest juz wyczerpany. Limit sprawdza sie dopiero w dniu wysylki.
Jesli w tym dniu limit okaze sie wyczerpany, bramka przeklada wiadomosc na kolejna
dobe (rekord wraca do pending). Po kilku takich przelozeniach - albo od
razu, gdy limit konta wynosi 0 - wiadomosc konczy jako
failed z opisem w last_error.
Odpowiedz 201 na wysylke zaplanowana nie jest wiec gwarancja, ze
limit ja pomiesci w dniu docelowym. Sprawdz status po terminie wysylki.
id, oddaj sterowanie i sprawdz statusy zbiorczo w zadaniu
cyklicznym albo przez GET /api/sms/history.429 zgodnie z Retry-After.
Poczekaj tyle sekund, ile podaje naglowek. Natychmiastowe ponawianie po 429
tylko generuje ruch - limit i tak nie odnowi sie przed polnoca.unknown na wlasna reke.
Nigdy automatycznie. Oznacz je do przejrzenia przez czlowieka.
Patrz sekcja 5.failed. To jedyny status, przy
ktorym pewnie wiadomo, ze SMS nie poszedl.+48 i dziewiec cyfr)
juz u siebie. Nie polegaj na tym, ze bramka domysli sie kraju.priority swiadomie. Jesli wszystko oznaczysz
jako 1, priorytety przestana cokolwiek znaczyc. Zostaw 5 dla ruchu
zwyklego, a 1-2 dla naprawde pilnych powiadomien.Kazdy przyklad jest kompletny - wystarczy wstawic swoj klucz API i numer odbiorcy.
Wszystkie robia to samo: wysylaja wiadomosc, a potem sprawdzaja jej status,
poprawnie obslugujac unknown.
#!/usr/bin/env bash
# Wysylka SMS przez bramke + sprawdzenie statusu.
set -euo pipefail
BRAMKA="https://sms.inonet.pl"
KLUCZ="smsb_TWOJ_KLUCZ_API"
NUMER="+48123456789"
TRESC="Przypominamy o wizycie jutro o 10:00."
# --- 1. wyslanie ---------------------------------------------------------
ODPOWIEDZ=$(curl -sS -m 15 -w '\n%{http_code}' \
-X POST "$BRAMKA/api/sms/send" \
-H "X-API-Key: $KLUCZ" \
-H "Content-Type: application/json" \
-d "{\"recipient\":\"$NUMER\",\"message\":\"$TRESC\",\"priority\":5}")
KOD=$(printf '%s' "$ODPOWIEDZ" | tail -n1)
CIALO=$(printf '%s' "$ODPOWIEDZ" | sed '$d')
if [ "$KOD" != "201" ]; then
echo "Blad wysylki (HTTP $KOD): $CIALO" >&2
exit 1
fi
# id bez zewnetrznych narzedzi (jesli masz jq: ID=$(echo "$CIALO" | jq -r .id))
ID=$(printf '%s' "$CIALO" | grep -o '"id":[0-9]*' | head -n1 | cut -d: -f2)
echo "Zakolejkowano, id=$ID"
# --- 2. sprawdzenie statusu (nie czesciej niz co 10 s) --------------------
for _ in 1 2 3 4 5 6; do
sleep 10
STATUS=$(curl -sS -m 15 "$BRAMKA/api/sms/status/$ID" -H "X-API-Key: $KLUCZ" \
| grep -o '"status":"[a-z]*"' | head -n1 | cut -d'"' -f4)
echo "status: $STATUS"
case "$STATUS" in
sent) echo "OK - wiadomosc nadana"; exit 0 ;;
failed) echo "Nie poszlo - mozna powtorzyc"; exit 1 ;;
unknown) echo "UWAGA: wynik nierozstrzygniety. NIE ponawiaj automatycznie -"
echo "wiadomosc mogla zostac nadana. Do przejrzenia przez czlowieka."
exit 2 ;;
esac
done
echo "Wiadomosc nadal w kolejce - sprawdz pozniej"
#!/usr/bin/env python3
"""Wysylka SMS przez bramke + sprawdzenie statusu.
Wymaga: pip install httpx
(dziala tak samo z 'requests' - zamien httpx.Client na requests.Session)
"""
import os
import sys
import time
import httpx
BRAMKA = "https://sms.inonet.pl"
KLUCZ = os.environ.get("SMS_API_KEY", "smsb_TWOJ_KLUCZ_API")
class BledneZadanie(Exception):
"""Blad, ktorego nie ma sensu ponawiac bez zmiany danych."""
class LimitWyczerpany(Exception):
"""429 - limit dzienny. Atrybut 'za_ile' mowi, ile sekund czekac."""
def __init__(self, za_ile: int) -> None:
super().__init__(f"Limit dzienny wyczerpany, ponow za {za_ile} s")
self.za_ile = za_ile
def wyslij(klient: httpx.Client, numer: str, tresc: str,
priorytet: int = 5, termin: str | None = None) -> int:
"""Kolejkuje SMS i zwraca jego id."""
dane = {"recipient": numer, "message": tresc, "priority": priorytet}
if termin:
# bez strefy = czas lokalny Europe/Warsaw
dane["scheduled_at"] = termin
odp = klient.post("/api/sms/send", json=dane)
if odp.status_code == 201:
return int(odp.json()["id"])
if odp.status_code == 429:
# Retry-After podaje bramka - nie licz polnocy samodzielnie
za_ile = int(odp.headers.get("Retry-After", "3600"))
raise LimitWyczerpany(za_ile)
if odp.status_code in (400, 401, 403, 422):
raise BledneZadanie(f"HTTP {odp.status_code}: {odp.text}")
# 500 / 503 - blad przejsciowy, ponowienie ma sens
odp.raise_for_status()
raise RuntimeError(f"Nieoczekiwany kod {odp.status_code}")
def sprawdz_status(klient: httpx.Client, sms_id: int) -> dict:
"""Zwraca pelny stan wiadomosci."""
odp = klient.get(f"/api/sms/status/{sms_id}")
odp.raise_for_status()
return odp.json()
def czekaj_na_wynik(klient: httpx.Client, sms_id: int,
prob: int = 6, odstep: int = 10) -> dict:
"""Odpytuje status co 'odstep' sekund - NIE co sekunde."""
for _ in range(prob):
time.sleep(odstep)
stan = sprawdz_status(klient, sms_id)
if stan["status"] in ("sent", "failed", "unknown"):
return stan
return sprawdz_status(klient, sms_id)
def main() -> int:
with httpx.Client(
base_url=BRAMKA,
headers={"X-API-Key": KLUCZ},
timeout=15.0,
) as klient:
try:
sms_id = wyslij(klient, "+48123456789",
"Przypominamy o wizycie jutro o 10:00.")
except LimitWyczerpany as exc:
print(f"Limit dzienny wyczerpany - ponow za {exc.za_ile} s", file=sys.stderr)
return 1
except BledneZadanie as exc:
print(f"Zadanie odrzucone: {exc}", file=sys.stderr)
return 1
print(f"Zakolejkowano, id={sms_id}")
stan = czekaj_na_wynik(klient, sms_id)
if stan["status"] == "sent":
print(f"OK - nadano {stan['sent_at']}")
return 0
if stan["status"] == "failed":
# jedyny status, przy ktorym wolno powtorzyc automatycznie
print(f"Nie poszlo: {stan['last_error']} - mozna powtorzyc", file=sys.stderr)
return 1
if stan["status"] == "unknown":
# NIE PONAWIAJ. SMS mogl zostac nadany.
print("UWAGA: wynik nierozstrzygniety - wiadomosc mogla zostac nadana.",
file=sys.stderr)
print("Nie ponawiaj automatycznie. Oznacz do przejrzenia przez czlowieka.",
file=sys.stderr)
print(f"Szczegoly: {stan['last_error']}", file=sys.stderr)
return 2
print(f"Nadal w kolejce (status={stan['status']}) - sprawdz pozniej")
return 0
if __name__ == "__main__":
raise SystemExit(main())
<?php
/**
* Wysylka SMS przez bramke + sprawdzenie statusu.
* Wymaga rozszerzenia ext-curl (standardowe w PHP).
*/
declare(strict_types=1);
const BRAMKA = 'https://sms.inonet.pl';
const KLUCZ = 'smsb_TWOJ_KLUCZ_API';
/**
* Jedno zapytanie do API. Zwraca ['kod' => int, 'dane' => array, 'naglowki' => array].
*/
function zapytaj(string $metoda, string $sciezka, ?array $cialo = null): array
{
$ch = curl_init(BRAMKA . $sciezka);
$naglowki = [];
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $metoda,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
CURLOPT_HTTPHEADER => [
'X-API-Key: ' . KLUCZ,
'Content-Type: application/json',
'Accept: application/json',
],
// zbieramy naglowki odpowiedzi - potrzebny bedzie Retry-After przy 429
CURLOPT_HEADERFUNCTION => function ($ch, string $linia) use (&$naglowki): int {
$czesci = explode(':', $linia, 2);
if (count($czesci) === 2) {
$naglowki[strtolower(trim($czesci[0]))] = trim($czesci[1]);
}
return strlen($linia);
},
]);
if ($cialo !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($cialo, JSON_UNESCAPED_UNICODE));
}
$odp = curl_exec($ch);
if ($odp === false) {
$blad = curl_error($ch);
curl_close($ch);
throw new RuntimeException('Blad polaczenia z bramka: ' . $blad);
}
$kod = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
return [
'kod' => $kod,
'dane' => json_decode((string) $odp, true) ?? [],
'naglowki' => $naglowki,
];
}
/** Kolejkuje SMS, zwraca jego id. */
function wyslij(string $numer, string $tresc, int $priorytet = 5): int
{
$r = zapytaj('POST', '/api/sms/send', [
'recipient' => $numer,
'message' => $tresc,
'priority' => $priorytet,
]);
if ($r['kod'] === 201) {
return (int) $r['dane']['id'];
}
if ($r['kod'] === 429) {
// liczbe sekund do polnocy podaje bramka
$zaIle = (int) ($r['naglowki']['retry-after'] ?? 3600);
throw new RuntimeException("Limit dzienny wyczerpany - ponow za {$zaIle} s");
}
throw new RuntimeException(
"Wysylka odrzucona (HTTP {$r['kod']}): " . json_encode($r['dane'], JSON_UNESCAPED_UNICODE)
);
}
/** Zwraca pelny stan wiadomosci. */
function status(int $smsId): array
{
$r = zapytaj('GET', '/api/sms/status/' . $smsId);
if ($r['kod'] !== 200) {
throw new RuntimeException("Nie mozna odczytac statusu (HTTP {$r['kod']})");
}
return $r['dane'];
}
// ---------------------------------------------------------------- uzycie
try {
$id = wyslij('+48123456789', 'Przypominamy o wizycie jutro o 10:00.');
echo "Zakolejkowano, id={$id}\n";
// odpytujemy co 10 s, a nie co sekunde
$stan = ['status' => 'pending', 'sent_at' => null, 'last_error' => null];
for ($i = 0; $i < 6; $i++) {
sleep(10);
$stan = status($id);
if (in_array($stan['status'], ['sent', 'failed', 'unknown'], true)) {
break;
}
}
switch ($stan['status']) {
case 'sent':
echo "OK - nadano {$stan['sent_at']}\n";
break;
case 'failed':
// jedyny status, przy ktorym wolno powtorzyc automatycznie
echo "Nie poszlo: {$stan['last_error']} - mozna powtorzyc\n";
break;
case 'unknown':
// NIE PONAWIAJ. SMS mogl zostac nadany.
echo "UWAGA: wynik nierozstrzygniety - wiadomosc mogla zostac nadana.\n";
echo "Nie ponawiaj automatycznie. Oznacz do przejrzenia przez czlowieka.\n";
echo "Szczegoly: {$stan['last_error']}\n";
break;
default:
echo "Nadal w kolejce (status={$stan['status']}) - sprawdz pozniej\n";
}
} catch (Throwable $e) {
fwrite(STDERR, 'Blad: ' . $e->getMessage() . "\n");
exit(1);
}
// Wysylka SMS przez bramke + sprawdzenie statusu.
// Node 18+ ma wbudowane fetch - nie potrzeba zadnych zaleznosci.
//
// UWAGA: ten kod uruchamiaj WYLACZNIE po stronie serwera.
// Klucz API w kodzie ladowanym przez przegladarke jest jawny dla kazdego.
const BRAMKA = 'https://sms.inonet.pl';
const KLUCZ = process.env.SMS_API_KEY || 'smsb_TWOJ_KLUCZ_API';
class LimitWyczerpany extends Error {
constructor(zaIle) {
super(`Limit dzienny wyczerpany - ponow za ${zaIle} s`);
this.zaIle = zaIle;
}
}
const spij = (ms) => new Promise((r) => setTimeout(r, ms));
async function zapytaj(metoda, sciezka, cialo) {
const odp = await fetch(BRAMKA + sciezka, {
method: metoda,
headers: {
'X-API-Key': KLUCZ,
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: cialo ? JSON.stringify(cialo) : undefined,
signal: AbortSignal.timeout(15000),
});
const dane = await odp.json().catch(() => ({}));
return { kod: odp.status, dane, naglowki: odp.headers };
}
/** Kolejkuje SMS, zwraca jego id. */
async function wyslij(numer, tresc, priorytet = 5, termin = null) {
const cialo = { recipient: numer, message: tresc, priority: priorytet };
// termin bez strefy jest rozumiany jako czas lokalny Europe/Warsaw
if (termin) cialo.scheduled_at = termin;
const r = await zapytaj('POST', '/api/sms/send', cialo);
if (r.kod === 201) return r.dane.id;
if (r.kod === 429) {
// liczbe sekund do polnocy podaje bramka - nie licz jej samodzielnie
throw new LimitWyczerpany(Number(r.naglowki.get('retry-after') || 3600));
}
throw new Error(`Wysylka odrzucona (HTTP ${r.kod}): ${JSON.stringify(r.dane)}`);
}
/** Zwraca pelny stan wiadomosci. */
async function status(smsId) {
const r = await zapytaj('GET', `/api/sms/status/${smsId}`);
if (r.kod !== 200) throw new Error(`Nie mozna odczytac statusu (HTTP ${r.kod})`);
return r.dane;
}
/** Odpytuje status co 10 s - NIE co sekunde. */
async function czekajNaWynik(smsId, prob = 6, odstepMs = 10000) {
for (let i = 0; i < prob; i++) {
await spij(odstepMs);
const stan = await status(smsId);
if (['sent', 'failed', 'unknown'].includes(stan.status)) return stan;
}
return status(smsId);
}
async function main() {
const id = await wyslij('+48123456789', 'Przypominamy o wizycie jutro o 10:00.');
console.log(`Zakolejkowano, id=${id}`);
const stan = await czekajNaWynik(id);
switch (stan.status) {
case 'sent':
console.log(`OK - nadano ${stan.sent_at}`);
break;
case 'failed':
// jedyny status, przy ktorym wolno powtorzyc automatycznie
console.error(`Nie poszlo: ${stan.last_error} - mozna powtorzyc`);
process.exitCode = 1;
break;
case 'unknown':
// NIE PONAWIAJ. SMS mogl zostac nadany.
console.error('UWAGA: wynik nierozstrzygniety - wiadomosc mogla zostac nadana.');
console.error('Nie ponawiaj automatycznie. Oznacz do przejrzenia przez czlowieka.');
console.error(`Szczegoly: ${stan.last_error}`);
process.exitCode = 2;
break;
default:
console.log(`Nadal w kolejce (status=${stan.status}) - sprawdz pozniej`);
}
}
main().catch((e) => {
if (e instanceof LimitWyczerpany) {
console.error(e.message);
} else {
console.error(`Blad: ${e.message}`);
}
process.exitCode = 1;
});
{"detail":[{"type":"value_error","loc":["body","recipient"],
"msg":"Value error, nieprawidlowy numer telefonu (oczekiwane np. +48123456789)",
"input":"abc"}]}
Przyczyna: numer zawiera litery albo ma mniej niz 6 lub wiecej
niz 15 cyfr.
Rozwiazanie: podawaj numer w formacie E.164:
+48123456789. Spacje i myslniki sa usuwane automatycznie, ale litery
i inne znaki - nie.
Bramka przyjmie 123456789 (bez +48) i zwroci
201, bo taki ciag miesci sie w regule "6-15 cyfr". Dopiero pozniej
wysylka moze sie nie udac albo trafic pod zly numer. Zawsze podawaj pelny
numer miedzynarodowy z + i prefiksem kraju. Poprawna odpowiedz
201 nie jest dowodem, ze numer jest sensowny.
{"detail":"Brak naglowka X-API-Key"}
{"detail":"Nieprawidlowy, odwolany lub wygasly klucz API"}
Przyczyna i rozwiazanie:
Authorization: Bearer zamiast X-API-Key.
To najczestsza pomylka - Bearer obsluguje panel administracyjny,
nie API klienckie.Retry-After: 47555
{"detail":{"message":"Dzienny limit wysylki zostal wyczerpany",
"daily_limit":100,"sent_today":87,"queued_today":13,"remaining_today":0}}
Przyczyna: suma wiadomosci juz wyslanych dzisiaj i czekajacych
w kolejce z terminem na dzis osiagnela Twoj limit.
Rozwiazanie: poczekaj liczbe sekund z naglowka
Retry-After (limit odnawia sie o polnocy czasu polskiego) albo poproś
administratora o podniesienie limitu. Odrzucona wiadomosc nie trafila do
kolejki - musisz powtorzyc zadanie samodzielnie.
| Objaw | Przyczyna | Rozwiazanie |
|---|---|---|
404 przy sprawdzaniu statusu wiadomosci, ktora na pewno wyslales |
Rekord kolejki zostal posprzatany po zrealizowaniu wysylki. | Uzyj GET /api/sms/history. Status odczytuj w ciagu kilku minut
od wyslania. |
400 przy /history?status=pending |
Historia zawiera wylacznie wysylki zakonczone. | Filtruj po sent, failed albo unknown.
Wiadomosci w kolejce sprawdzaj przez /status/{id}. |
SMS wyszedl o innej godzinie niz w scheduled_at |
Podano date bez strefy, zakladajac UTC. | Data bez strefy = czas polski. Podawaj strefe jawnie
(...+02:00 albo ...Z) i sprawdz, co zwrocila
odpowiedz. |
| Duplikaty u odbiorcow | Kod ponawia wysylke dla kazdego statusu innego niz sent,
wiec takze dla unknown. |
Ponawiaj wylacznie po failed.
Patrz sekcja 5. |
Ustawiono priority: 1, a wiadomosc dalej czeka |
Priorytet ustala kolejnosc w kolejce, nie omija terminu
scheduled_at ani niedostepnosci urzadzenia. |
Sprawdz scheduled_at w odpowiedzi. |
422 mimo poprawnej tresci |
Tresc przekracza 1600 znakow albo sklada sie z samych spacji. | Skroc tresc albo podziel na kilka wiadomosci. |
503 przy kazdym zadaniu |
Urzadzenie nadawcze przypisane do konta jest wylaczone. | Zglos administratorowi, podajac pole detail.reason. |