Wbudowany moduł Statystyk w BaseLinker jest wygodny, ale szybko trafia na sufit. Analizuje tylko sześć stałych kategorii (liczba i wartość zamówień, źródła zamówień, typy wysyłki i płatności, klienci, produkty oraz pracownicy), a filtrować można właściwie wyłącznie po zakresie czasu, źródłach i statusach zamówień. Nie ma tu własnego SQL, nie zdefiniujesz dowolnych metryk ani wymiarów, nie połączysz danych między zbiorami, a jedyny eksport to CSV per wykres. Gdy szef e-commerce chce zobaczyć marżę po kanale, porównać Allegro z własnym sklepem albo policzyć LTV, natywne statystyki po prostu tego nie policzą (do tego BaseLinker oferuje osobny, płatny produkt Base Analytics).

Rozwiązaniem jest przeniesienie surowych danych o zamówieniach z API BaseLinker do własnej hurtowni. W tym przewodniku pokażę kompletną, darmową ścieżkę: metoda getOrders pobierana skryptem Google Apps Script do arkusza Google Sheets, codzienny wyzwalacz, a następnie dwie drogi do BigQuery i gotowy dashboard sprzedaży w Data Studio. Dzięki temu jedno zamówienie z zagnieżdżoną tablicą produktów rozłożysz na tabelę pozycji, dołączysz czytelny wymiar kanału i zbudujesz raporty, których wbudowany moduł nigdy nie wyprodukuje. Cały czas trzymamy się faktów z oficjalnej dokumentacji API (https://api.baselinker.com) – jeśli któraś wartość u Ciebie się różni, potwierdź ją w dokumentacji.

1. Dlaczego eksportować dane z BaseLinker

Kluczowy powód jest strukturalny. Metoda getOrders zwraca na każde zamówienie zagnieżdżoną tablicę produktów (jedno zamówienie to N pozycji), a natywne statystyki pokazują tylko zagregowane wykresy. Nie połączysz w nich zamówień z produktami, klientami czy kosztami po dowolnych kluczach ani nie utrzymasz długiej, odpytywalnej historii. W BigQuery spłaszczasz tablicę produktów do tabeli faktów na poziomie pozycji i dokładasz mapę z getOrderSources jako pełnoprawny wymiar kanału. Dopiero to daje przychód po marketplace, marżę, LTV i kohorty oraz dashboardy w Data Studio, których wbudowany moduł nie zbuduje.

  • Brak własnych metryk i SQL w natywnych statystykach – tylko gotowe wykresy i CSV per wykres.
  • Brak modelowania relacyjnego – nie połączysz zamówień z pozycjami, kosztami czy danymi z innych systemów.
  • Ograniczona historia – hurtownia daje trwały, odpytywalny magazyn danych sprzedażowych.
  • Uwaga o wartości zamówienia – w nagłówku zamówienia nie ma jednego pola z sumą. Wartość liczysz z tablicy produktów (price_brutto * quantity) powiększonej o delivery_price.

2. API BaseLinker i token

API BaseLinker to jedna brama. Wszystkie wywołania to żądania HTTP POST na pojedynczy endpoint https://api.baselinker.com/connector.php (konta Enterprise korzystają z https://api-e.baselinker.com/connector.php). Nie ma osobnych ścieżek per metoda. W ciele żądania przesyłasz dwa pola formularza: method (nazwa metody, np. getOrders) oraz parameters (argumenty metody jako łańcuch JSON). Dane są zakodowane jak formularz (application/x-www-form-urlencoded) w UTF-8. Uwaga: dla treści base64 znak + trzeba zamienić na sekwencję %2B.

Autoryzacja odbywa się nagłówkiem HTTP X-BLToken (zalecana metoda). Starszy sposób z polem POST token nadal działa, ale jest oficjalnie wycofany (deprecated). Token generujesz w panelu BaseLinker w sekcji Moje konto -> API (w wersji angielskiej: Account & other -> My account -> API), podając nazwę aplikacji i klikając przycisk generowania. Token jest przypisany do Twojego konta użytkownika.

# Przykładowe żądanie (curl) zgodne z dokumentacją:
curl 'https://api.baselinker.com/connector.php' \
  -H 'X-BLToken: 1-23-ABC' \
  --data-raw 'method=getOrders&parameters={"date_confirmed_from":1407341754}'

Każda odpowiedź to JSON z polem status równym SUCCESS lub ERROR. Przy błędzie dochodzą error_code i error_message, a przy sukcesie klucze zależne od metody (np. orders, statuses, logs). Warto znać kilka metod, które przydadzą się w raportowaniu:

  • getOrders – paczka szczegółowych danych zamówień (max 100 na wywołanie).
  • getOrderStatusList – lista statusów zdefiniowanych przez użytkownika (id, name, name_for_customer, color, group_id, is_primary). Służy do zamiany numerycznego order_status_id na czytelną nazwę.
  • getOrderSources – mapa kanałów: dekoduje order_source i order_source_id na nazwy kont (allegro, amazon, ebay, shop, personal itd.).
  • getJournalList – dziennik zdarzeń zamówień z ostatnich 3 dni, przydatny do przyrostowego wykrywania zmian.
Limit zapytań. API pozwala na 100 zapytań na minutę na token. Przekroczenie zwraca odpowiedź z status: ERROR i skutkuje tymczasową blokadą klucza. Dokładnej nazwy kodu błędu nie ma potrzeby zgadywać – w razie wątpliwości sprawdź ją w dokumentacji na https://api.baselinker.com.

3. Apps Script: getOrders do Google Sheets

Poniższy skrypt pobiera potwierdzone zamówienia metodą getOrders i zapisuje je do arkusza, spłaszczając tablicę produktów do pojedynczych wierszy (jedna pozycja = jeden wiersz). W Apps Script przekazujemy payload jako obiekt, dzięki czemu UrlFetchApp automatycznie wysyła je jako application/x-www-form-urlencoded – dokładnie tak, jak oczekuje BaseLinker. Nie ustawiamy contentType: 'application/json' i nie serializujemy całego payloadu do JSON, bo API czyta pola formularza.

Bezpieczeństwo tokenu. Nie wpisuj tokenu na stałe w kodzie. Zapisz go w Ustawienia projektu -> Właściwości skryptu (PropertiesService.getScriptProperties) i odczytuj w locie. Każdy, kto zna token, może czytać i modyfikować dane Twojego konta BaseLinker.
// === Konfiguracja ===
const API_URL = 'https://api.baselinker.com/connector.php';

function getToken_() {
  // Token trzymamy we Właściwościach skryptu, nie w kodzie.
  return PropertiesService.getScriptProperties().getProperty('BL_TOKEN');
}

// Uniwersalne wywołanie API BaseLinker
function blRequest_(method, parameters) {
  const options = {
    method: 'post',
    headers: { 'X-BLToken': getToken_() }, // zalecana autoryzacja nagłówkiem
    payload: {
      method: method,
      parameters: JSON.stringify(parameters || {})
    },
    muteHttpExceptions: true // sami obsługujemy kody błędów
  };

  const response = UrlFetchApp.fetch(API_URL, options);
  const data = JSON.parse(response.getContentText());

  if (data.status !== 'SUCCESS') {
    throw new Error('BaseLinker API: ' + (data.error_code || '') + ' ' + (data.error_message || ''));
  }
  return data;
}

// Konwersja uniksowego znacznika czasu (sekundy!) na czytelną datę w PL
function formatujDate_(ts) {
  if (!ts) return '';
  // BaseLinker zwraca sekundy; Date oczekuje milisekund -> ts * 1000
  return Utilities.formatDate(new Date(ts * 1000), 'Europe/Warsaw', 'yyyy-MM-dd HH:mm:ss');
}

function pobierzZamowienia() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName('Zamowienia') || ss.insertSheet('Zamowienia');

  // Nagłówki tylko na pustym arkuszu
  if (sheet.getLastRow() === 0) {
    sheet.appendRow([
      'order_id', 'order_source', 'order_source_id', 'date_add', 'date_confirmed',
      'order_status_id', 'currency', 'product_name', 'sku', 'ean',
      'price_brutto', 'quantity', 'wartosc_pozycji', 'tax_rate'
    ]);
  }

  const props = PropertiesService.getScriptProperties();
  // Kursor stronicowania: data potwierdzenia ostatniego pobranego zamówienia (unix, sekundy).
  // Przy pierwszym uruchomieniu ustaw punkt startowy, np. początek roku.
  let dateConfirmedFrom = Number(props.getProperty('BL_CURSOR')) ||
    Math.floor(new Date('2026-01-01T00:00:00Z').getTime() / 1000);

  const START = Date.now();
  const LIMIT_MS = 5 * 60 * 1000; // margines wobec twardego limitu 6 minut

  while (true) {
    // Przerywamy przed limitem czasu wykonania i dokończymy przy kolejnym wyzwoleniu.
    if (Date.now() - START > LIMIT_MS) break;

    const data = blRequest_('getOrders', {
      date_confirmed_from: dateConfirmedFrom,
      get_unconfirmed_orders: false // pobieramy tylko potwierdzone zamówienia
    });

    const orders = data.orders || [];
    if (orders.length === 0) break;

    // Spłaszczamy tablicę produktów: 1 pozycja = 1 wiersz
    const rows = [];
    orders.forEach(function (o) {
      (o.products || []).forEach(function (p) {
        rows.push([
          o.order_id,
          o.order_source,        // kanał: allegro, amazon, ebay, shop, personal...
          o.order_source_id,     // ID konkretnego konta w danym kanale
          formatujDate_(o.date_add),
          formatujDate_(o.date_confirmed),
          o.order_status_id,     // ID statusu (nazwy z getOrderStatusList)
          o.currency,
          p.name,
          p.sku,
          p.ean,
          Number(p.price_brutto),
          Number(p.quantity),
          Number(p.price_brutto) * Number(p.quantity), // wartość pozycji
          Number(p.tax_rate)
        ]);
      });
    });

    // Jeden zbiorczy zapis zamiast wielu appendRow (wydajność)
    if (rows.length) {
      sheet.getRange(sheet.getLastRow() + 1, 1, rows.length, rows[0].length).setValues(rows);
    }

    // Kursor: date_confirmed ostatniego zamówienia + 1 s, by nie pobrać go ponownie
    const last = orders[orders.length - 1];
    dateConfirmedFrom = Number(last.date_confirmed) + 1;
    props.setProperty('BL_CURSOR', String(dateConfirmedFrom));

    // Mniej niż 100 zamówień = koniec danych
    if (orders.length < 100) break;

    Utilities.sleep(700); // ~85 zapytań/min, poniżej limitu 100/min
  }
}

Kluczowa jest tu logika stronicowania. getOrders zwraca maksymalnie 100 zamówień na wywołanie, więc bierzemy wartość date_confirmed ostatniego zamówienia z paczki, zwiększamy ją o 1 sekundę (żeby nie pobrać tego samego zamówienia ponownie) i przekazujemy jako date_confirmed_from w kolejnym żądaniu. Powtarzamy, aż otrzymamy paczkę mniejszą niż 100 zamówień. Kursor zapisujemy w Script Properties, dzięki czemu pobieranie jest wznawialne między uruchomieniami.

4. Codzienny wyzwalacz (trigger)

Automatyzację uzyskasz przez instalowalny wyzwalacz czasowy (clock trigger). Uruchom raz poniższą funkcję albo dodaj wyzwalacz ręcznie w edytorze Apps Script (ikona zegara). Poniżej codzienne uruchomienie o 6:30 czasu warszawskiego.

function utworzWyzwalacz() {
  ScriptApp.newTrigger('pobierzZamowienia')
    .timeBased()
    .everyDays(1)
    .atHour(6)
    .nearMinute(30)
    .inTimezone('Europe/Warsaw')
    .create();
}

Ponieważ kursor zapamiętujemy w Script Properties, każde dzienne uruchomienie dobiera tylko nowe, potwierdzone zamówienia od miejsca, w którym poprzednio skończyło. To robi z tego prosty, przyrostowy proces ETL.

5. Dwie drogi do BigQuery

Droga A: natywny konektor Google Sheets (proste wolumeny)

Google Sheets i Data Studio mają darmowy, natywny konektor – arkusz dodajesz wprost jako źródło danych. To najprostsza droga, wystarczająca przy umiarkowanych wolumenach. Jeśli chcesz sięgnąć do BigQuery przez arkusz, możesz użyć Connected Sheets, ale pamiętaj o ograniczeniu: zestaw wyników Connected Sheets jest ograniczony do 100 000 wierszy, więc dla dużych i rosnących zbiorów to się nie skaluje. Minusy względem BigQuery to mniejsza skala i brak transformacji po stronie serwera.

Droga B: ładowanie do tabeli BigQuery (skala)

Dla rosnących danych lepiej ładować je do tabeli w BigQuery. BigQuery w Apps Script to usługa zaawansowana (Advanced Service), którą trzeba najpierw włączyć (Usługi -> BigQuery API w projekcie Cloud). Do dziennego dopisywania idealnie pasuje zadanie ładowania (load job): jest darmowe w warstwie ingestu, a limit to 1500 zadań ładowania na tabelę na dobę, więc raz dziennie to komfort. Alternatywne streamowanie (Tabledata.insertAll) jest bliskie czasu rzeczywistego, ale płatne za bajty, wiersze trafiają do bufora, a Google zaleca dziś Storage Write API zamiast starego insertAll. Dla tabeli rosnącej raz dziennie wybierz load job z WRITE_APPEND.

function zaladujDoBigQuery(csvString) {
  const projectId = 'twoj-projekt';
  const datasetId = 'baselinker';
  const tableId   = 'zamowienia';

  const job = {
    configuration: {
      load: {
        destinationTable: { projectId: projectId, datasetId: datasetId, tableId: tableId },
        sourceFormat: 'CSV',
        skipLeadingRows: 1,          // pomiń wiersz nagłówka
        writeDisposition: 'WRITE_APPEND', // dopisz nowe wiersze
        autodetect: true             // wykryj schemat automatycznie
      }
    }
  };

  const blob = Utilities.newBlob(csvString, 'text/csv');
  BigQuery.Jobs.insert(job, projectId, blob);
}
Wskazówka modelowania. Ładuj dane już spłaszczone do poziomu pozycji (tak jak w skrypcie z sekcji 3). W BigQuery zbudujesz wtedy tabelę faktów, do której dołączysz mapę kanałów z getOrderSources oraz nazwy statusów z getOrderStatusList jako wymiary.

6. Dashboard sprzedaży w Data Studio

Gdy dane są już w arkuszu lub w BigQuery, w Data Studio dodajesz je jako źródło i budujesz raport. Trzy widoki, które w praktyce pokrywają większość potrzeb polskiego e-commerce:

  • Sprzedaż po kanale (marketplace / Allegro) – wymiar zbudowany na order_source i order_source_id, zdekodowany przez mapę z getOrderSources na czytelne nazwy kont. To jedyny sposób, by odróżnić zamówienie z Allegro od Amazona od własnego sklepu. Wykres słupkowy przychodu i liczby zamówień w rozbiciu na kanały.
  • Sprzedaż po produkcie – dzięki spłaszczonej tablicy produktów agregujesz po product_name lub sku. Miara przychodu to suma price_brutto * quantity. Tabela z topem bestsellerów oraz wykres udziału produktów.
  • Przychód w czasie – wykres szeregu czasowego oparty na date_confirmed. Pamiętaj o poprawnej konwersji znacznika czasu i strefie Europe/Warsaw, żeby dobowe słupki nie przesuwały się o jeden dzień.
Statusy i średnie. Numeryczny order_status_id zamień na nazwy z getOrderStatusList, aby filtry statusów były czytelne. Wskaźniki i średnie w Data Studio licz zawsze jako SUM(a)/SUM(b) (np. średnia wartość zamówienia = SUM(wartość)/SUM(liczba zamówień)), nigdy jako średnią z ilorazów.

7. Pułapki i dobre praktyki

  • Limit 6 minut na wykonanie skryptu. Jedno wywołanie Apps Script ma twardy limit 6 minut, którego nie podniesiesz nawet w płatnych planach. Duży backfill (wiele stronicowanych wywołań po 100 zamówień) może go przekroczyć. Ratunek to wzorzec wznawialny: kursor date_confirmed_from w PropertiesService i ponowne wyzwalanie lub praca w dobowych przyrostach. Wyzwalacze czasowe mają też łączny budżet około 90 minut dziennie.
  • Stronicowanie getOrders. Maksymalnie 100 zamówień na wywołanie. Zawsze przesuwaj kursor o date_confirmed ostatniego zamówienia + 1 sekunda i kończ, gdy paczka ma mniej niż 100 pozycji.
  • Limit 100 zapytań na minutę. Throttluj (np. Utilities.sleep), by nie przekroczyć limitu API i nie doprowadzić do tymczasowej blokady klucza.
  • Znaczniki czasu w sekundach. Pola dat (date_add, date_confirmed) to uniksowy epoch w sekundach (10 cyfr). W JS/Apps Script mnóż przez 1000: new Date(ts * 1000), inaczej dostaniesz daty z 1970 roku. Formatuj jawnie w strefie Europe/Warsaw (epoch jest w UTC, Polska to UTC+1/UTC+2 z DST), by uniknąć błędów o jeden dzień w wymiarach dat.
  • Bezpieczeństwo tokenu. Token trzymaj wyłącznie w Script Properties i przesyłaj nagłówkiem X-BLToken. Nie commituj go do współdzielonego ani eksportowanego kodu i ogranicz udostępnianie skryptu.
  • Dzienny limit UrlFetch. Liczba wywołań UrlFetchApp na dobę jest ograniczona i zależy od typu konta (orientacyjnie 20 000 dla kont konsumenckich i 100 000 dla Google Workspace). Każde pojedyncze fetch liczy się jako jedno wywołanie – potwierdź aktualne wartości w dokumentacji limitów Apps Script, jeśli robisz masowe pobrania.

Podsumowanie

Natywne statystyki BaseLinker odpowiadają na proste pytania, ale nie dają własnych metryk, modelu relacyjnego ani trwałej historii. Przenosząc zamówienia przez API (getOrders) skryptem Apps Script do Google Sheets, a stamtąd do BigQuery i Data Studio, zyskujesz elastyczne raporty sprzedaży po kanale, produkcie i w czasie – automatyczne, wznawialne i skalowalne. Zacznij od jednego dziennego wyzwalacza i widoku po kanałach, a resztę wymiarów dokładaj stopniowo. Gdy któraś wartość API u Ciebie się różni, potwierdź ją w oficjalnej dokumentacji: https://api.baselinker.com.