← Blog

API SMS w Node.js. Wysyłka SMS z fetch, paczka npm i webhooki w Express

Zespół Przypominamy.com · 7 września 2026 · 9 min czytania

Jeśli Twoja aplikacja działa na Node.js, Deno, Bun albo Cloudflare Workers i ma wysyłać SMS-y — przypomnienia o wizytach, kody 2FA, powiadomienia o zamówieniach — potrzebujesz jednego żądania HTTP. W tym przewodniku pokazujemy dwie drogi: oficjalną paczkę przypominamy z npm (zero zależności, TypeScript) oraz surowy fetch, jeśli wolisz nie dokładać biblioteki. Do tego webhooki statusów w Express z weryfikacją podpisu HMAC i obsługa błędów, które naprawdę zdarzają się w produkcji.

Wymagania wstępne: Node.js 18+ (wbudowany fetch), konto na Przypominamy.com (testowe działa od razu po rejestracji) i klucz API pk_test_… lub pk_live_….

1. Instalacja i klucz

npm install przypominamy

Klucz trzymaj w zmiennej środowiskowej — nigdy nie commituj go do gita:

# .env (nie commituj!)
PRZYPOMINAMY_API_KEY=pk_live_a1b2c3d4e5f6...

2. Pierwszy SMS w 5 liniach

import { Przypominamy } from 'przypominamy';

const sms = new Przypominamy(process.env.PRZYPOMINAMY_API_KEY);

const msg = await sms.send({
  to: '+48600123456',
  text: 'Cześć! Przypominamy o wizycie jutro o 14:00.',
  reference: 'wizyta-4521',
});

console.log(msg.id, msg.status, msg.cost_grosze); // msg_… queued 9

To samo bez biblioteki, czystym fetch — dokładnie to robi paczka pod spodem:

const res = await fetch('https://api.przypominamy.com/v1/messages', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.PRZYPOMINAMY_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ to: '+48600123456', text: 'Cześć! Przypominamy o wizycie jutro o 14:00.' }),
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
const msg = await res.json(); // { id, status, to, parts, cost_grosze, ... }

Odpowiedź (HTTP 201):

{
  "id": "msg_qY08hZ2mmDXAazdRTytS",
  "status": "queued",
  "to": "+48600123456",
  "from": "PRZYPOMINAM",
  "text": "Cześć! Przypominamy o wizycie jutro o 14:00.",
  "parts": 1,
  "cost_grosze": 9,
  "reference": "wizyta-4521",
  "send_at": null,
  "delivered_at": null,
  "error": null,
  "created_at": "2026-09-07T10:30:00.000Z",
  "updated_at": "2026-09-07T10:30:00.000Z"
}

Parametry warte zapamiętania

3. Wysyłka do wielu odbiorców

Ta sama treść do wielu numerów to jedno żądanie z tablicą to (do 500 numerów, duplikaty usuwane):

const batch = await sms.send({
  to: ['+48600123456', '+48600234567', '+48600345678'],
  text: 'Promocja -20% tylko do niedzieli! Kod: SMS20',
  from: 'SKLEP',
  reference: 'promo-2026-09',
});

console.log(`${batch.accepted} z ${batch.count} przyjęto, koszt ${batch.total_cost_grosze / 100} zł`);
for (const m of batch.messages) console.log(m.to, '→', m.status, m.error ?? '');

Spersonalizowane treści (imię, godzina) wysyłasz osobnymi żądaniami. Przy większej liczbie rób to z ograniczoną równoległością, żeby nie przekroczyć limitu 120 żądań na minutę:

const wizyty = [
  { id: 4521, to: '+48600123456', imie: 'Anna',  godzina: '14:00' },
  { id: 4522, to: '+48600234567', imie: 'Piotr', godzina: '15:30' },
];

// prosta pula: max 10 równoległych żądań
const queue = [...wizyty];
await Promise.all(Array.from({ length: 10 }, async () => {
  while (queue.length) {
    const w = queue.shift();
    await sms.send({
      to: w.to,
      text: `Cześć ${w.imie}, wizyta jutro o ${w.godzina}. Potwierdź odpisując TAK.`,
      reference: `wizyta-${w.id}`,
      idempotencyKey: `wizyta-${w.id}-przypomnienie`,
    });
  }
}));

4. Obsługa błędów

Każdy błąd API to PrzypominamyError z polami status, code, param, requestId i retryAfter. Rozróżniaj to, co warto ponowić, od tego, czego nie:

import { PrzypominamyError } from 'przypominamy';

async function sendWithRetry(options, attempts = 3) {
  for (let i = 0; i < attempts; i++) {
    try {
      return await sms.send(options);             // options.idempotencyKey = ten sam przy każdej próbie
    } catch (e) {
      if (!(e instanceof PrzypominamyError)) throw e;
      if (e.code === 'rate_limited') {
        await new Promise((r) => setTimeout(r, (e.retryAfter ?? 5) * 1000));
        continue;
      }
      if (e.code === 'provider_error' || e.code === 'network_error') {
        await new Promise((r) => setTimeout(r, 2 ** i * 1000));  // 1s, 2s, 4s
        continue;
      }
      throw e;                                    // invalid_request, insufficient_funds, unauthorized — nie ponawiaj
    }
  }
  throw new Error('Nie udało się wysłać SMS po 3 próbach');
}
KodZnaczenieCo robić
400 / 422invalid_request — zły numer, nadpis, data; pole w paramPopraw payload, nie ponawiaj
401unauthorized — zły lub odwołany kluczSprawdź zmienną środowiskową
402insufficient_funds — komunikat podaje koszt i saldoDoładuj konto, sprawdzaj sms.account()
409idempotency_conflict — ten sam klucz, inna treśćNowy klucz idempotencji
429rate_limitedOdczekaj retryAfter sekund
502 / 504provider_error — dostawca SMSRetry z backoff i tym samym idempotencyKey

5. Webhooki statusów w Express

Po wysyłce chcesz wiedzieć, czy SMS dotarł. Ustaw adres jednym wywołaniem; w odpowiedzi dostajesz sekret do weryfikacji podpisu:

const { webhook_secret } = await sms.setWebhook('https://twojadomena.pl/webhooks/sms');
// zapisz webhook_secret w sekretach jako PRZYPOMINAMY_WEBHOOK_SECRET

Handler w Express. Kluczowe: surowe body (nie sparsowany JSON), bo podpis liczony jest z bajtów:

import express from 'express';
import { verifyWebhook } from 'przypominamy';

const app = express();

app.post('/webhooks/sms', express.raw({ type: 'application/json' }), async (req, res) => {
  let event;
  try {
    event = await verifyWebhook(req.body, req.header('X-Przypominamy-Signature'), process.env.PRZYPOMINAMY_WEBHOOK_SECRET);
  } catch {
    return res.sendStatus(403);
  }

  const msg = event.data.message;
  // event.type: message.sent | message.delivered | message.undelivered | message.failed | message.expired
  queue.add(() => db.updateSmsStatus(msg.id, msg.status, msg.reference));   // ciężka praca w tle
  res.sendStatus(200);                                                        // odpowiedz od razu
});

Bez biblioteki: podpis to t=<unix>,v1=<hex>, gdzie v1 = HMAC-SHA256(secret, `${t}.${body}`). Odrzucaj, gdy podpis się nie zgadza albo t jest starsze niż 5 minut:

import { createHmac, timingSafeEqual } from 'node:crypto';

function verify(rawBody, header, secret) {
  const [, t, sig] = /t=(\d+),v1=([0-9a-f]+)/.exec(header ?? '') ?? [];
  if (!t) return false;
  const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
  return timingSafeEqual(Buffer.from(sig), Buffer.from(expected)) && Math.abs(Date.now() / 1000 - t) < 300;
}

Przy 5xx lub timeoucie platforma ponawia dostarczenie po 2 s i po 10 s (łącznie 3 próby). Stan zawsze możesz też odczytać z sms.get(id).

6. Cloudflare Workers, Deno, Bun

Paczka używa wyłącznie fetch i Web Crypto, więc działa bez zmian poza Node. W Workerze klucz trzymaj jako sekret:

import { Przypominamy } from 'przypominamy';

export default {
  async fetch(request, env) {
    const sms = new Przypominamy(env.PRZYPOMINAMY_API_KEY);
    const msg = await sms.send({ to: '+48600123456', text: 'Hej z Workera!' });
    return Response.json({ id: msg.id });
  },
};

7. Najlepsze praktyki produkcyjne

FAQ

Czy do wysyłki SMS w Node.js potrzebuję SDK?

Nie. Wbudowany fetch w Node 18+ wystarczy: jeden POST JSON na /v1/messages z nagłówkiem Authorization: Bearer. Paczka npm install przypominamy dodaje typy TypeScript, klasę błędu z kodem i requestId oraz weryfikację podpisu webhooków — zero zależności.

Jak obsłużyć rate limit 120 req/min w Node.js?

Łap PrzypominamyError z code === 'rate_limited' i odczekaj retryAfter sekund. Dla kampanii z tą samą treścią wyślij tablicę to (do 500 numerów) w jednym żądaniu. Dla spersonalizowanych wiadomości ogranicz równoległość do ok. 10 żądań naraz.

Jak odbierać webhooki statusów w Express?

Użyj express.raw({ type: 'application/json' }) na trasie webhooka, zweryfikuj nagłówek X-Przypominamy-Signature funkcją verifyWebhook z paczki (albo HMAC-SHA256 z node:crypto) i odpowiedz 200 od razu. Ciężką logikę przenieś do kolejki.

Czy paczka działa w Cloudflare Workers, Deno i Bun?

Tak. Używa wyłącznie fetch i Web Crypto, bez modułów Node. W Workerze klucz API trzymaj jako sekret (wrangler secret put).

Jak zabezpieczyć klucz API w aplikacji produkcyjnej?

Klucz trzymaj w zmiennej środowiskowej lub w secrets managerze, nigdy w repozytorium. W razie wycieku napisz na [email protected] — odwołamy klucz natychmiast i wydamy nowy. Używaj osobnych kluczy dla produkcji i testów.

Gotowy na pierwszy SMS?

Załóż konto — klucz pk_test_… i 25 SMS-ów gratis dostajesz od razu, bez czekania na weryfikację. Pierwszy SMS wysyłasz minutę później.

Załóż konto
Przypominamy.com – logo platformy SMS, MMS i IVR
Asystent Przypominamy ● Online. Odpowiada od razu
Cześć! 👋 Jestem asystentem platformy Przypominamy.com.

Pomogę Ci dobrać plan, wyjaśnię jak działa API lub odpowiem na pytania o SMS, MMS i IVR. Jak mogę pomóc?
teraz