API BaseLinkera (Base) to jedno POST-owe wejście pod adresem https://api.base.com/connector.php, token w nagłówku X-BLToken i limit 100 zapytań na minutę. Tygodniowy raport sprzedaży zbudujesz z trzech metod: getOrderSources (nazwy kanałów), getOrderStatusList (który status to anulowanie) i getOrders (zamówienia, po 100 na wywołanie).

Poniżej cały skrypt w Pythonie, bez zewnętrznych bibliotek, sprawdzony na koncie Base sklepu z ceramiką Ochra (Allegro, sklep internetowy, Erli). Pobrał 376 zamówień z pięciu tygodni w czterech wywołaniach getOrders. Po drodze trafiliśmy na kilka pułapek, w tym datę, którą API przyjęło bez błędu i zwróciło nie ten okres. Opisujemy je niżej.

MetodaPo co w raporciePola, których używamyNa co uważać
getOrderSourcesNazwy kanałówtyp źródła, identyfikator, nazwaKanał to para typu i identyfikatora
getOrderStatusListIdentyfikator statusu „Anulowane”id, nameStatusy tworzy sprzedawca, identyfikatory są różne na każdym koncie
getOrdersZamówienia z produktamidate_add, order_source, order_source_id, order_status_id, products100 zamówień na wywołanie, daty jako Unix, dane kupujących w każdym zamówieniu
getJournalListBieżąca synchronizacja zamiast raportudziennik zdarzeńTylko ostatnie 3 dni; na naszym koncie zwrócił pustą listę

Krok 1: token i pierwsze wywołanie

Token generujesz w panelu Base, w sekcji Moje konto, zakładka API. Dokumentacja API pod adresem api.base.com opisuje ten sam adres wejścia, nagłówek i limit (stan na 2 listopada 2026 r.). Źródło: dokumentacja API Base

Wstęp dokumentacji API Base: adres https://api.base.com/connector.php, zalecenie autoryzacji nagłówkiem X-BLToken z przekreślonym parametrem token, przykładowe polecenie curl z metodą getOrders i limit 100 zapytań na minutę.

Wstęp dokumentacji API Base. 1 Adres, na który idą wszystkie metody. 2 Token w nagłówku X-BLToken; parametr token w treści jest przestarzały. 3 Limit 100 zapytań na minutę.

W panelu ta sama zakładka pokazuje limit, listę tokenów i przycisk dodawania nowego:

Zakładka API w panelu Base (Moje konto): limit 100 zapytań na minutę, przycisk Dodaj Token API i tabela Tokeny API z kolumnami nazwa, token API, uprawnienia, użycie tokenu w ciągu 24 godzin, utworzone przez i data utworzenia.

Moje konto › API w panelu Base, konto demo Ochry. 1 Limit 100 zapytań na minutę. 2 Dodawanie nowego tokenu. 3 Kolumna z tokenem: panel pokazuje tylko końcówkę (tu zamaskowaną). 4 Uprawnienia tokenu; ten ma pełne.

Przy dodawaniu tokenu wybierasz uprawnienia: „Pełny”, „Odczyt” albo „Wybrane metody”. Do raportu wystarczy „Odczyt”. Token zapisz od razu, bo panel pokaże go tylko raz.

Okno Dodaj Token API w panelu Base: informacja, że token jest widoczny tylko jeden raz, pole Nazwa i lista Uprawnienia API z wybraną opcją Odczyt.

Nowy token w panelu Base (okno zamknięte bez zapisu). 1 Token widać tylko raz, zaraz po wygenerowaniu. 2 Uprawnienia: do raportu „Odczyt”.

Zanim napiszesz kod, sprawdź token i parametry w testerze zapytań na tej samej stronie („Test your request”). Uwaga z dokumentacji: tester wykonuje prawdziwe zapytanie na koncie, do którego należy token. Do testów używaj więc tylko metod get.

Formularz Test your request w dokumentacji API Base: puste pole Token z zaleceniem nagłówka X-BLToken, wybrana metoda getOrders i parametry {"date_from": 1789941600}.

Tester zapytań w dokumentacji API Base, wypełniony, ale niewysłany. 1 Pole na token. 2 Metoda getOrders. 3 Parametry w JSON: date_from jako znacznik czasu Unix (tu 21 września 2026 r., północ w Polsce).

Token z pełnymi uprawnieniami wywoła także metody zapisu (setOrderStatus, addOrder, deleteOrders). Każdy token trzymaj w zmiennej środowiskowej, poza repozytorium i poza plikami, które czyta asystent AI.

Krok 2: funkcja do wywołań

Każda metoda to ten sam POST z dwoma polami formularza: method i parameters (JSON jako tekst). Odpowiedź ma pole status: SUCCESS albo ERROR z error_code i error_message.

API_URL = "https://api.base.com/connector.php"

def call(method, params=None):
    body = urllib.parse.urlencode({"method": method, "parameters": json.dumps(params or {})})
    req = urllib.request.Request(API_URL, data=body.encode(),
                                 headers={"X-BLToken": os.environ["BASE_API_TOKEN"]})
    with urllib.request.urlopen(req, timeout=60) as resp:
        answer = json.load(resp)
    time.sleep(0.7)  # limit: 100 zapytań na minutę na token
    if answer.get("status") != "SUCCESS":
        raise RuntimeError(f"{method}: {answer.get('error_code')} {answer.get('error_message')}")
    return answer

Przerwa 0,7 s po każdym wywołaniu trzyma skrypt poniżej limitu z zapasem na inne narzędzia, które używają tego samego tokenu. Adres api.base.com zamiast starego api.baselinker.com: w changelogu z 21 września 2026 r. Base zapowiedział koniec TLS 1.0 i 1.1 od 1 października i zalecił przejście na nową domenę. Źródło: changelog API Base

Krok 3: kanały i statusy

Kanał zamówienia to dwa pola: order_source (typ: shop, personal, allegro i inne marketplace’y) i order_source_id. Dokumentacja zaznacza, że identyfikator jest unikalny tylko razem z typem.

Opis pól order_source i order_source_id w dokumentacji metody getOrders: lista typów źródła (shop, personal, order_return, kody marketplace'ów) i uwaga, że identyfikator źródła jest unikalny tylko razem z order_source.

Pola zamówienia w dokumentacji getOrders. 1 order_source, czyli typ źródła. 2 order_source_id, unikalny tylko w parze z typem, np. dwa konta Allegro mają różne identyfikatory.

Nazwy kanałów daje getOrderSources, a statusy getOrderStatusList. Słownik (typ, identyfikator) → nazwa i zbiór identyfikatorów statusów, których nie liczymy:

SKIP_STATUSES = {"Anulowane"}  # nazwy statusów z getOrderStatusList

def channel_names():
    names = {}
    for source, ids in call("getOrderSources")["sources"].items():
        for source_id, name in ids.items():
            names[(source, int(source_id))] = name
    return names

skip = {s["id"] for s in call("getOrderStatusList")["statuses"] if s["name"] in SKIP_STATUSES}

Statusy szukamy po nazwie, a nie po identyfikatorze, bo każde konto ma własne. Jeśli w Twoim procesie są też statusy typu „Zwrot” albo „Nieopłacone”, dopisz je do zbioru.

Krok 4: wszystkie zamówienia z okresu

getOrders zwraca najwyżej 100 zamówień. Dokumentacja Base każe stronicować po dacie potwierdzenia: date_confirmed_from, potem date_confirmed ostatniego zamówienia plus jedna sekunda, aż przyjdzie mniej niż 100 zamówień. Źródło: dokumentacja API Base, getOrders

Parametry metody getOrders w dokumentacji API Base: date_confirmed_from, date_from i id_from typu int oraz get_unconfirmed_orders typu bool, domyślnie false, z opisem niepotwierdzonych zamówień z Allegro.

Parametry dat w getOrders. 1 date_confirmed_from: od daty potwierdzenia, tak stronicuje dokumentacja. 2 date_from: od daty dodania zamówienia, tego używamy w raporcie. 3 get_unconfirmed_orders: domyślnie false, więc niepotwierdzone zamówienia (np. z Allegro bez wypełnionego formularza dostawy) nie przychodzą.

Do raportu sprzedaży stronicujemy po date_add, bo pytamy, kiedy kupujący kupił. Powód zobaczysz w „Co zauważyliśmy”. Z każdego zamówienia od razu zostawiamy tylko pola do raportu, bo getOrders zwraca też imiona, adresy, e-maile i telefony kupujących.

def orders_between(start, end):
    """Zamówienia z date_add w [start, end), stronicowane po date_add."""
    orders, cursor = {}, int(start.timestamp())
    while True:
        page = call("getOrders", {"date_from": cursor})["orders"]
        for o in page:
            if o["date_add"] < end.timestamp():
                # zostawiamy tylko pola do raportu: bez adresów, e-maili i telefonów
                orders[o["order_id"]] = {k: o[k] for k in (
                    "date_add", "order_source", "order_source_id", "order_status_id", "products")}
        if len(page) < 100 or page[-1]["date_add"] >= end.timestamp():
            return list(orders.values())
        cursor = page[-1]["date_add"] + 1

Słownik po order_id chroni przed policzeniem tego samego zamówienia dwa razy, gdyby strony się nakładały. Na Ochrze cztery wywołania od 21 września dały 376 zamówień, a strony zaczynały się od kolejnych sekund: 1789941600, 1790760061, 1791553801, 1792318981. To samo stronicowanie zrobił Claude Code w sesji z MCP Base, kiedy sprawdzaliśmy spadek sprzedaży na Allegro:

Fragment sesji Claude Code: na stronie jest 100 zamówień, ostatnie ma date_add 1791553800, a kolejne wywołanie getOrders zaczyna od date_from 1791553801; wynik ma 192 070 znaków i przekracza limit rozmowy.

Kolejna strona getOrders w sesji Claude Code z MCP Base. 1 Na stronie jest 100 zamówień, więc to nie koniec. 2 date_add ostatniego zamówienia. 3 Następne wywołanie od tej daty plus jedna sekunda. 4 Strona ma ok. 190 tys. znaków JSON.

To kilkadziesiąt tysięcy tokenów na jedną stronę, za które w planie Claude Enterprise płacisz po stawkach API. Ile kosztuje pytanie o miesiąc zamówień w rozmowie, a ile przez skrypt, policzyliśmy w artykule Claude dla firm w dużym e-commerce.

Krok 5: tydzień i kanał

Daty w API to znaczniki czasu Unix. Tydzień liczymy od poniedziałku w strefie Europe/Warsaw, a nie w UTC, bo inaczej zamówienia z poniedziałku przed 2:00 (latem) albo 1:00 (zimą) wpadają do poprzedniego tygodnia. Sprzedaż to suma price_brutto × quantity produktów, bez kosztu dostawy i bez anulowanych zamówień.

def main(first_monday, end_monday):
    start = datetime.combine(date.fromisoformat(first_monday), datetime.min.time(), TZ)
    end = datetime.combine(date.fromisoformat(end_monday), datetime.min.time(), TZ)
    names = channel_names()
    skip = {s["id"] for s in call("getOrderStatusList")["statuses"] if s["name"] in SKIP_STATUSES}

    rows = defaultdict(lambda: [0, 0.0])  # (tydzień, kanał) -> [zamówienia, sprzedaż brutto]
    for o in orders_between(start, end):
        if o["order_status_id"] in skip:
            continue
        day = datetime.fromtimestamp(o["date_add"], TZ).date()
        week = day - timedelta(days=day.weekday())
        channel = names.get((o["order_source"], o["order_source_id"]), o["order_source"])
        row = rows[(week, channel)]
        row[0] += 1
        row[1] += sum(p["price_brutto"] * p["quantity"] for p in o["products"])

    out = csv.writer(sys.stdout)
    out.writerow(["tydzien_od", "kanal", "zamowienia", "sprzedaz_brutto", "srednie_zamowienie"])
    for (week, channel), (count, value) in sorted(rows.items()):
        out.writerow([week, channel, count, round(value, 2), round(value / count, 2)])

Cały plik raport_base.py to te trzy fragmenty plus importy:

import csv, json, os, sys, time, urllib.parse, urllib.request
from collections import defaultdict
from datetime import date, datetime, timedelta
from zoneinfo import ZoneInfo

TZ = ZoneInfo("Europe/Warsaw")
# ... call(), channel_names(), orders_between(), main() jak wyżej ...

if __name__ == "__main__":
    main(*sys.argv[1:3])

Uruchomienie dla pięciu tygodni od 21 września (drugi argument to poniedziałek po ostatnim tygodniu):

export BASE_API_TOKEN=...   # z panelu Base, Moje konto › API
python3 raport_base.py 2026-09-21 2026-10-26 > raport.csv

Wynik

Sześć wywołań API (kanały, statusy i cztery strony zamówień), 376 zamówień, z czego 14 anulowanych. W pliku CSV wychodzi tabela, tutaj w skrócie:

TydzieńAllegroSklep internetowyErli
21–27.0942 zam., 4 728 zł25 zam., 3 567 zł4 zam., 497 zł
28.09–4.1036 zam., 5 453 zł21 zam., 3 300 zł14 zam., 1 363 zł
5–11.1043 zam., 6 066 zł23 zam., 3 766 zł7 zam., 991 zł
12–18.1049 zam., 6 418 zł20 zam., 3 552 zł12 zam., 2 221 zł
19–25.1028 zam., 5 070 zł22 zam., 4 048 zł15 zam., 1 613 zł

W ostatnim tygodniu CSV miał jeszcze czwarty wiersz: kanał „Osobiście/tel.” z jednym zamówieniem na 1 633 zł. O nim niżej.

Sprawdzenie drugim sposobem

  1. Ceny z katalogu zamiast z zamówień. Te same sztuki pomnożone przez ceny brutto z osobnego pliku produktów sklepu dają co do złotówki te same kwoty w każdym tygodniu i kanale. Żaden produkt nie był sprzedany w innej cenie niż katalogowa.
  2. Ten sam wynik bez skryptu. Tygodnie i kanały zgadzają się z tabelą, którą Claude Code policzył przez MCP Base w artykule o spadku sprzedaży na Allegro, czyli innym narzędziem i bez tego kodu.
  3. Trzy sposoby stronicowania. Pobranie wszystkich zamówień od 1 września po date_add z get_unconfirmed_orders równym true, po date_add z false i po date_confirmed dało za każdym razem 581 różnych zamówień w sześciu wywołaniach.

Co zauważyliśmy

  • Data jako tekst nie daje błędu. Gdy wysłaliśmy {"date_from": "2026-10-19"}, zamiast znacznika czasu, API odpowiedziało SUCCESS i zwróciło 100 zamówień od 1 września, czyli od początku konta. Daty licz jako znacznik czasu Unix i sprawdzaj w odpowiedzi datę pierwszego zamówienia.
  • Data potwierdzenia to nie data zakupu. Na tym koncie 580 z 581 zamówień ma date_confirmed z dwóch kilkuminutowych okien: 490 z 25 października (13:01–13:11) i 90 z 22 października (16:18–16:20), tak jak bywa po imporcie zamówień z innego systemu. Raport po dacie potwierdzenia wrzuciłby sprzedaż od września do dwóch tygodni. Stronicowanie z dokumentacji (date_confirmed_from) jest dobre do bieżącej synchronizacji, bo zamówienie nie może dostać wcześniejszej daty potwierdzenia. Do pytań o sprzedaż w czasie bierz date_add.
  • Sam order_source nie mówi, który to kanał. Grupowanie po samym order_source dałoby jeden wiersz, personal, bo na tym koncie Allegro, sklep i Erli są źródłami ręcznymi z różnymi identyfikatorami. Na koncie z podpiętym Allegro typem będzie allegro, ale przy dwóch kontach Allegro problem jest ten sam. Dlatego para pól i nazwy z getOrderSources.
  • Raport pokazał zamówienie, którego nikt nie szukał. W tygodniu 19–25.10 pojawił się kanał „Osobiście/tel.” z jednym zamówieniem: dodanym ręcznie, w statusie spoza procesu sprzedaży, z produktem spoza katalogu. Skrypt go nie ukrył i dobrze, bo w prawdziwym sklepie takie zamówienie zawyża sprzedaż tygodnia. Zdecyduj, które kanały liczysz, i wypisuj resztę osobno.
  • payment_done to wpłaty, nie sprzedaż. Wartość zamówień liczona jako payment_done minus delivery_price dała w sklepie internetowym w tygodniu 12–18.10 o ok. 510 zł mniej, bo dwa nowe zamówienia nie były jeszcze opłacone. Sprzedaż licz z produktów.
  • Jedna strona to ok. 200 tys. znaków JSON. 100 zamówień z adresami i produktami to za dużo, żeby wklejać je do rozmowy z modelem. Skrypt odcina dane kupujących od razu i zostawia pięć pól. Gdy dajesz ten skrypt Claude Code, model widzi CSV, a nie strony zamówień.
  • Sekunda plus jeden może zgubić zamówienie. Na Ochrze trzy razy dwa zamówienia mają tę samą sekundę date_add, ale żadne nie wypadło na granicy strony. Jeśli na granicy trafią dwa zamówienia z tą samą sekundą, a na stronie zmieści się tylko pierwsze, plus jedna sekunda pominie drugie. Przy dużym ruchu kontroluj liczbę zamówień innym zapytaniem albo stronicuj od tej samej sekundy i odrzucaj powtórki po order_id (słownik w kodzie już to robi).

Co tydzień, bez ręcznego uruchamiania

Skrypt nie potrzebuje bibliotek spoza Pythona 3.9+, więc wystarczy cron na serwerze albo komputerze, który jest włączony w poniedziałek rano. Dla pięciu tygodni, z których ostatni właśnie się skończył:

# poniedziałek 7:00, ostatnie 5 pełnych tygodni
0 7 * * 1  cd ~/raporty && python3 raport_base.py $(date -d 'monday -5 weeks' +\%F) $(date -d 'monday' +\%F) > raport-$(date +\%F).csv

To samo zapytanie zbudujesz w n8n albo Make: węzeł HTTP z metodą POST na https://api.base.com/connector.php, nagłówkiem X-BLToken i polami formularza method oraz parameters. Pętlę po stronach i podział na tygodnie trzeba w obu przypadkach złożyć z kilku węzłów, więc przy pierwszej wersji raportu Python jest krótszy.

Jeśli nie masz serwera ani włączonego komputera, to samo pytanie możesz zaplanować w Claude Cowork z konektorem MCP Base: zadanie działa w chmurze Anthropic, ale liczy model, więc wynik warto raz porównać z tym skryptem. Polecenie, które zamyka pułapki opisane wyżej, jest w artykule o Claude Cowork w sklepie internetowym.

Jeśli wynik ma trafić do arkusza, BigQuery albo raportu w Power BI, zmienia się tylko koniec skryptu. Najtrudniejsze bywa co innego: sprzedaż w Base nie zgadza się z ERP albo z magazynem, bo część zamówień trafia do systemów inną drogą. Wtedy raport jest szybki, ale błędny. Napisz, na którym kroku utknąłeś, a powiemy, co poprawić.

API z tokenem czy MCP Base

API z tokenem i skryptemMCP Base w Claude
LogowanieToken w zmiennej środowiskowejKonto Base przez OAuth
Kto liczyTwój kod, za każdym razem tak samoModel, za każdym razem od nowa
Dobre doRaport co tydzień, eksport do arkusza, synchronizacjaJednorazowe pytania („czemu spadło”, „co utknęło”)
RyzykoBłąd w kodzie powtarza się co tydzieńModel może źle dobrać pole albo okres
Zapis na koncieTylko jeśli wywołasz metodę zapisuNarzędzie z zapisem wymaga potwierdzenia, można je zablokować

My używamy obu: pytanie zadajemy przez MCP, a gdy odpowiedź ma przychodzić co tydzień, zamieniamy ją w skrypt i sprawdzamy, czy liczby się zgadzają. Jak podłączyć MCP Base i co widzi Claude, opisaliśmy w artykule BaseLinker (Base) MCP w Claude i ChatGPT. Gdy dane w Base nie zgadzają się z ERP albo magazynem, zacznij od audytu integracji Base i Apilo.

← Wszystkie artykuły