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.
| Metoda | Po co w raporcie | Pola, których używamy | Na co uważać |
|---|---|---|---|
getOrderSources | Nazwy kanałów | typ źródła, identyfikator, nazwa | Kanał to para typu i identyfikatora |
getOrderStatusList | Identyfikator statusu „Anulowane” | id, name | Statusy tworzy sprzedawca, identyfikatory są różne na każdym koncie |
getOrders | Zamówienia z produktami | date_add, order_source, order_source_id, order_status_id, products | 100 zamówień na wywołanie, daty jako Unix, dane kupujących w każdym zamówieniu |
getJournalList | Bieżąca synchronizacja zamiast raportu | dziennik 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. 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:

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.

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.

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.

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 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:

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ń | Allegro | Sklep internetowy | Erli |
|---|---|---|---|
| 21–27.09 | 42 zam., 4 728 zł | 25 zam., 3 567 zł | 4 zam., 497 zł |
| 28.09–4.10 | 36 zam., 5 453 zł | 21 zam., 3 300 zł | 14 zam., 1 363 zł |
| 5–11.10 | 43 zam., 6 066 zł | 23 zam., 3 766 zł | 7 zam., 991 zł |
| 12–18.10 | 49 zam., 6 418 zł | 20 zam., 3 552 zł | 12 zam., 2 221 zł |
| 19–25.10 | 28 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
- 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.
- 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.
- Trzy sposoby stronicowania. Pobranie wszystkich zamówień od 1 września po
date_addzget_unconfirmed_ordersrównymtrue, podate_addzfalsei podate_confirmeddał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łoSUCCESSi 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_confirmedz 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 bierzdate_add. - Sam
order_sourcenie mówi, który to kanał. Grupowanie po samymorder_sourcedał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ędzieallegro, ale przy dwóch kontach Allegro problem jest ten sam. Dlatego para pól i nazwy zgetOrderSources. - 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_doneto wpłaty, nie sprzedaż. Wartość zamówień liczona jakopayment_doneminusdelivery_pricedał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 poorder_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 skryptem | MCP Base w Claude | |
|---|---|---|
| Logowanie | Token w zmiennej środowiskowej | Konto Base przez OAuth |
| Kto liczy | Twój kod, za każdym razem tak samo | Model, za każdym razem od nowa |
| Dobre do | Raport co tydzień, eksport do arkusza, synchronizacja | Jednorazowe pytania („czemu spadło”, „co utknęło”) |
| Ryzyko | Błąd w kodzie powtarza się co tydzień | Model może źle dobrać pole albo okres |
| Zapis na koncie | Tylko jeśli wywołasz metodę zapisu | Narzę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.