SMS z Arkuszy Google: Apps Script, statusy i odpowiedzi

Sprawdzone z dokumentacją:

W Arkuszach Google wysyłasz SMS skryptem Apps Script: UrlFetchApp.fetch na https://smsportal.app/api/v1/gateway/send-bulk-sms z nagłówkiem x-api-key, po 50 wierszy w jednym żądaniu. Skrypt dodaje menu „SMS”, pomija wiersze już wysłane i wpisuje status obok numeru. Bez kodu: pobierz arkusz jako CSV i wgraj go w Wysyłce zbiorczej w panelu.

Czego potrzebujesz

  • Arkusz Google z zakładką SMS: A telefon, B imię, C treść, D status (pusta).
  • Konto smsportal z podłączonym telefonem z Androidem i kartą SIM.
  • Klucz API z panelu smsportal (Klucze API → Wygeneruj klucz API).
  • Do odbioru odpowiedzi: zakładka Odpowiedzi i wdrożenie skryptu jako aplikacji internetowej.

Jak wysłać SMS z Arkuszy Google

  1. Przygotuj kolumny

    Numer w kolumnie A, najlepiej jako tekst (+48600100200), bo arkusz zamienia go na liczbę i gubi plus. Treść w kolumnie C może być formułą, np. ="Dzień dobry "&B2&", przypominamy o wizycie jutro.". Wiersz z pustą treścią skrypt pomija.

  2. Zapisz klucz we właściwościach skryptu

    Rozszerzenia → Apps Script, potem Ustawienia projektu → Właściwości skryptu → Dodaj właściwość skryptu: nazwa SMSPORTAL_API_KEY, wartość to klucz. Klucz nie leży w kodzie, więc kopia arkusza go nie zdradzi.

  3. Wklej skrypt

    W edytorze Apps Script zastąp zawartość pliku Code.gs kodem poniżej i zapisz. Skrypt wysyła 50 wierszy na żądanie i przy błędzie zatrzymuje się, wpisując jego treść w kolumnie D.

    Apps Script: menu SMS i wysyłka partiami
    // Wysyłka SMS z arkusza przez smsportal.
    // Klucz: Ustawienia projektu → Właściwości skryptu → SMSPORTAL_API_KEY.
    const SHEET_NAME = 'SMS';
    const PHONE_COL = 1;   // A: telefon
    const MESSAGE_COL = 3; // C: treść (może być formułą)
    const STATUS_COL = 4;  // D: status, wpisuje go skrypt
    const BATCH_SIZE = 50;
    const COUNTRY_CODE = '48';
    const SENT_PREFIX = 'W kolejce';
    const API_URL = 'https://smsportal.app/api/v1/gateway/send-bulk-sms';
    
    function onOpen() {
      SpreadsheetApp.getUi()
        .createMenu('SMS')
        .addItem('Wyślij niewysłane wiersze', 'sendPendingSms')
        .addToUi();
    }
    
    function sendPendingSms() {
      const apiKey = PropertiesService.getScriptProperties().getProperty('SMSPORTAL_API_KEY');
      if (!apiKey) throw new Error('Dodaj SMSPORTAL_API_KEY we właściwościach skryptu.');
      const sheet = SpreadsheetApp.getActive().getSheetByName(SHEET_NAME);
      const rows = sheet.getDataRange().getValues();
    
      const pending = [];
      for (let i = 1; i < rows.length; i++) {
        const rawPhone = rows[i][PHONE_COL - 1];
        const message = String(rows[i][MESSAGE_COL - 1]).trim();
        const status = String(rows[i][STATUS_COL - 1]);
        if (rawPhone === '' || !message || status.startsWith(SENT_PREFIX)) continue;
        const phone = normalizePhone(rawPhone);
        if (!phone) {
          sheet.getRange(i + 1, STATUS_COL).setValue('Błędny numer');
          continue;
        }
        pending.push({ row: i + 1, phone: phone, message: message });
      }
    
      let queued = 0;
      for (let start = 0; start < pending.length; start += BATCH_SIZE) {
        const batch = pending.slice(start, start + BATCH_SIZE);
        const response = UrlFetchApp.fetch(API_URL, {
          method: 'post',
          contentType: 'application/json',
          headers: { 'x-api-key': apiKey },
          payload: JSON.stringify({
            messages: batch.map(item => ({ recipients: [item.phone], message: item.message })),
          }),
          muteHttpExceptions: true,
        });
        const code = response.getResponseCode();
        const ok = code >= 200 && code < 300;
        const status = ok
          ? SENT_PREFIX + ' ' + Utilities.formatDate(new Date(), Session.getScriptTimeZone(), 'yyyy-MM-dd HH:mm')
          : 'Błąd ' + code + ': ' + response.getContentText().slice(0, 150);
        batch.forEach(item => sheet.getRange(item.row, STATUS_COL).setValue(status));
        if (!ok) break; // 400, 401 i 429 nie znikną przy kolejnej partii
        queued += batch.length;
      }
      SpreadsheetApp.getActive().toast('Do kolejki trafiło SMS-ów: ' + queued);
    }
    
    // 600 100 200 -> +48600100200. Numery z + albo 00 zostają bez zmian.
    function normalizePhone(value) {
      const raw = String(value).trim();
      let digits = raw.replace(/\D/g, '');
      if (!digits) return '';
      if (raw.startsWith('+')) {
        // już w formacie międzynarodowym
      } else if (digits.startsWith('00')) {
        digits = digits.slice(2);
      } else if (!(digits.startsWith(COUNTRY_CODE) && digits.length > 9)) {
        digits = COUNTRY_CODE + digits.replace(/^0+/, '');
      }
      return digits.length >= 8 && digits.length <= 15 ? '+' + digits : '';
    }
  4. Uruchom z menu

    Odśwież arkusz. Pojawi się menu SMS → Wyślij niewysłane wiersze. Za pierwszym razem Google poprosi o zgodę na dostęp do arkusza i do zewnętrznych adresów. Najpierw wyślij dwa wiersze z własnymi numerami.

  5. Sprawdź statusy

    Kolumna D dostaje „W kolejce” z datą. To znaczy, że SMS-y czekają na telefon, który wysyła je co 5 sekund (około 720 na godzinę). Wiersz z błędem dostanie jego opis i wyjdzie przy następnym uruchomieniu.

Jak zbierać odpowiedzi klientów w arkuszu

smsportal wysyła zdarzenie MESSAGE_RECEIVED jako POST z JSON-em, a skrypt wdrożony jako aplikacja internetowa dopisuje wiersz: czas, nadawca, treść. Apps Script nie widzi nagłówków żądania, więc nie sprawdzi X-Signature. Zamiast tego adres zawiera tajny token, a powtórki tego samego zdarzenia odsiewa idempotencyKey.

  1. Dodaj zakładkę Odpowiedzi, w tym samym projekcie Apps Script nowy plik z kodem poniżej, a we właściwościach skryptu WEBHOOK_TOKEN z losowym ciągiem.
  2. Wdróż → Nowe wdrożenie → Aplikacja internetowa. Wykonaj jako: Ja, kto ma dostęp: Każdy. Skopiuj adres kończący się na /exec.
  3. W panelu smsportal Webhooki → Utwórz webhook: adres .../exec?token=TWOJ_TOKEN, sekret podpisu (co najmniej 20 znaków), zdarzenie MESSAGE_RECEIVED.
  4. Apps Script odpowiada na POST przekierowaniem, a smsportal za nim podąża. Odpisz na SMS i sprawdź wynik w Webhooki → Dostarczenia. Jeśli dostawy nie przechodzą, odbieraj odpowiedzi w n8n lub Make.
Apps Script: doPost dopisujący odpowiedzi
// Odpowiedzi klientów do zakładki „Odpowiedzi”.
// Właściwość skryptu WEBHOOK_TOKEN: losowy ciąg, ten sam co w adresie webhooka.
const REPLIES_SHEET = 'Odpowiedzi';

function doPost(e) {
  const token = PropertiesService.getScriptProperties().getProperty('WEBHOOK_TOKEN');
  if (!token || e.parameter.token !== token) {
    return ContentService.createTextOutput('forbidden');
  }
  const data = JSON.parse(e.postData.contents);
  if (data.webhookEvent !== 'MESSAGE_RECEIVED') {
    return ContentService.createTextOutput('ignored');
  }
  const lock = LockService.getScriptLock();
  lock.waitLock(10000);
  try {
    const sheet = SpreadsheetApp.getActive().getSheetByName(REPLIES_SHEET);
    const keys = sheet.getRange(1, 4, Math.max(sheet.getLastRow(), 1)).getValues().flat();
    if (!keys.includes(data.idempotencyKey)) {
      sheet.appendRow([data.receivedAt, data.sender, data.message, data.idempotencyKey]);
    }
  } finally {
    lock.releaseLock();
  }
  return ContentService.createTextOutput('ok');
}

Gotowe scenariusze

  • Przypomnienia o jutrzejszych wizytach

    Kolumna E z datą wizyty, F z godziną wpisaną jako tekst, w C formuła =JEŻELI(E2=DZIŚ()+1; "Przypominamy o wizycie jutro o "&F2; ""). Wyzwalacz czasowy (Wyzwalacze → Dodaj wyzwalacz, codziennie 17-18) uruchamia sendPendingSms, a wiersze z innymi datami mają pustą treść i czekają.

  • Przypomnienia o zaległych płatnościach

    Arkusz z kwotą i terminem, treść z formuły: „Przypominamy o fakturze 12/10 na 450 zł, termin minął 20.10”. Status w D pokazuje, komu przypomnienie już poszło, więc drugie uruchomienie nie wyśle go ponownie.

  • Zmiana dla uczestników wydarzenia

    Lista zapisanych z formularza, ta sama treść w każdym wierszu: „Warsztaty przeniesione do sali 2”. 120 osób to trzy żądania po 50 wierszy, a telefon wyśle wszystko w około 10 minut.

Błędy i rozwiązania

W kolumnie D „Błąd 401”
Przyczyna: Zły klucz albo literówka w nazwie właściwości SMSPORTAL_API_KEY.
Co zrobić: Sprawdź właściwość w Ustawieniach projektu. Skrypt wysyła nagłówek x-api-key, nie Authorization.
W kolumnie D „Błąd 429”
Przyczyna: Wyczerpany limit SMS-ów w pakiecie albo partia większa niż pozwala plan.
Co zrobić: Treść błędu mówi, który limit się skończył. Przy limicie partii zmniejsz BATCH_SIZE.
„Błędny numer” w wielu wierszach
Przyczyna: Numer bez kierunkowego innego kraju albo zbyt krótki po usunięciu spacji.
Co zrobić: Wpisuj numery z + i kierunkowym. Numery z 9 cyfr skrypt traktuje jako polskie (COUNTRY_CODE).
Odpowiedzi nie trafiają do arkusza po zmianie kodu
Przyczyna: Adres /exec serwuje wdrożoną wersję, nie bieżący kod.
Co zrobić: Wdróż → Zarządzaj wdrożeniami → edytuj → Nowa wersja. Adres się nie zmienia.
Status „W kolejce”, ale SMS nie dochodzi
Przyczyna: Telefon jest offline albo Android usypia aplikację.
Co zrobić: Sprawdź zasięg, internet i stan telefonu w panelu. Wiadomości czekają do 72 godzin, potem wygasają.

Najczęstsze pytania

Jakie limity ma Apps Script?

Według Google: 20 000 wywołań UrlFetch dziennie na koncie prywatnym i 100 000 w Google Workspace, do 6 minut na jedno uruchomienie, a wyzwalacze łącznie 90 minut dziennie (prywatne) lub 6 godzin (Workspace). Przy 50 wierszach na żądanie realnym limitem jest telefon, nie Google.

Dlaczego jedno żądanie na partię, a nie na wiersz?

`send-bulk-sms` przyjmuje listę wiadomości z różną treścią dla każdej osoby, więc 500 wierszy to 10 żądań zamiast 500. Skrypt kończy się w kilka sekund, a resztę robi kolejka telefonu.

Czy mogę wysłać SMS bez pisania skryptu?

Tak. Plik → Pobierz → Wartości rozdzielane przecinkami (.csv), potem w panelu smsportal Wysyłka zbiorcza: wgrywasz plik, wskazujesz kolumnę z numerem i wstawiasz zmienne typu `{{ imie }}`.

Czy skrypt wyśle SMS automatycznie po dodaniu wiersza?

Dodaj wyzwalacz czasowy, np. co 15 minut, na `sendPendingSms`. Wiersze ze statusem „W kolejce” są pomijane, więc nikt nie dostanie SMS-a dwa razy.

Czy mogę tak wysłać promocję do całej bazy?

Tylko do osób, które dały zgodę marketingową, zgodnie z Prawem komunikacji elektronicznej. Z karty SIM wysyłaj wiadomości, których odbiorca się spodziewa: przypomnienia, statusy, zmiany terminu. Kampanie należą do dostawcy z nazwą nadawcy.

Źródła

  1. Google Apps Script: limity i kwoty
  2. Google Apps Script: UrlFetchApp
  3. Google Apps Script: właściwości skryptu
  4. Google Apps Script: własne menu
  5. Google Apps Script: aplikacje internetowe (doPost)
  6. smsportal: dokumentacja API

Więcej w poradnikach

Wyślij pierwszego SMS-a jeszcze dziś

Załóż darmowe konto, podłącz telefon i sprawdź, jak klienci reagują na wiadomości z Twojego numeru.