# Dokumentacja API Senuto — pełna treść > Dokumentacja REST API Senuto: raporty widoczności, monitoring pozycji, baza słów kluczowych i analiza SERP. Wygenerowano: 2026-09-15 · źródło: https://docs.senuto.com Stron: 135. Każda sekcja zaczyna się adresem strony źródłowej. --- # Dokumentacja API Senuto ## Wybierz dane, których potrzebujesz - [Analiza widoczności](/modules/visibility_analysis) - [Monitoring](/modules/rank_tracker) - [Baza słów kluczowych](/modules/keywords_analysis) - [Analiza SERP](/modules/serp_analysis) - [Użytkownik](/modules/user) ## Od tokenu do pierwszej odpowiedzi 1. **Sprawdź dostęp** — potrzebujesz konta Senuto z aktywnym dostępem do API. [Dostęp do API →](/access) 2. **Pobierz token** — zaloguj się kontem Senuto i pobierz token JWT. [Autoryzacja →](/authorization) 3. **Wyślij zapytanie** — skopiuj przykład lub użyj playgroundu w przeglądarce. [Pierwsze kroki →](/get-started) Bazowy adres API: ```text https://api.senuto.com ``` ## Przydatne podczas integracji - **Przygotowanie żądania:** [kraje i `country_id`](/countries), [`fetch_mode`](/types/fetch-mode), [filtrowanie](/types/filter) i [paginacja](/types/pagination). - **Obsługa odpowiedzi:** [błędy](/types/errors), [limity zapytań](/rate-limits) oraz [eksporty i zadania async](/exports-and-tasks). - **Wybór integracji:** [API, MCP czy no-code](/api-mcp-nocode). ## Co możesz zbudować - **Własne raporty i dashboardy** — zasilaj Looker Studio, Power BI czy wewnętrzną hurtownię danymi o widoczności i pozycjach, zamiast ręcznie eksportować CSV. - **Automatyczny monitoring konkurencji** — cykliczne porównania widoczności Twojej domeny z konkurentami, alerty o wzrostach i spadkach fraz. - **Pipeline'y keyword research** — programowe pobieranie fraz, na które rankuje dowolna domena, wraz z filtrowaniem i statystykami. - **Integracje z narzędziami zespołu** — wysyłka danych SEO do Slacka, arkuszy, CRM czy systemu ticketowego (np. przez Make/Zapier lub własny skrypt). - **Agentów AI / LLM** — spójna koperta `{ success, data, pagination }` i jawnie oznaczone pola wymagane pozwalają modelowi złożyć żądanie bez zgadywania; gotowe narzędzia bez pisania kodu daje [serwer MCP](/mcp). --- # Pierwsze kroki REST API Senuto pozwala pobierać dane z modułów Senuto (Analiza widoczności, Monitoring, Baza słów kluczowych i inne) bezpośrednio do Twoich integracji. Bazowy adres: ``` https://api.senuto.com ``` ## Twoje pierwsze zapytanie Od zera do pierwszej odpowiedzi: pobierzemy token, a nim — frazy, na które rankuje domena (`positions/getData` z Analizy Widoczności). ### Pobierz token Potrzebujesz konta Senuto z aktywnym **dostępem do API**. Token JWT (ważny 31 dni) pobierzesz e‑mailem i hasłem konta: ```bash curl --location --request POST 'https://api.senuto.com/api/users/token' \ --header 'Content-Type: application/json' \ --data-raw '{ "email": "twoj@email.com", "password": "TWOJE_HASŁO" }' ``` Token znajdziesz w polu `data.token` odpowiedzi. Wolisz bez terminala? Zaloguj się tutaj — token zostanie też zapamiętany w playgroundzie: Szczegóły przepływu — formaty ciała, pełna odpowiedź, błędy autoryzacji, zasady bezpieczeństwa — są na stronie **[Autoryzacja](/authorization)**. ### Wyślij żądanie Wstaw token w miejsce `$YOUR_TOKEN_HERE` w przykładzie poniżej — albo kliknij **Wypróbuj ten endpoint** i wyślij żądanie z przeglądarki, bez pisania kodu: **`POST /api/visibility_analysis/reports/positions/getData`** Wymagane pola tego raportu to **`domain`** i **`fetch_mode`**: **cURL** ```bash curl --location --request POST 'https://api.senuto.com/api/visibility_analysis/reports/positions/getData' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' \ --data-raw '{ "domain": "zalando.pl", "fetch_mode": "topLevelDomain" }' ``` **JSON (body)** ```json filename="ciało żądania" { "domain": "zalando.pl", "fetch_mode": "topLevelDomain" } ``` ### Odczytaj odpowiedź Odpowiedź jest opakowana w `success` / `data` / `pagination`: ```json filename="przykładowa-odpowiedź (skrócona)" { "success": true, "data": [ { "keyword_id": 184, "keyword": "toni and paul", "statistics": { "position": { "current": 29 } /* … */ } } ], "pagination": { "page_count": 97041, "current_page": 1, "has_next_page": true, "count": 291121, "limit": 10 } } ``` ## Zasady ogólne Adres endpointu ma postać `/api////`, na przykład `/api/visibility_analysis/reports/positions/getData`. > **Ostrzeżenie:** > Moduł, sekcję i kontroler zapisujesz w `snake_case` (`ai_overviews`, `domains_ranking`), > ale **akcję zawsze w `camelCase`** (`getData`, `getDomainStatistics`). Zapisana jako > `get_data` zwróci `404`. Pozostałe przyczyny `404`: [Błędy](/types/errors). - **Nagłówki.** Każde żądanie wymaga `Authorization: Bearer `; przy `POST` dodaj też `Content-Type: application/json`. - **`fetch_mode`** (wymagane w wielu endpointach) określa, jak interpretowana jest `domain`. Dozwolone wartości: `topLevelDomain` (cała domena — to jest „domain"), `subdomain`, `catalog`, `url`. - **Koperta odpowiedzi.** Sukces: `{ "success": true, "data": …, "pagination": … }`. Błąd: `{ "success": false, "data": { "error": { "type", "message", "params" } } }`. - **GET vs POST.** Większość raportów to `POST` z ciałem JSON, ale część (np. Dashboard) to `GET` z parametrami w **query stringu** — wysłanie ich w ciele JSON zwraca wtedy `418`. Metoda jest podana na stronie każdego endpointu. - **Paginacja.** Tam gdzie zwracana jest lista, używaj `limit` (domyślnie 10) i `page` (domyślnie 1); meta jest w `pagination`. > **Błąd:** > Status **`418`** oznacza błąd walidacji żądania (`invalid_data`) — nie tylko przekroczenie limitu zapytań. W polu `data.error.params` znajdziesz, które pole jest nieprawidłowe, np. brak wymaganego `fetch_mode`. ## Co dalej - **[Autoryzacja](/authorization)** — pozyskanie i odświeżanie tokenu, wymagany dostęp do API, błędy autoryzacji. - **[Moduły](/modules)** — endpointy raportowe (Analiza widoczności, Monitoring, Baza słów kluczowych…) z interaktywnym playgroundem. - **[Typy](/types)** — wspólne struktury: [`Filter`](/types/filter) (parametr `filtering`), [Paginacja](/types/pagination) (`limit`/`page`), [Błędy](/types/errors) (koperta błędu i status `418`). --- # Dostęp do API — plan, zakup, aktywacja Zanim wyślesz pierwsze żądanie, konto musi mieć **dostęp do API**. Ta strona odpowiada na cztery pytania, które najczęściej trafiają do supportu. ## Kto ma dostęp | Plan | Dostęp do API | | ----------------------- | -------------------------- | | **Advanced**, **Prime** | wliczony w cenę planu | | **Basic** | dodatek — **99 zł/mies.** | | **Lite** | dodatek — **199 zł/mies.** | Ceny wyżej to stawka miesięczna, ale **dodatek kupujesz na tyle, ile zostało Twojego planu**, i płacisz proporcjonalnie za te dni. Przy pakiecie rocznym ze 100 dniami do końca zapłacisz za 100 dni dodatku, a nie za jeden miesiąc; przy pakiecie rozliczanym miesięcznie wychodzi dokładnie miesiąc. Porównanie pakietów: [cennik Senuto](https://www.senuto.com/pl/cennik/) (ceny dodatku: stan na 2026-08-12). Dodatek API obejmuje też **[serwer MCP](/mcp)**. Sam MCP, bez bezpośredniego dostępu do REST API, jest tańszym, osobnym dodatkiem — [czego potrzebujesz do MCP](/mcp#czego-potrzebujesz). ### Jak dokupić Dodatek włączasz **samodzielnie w aplikacji**, bez kontaktu z handlowcem: **[app.senuto.com/user/user-package](https://app.senuto.com/user/user-package)** — tam też zarządzasz nim później. Potrzebujesz faktury proforma? Napisz na czacie w aplikacji. **Nie wiąże Cię żadna dłuższa umowa.** Dodatek kończy się razem z bieżącym okresem planu i odnawia razem z nim, a rezygnacja polega na **nieprzedłużeniu go w panelu** — po zakończeniu opłaconego okresu dostęp do API wygasa sam. Nie musisz nikomu tego zgłaszać ani wysyłać wypowiedzenia. > **Ostrzeżenie:** > **Nie ma darmowego trialu API.** Nie da się przetestować endpointów bez wykupionego dostępu (albo > pakietu Advanced/Prime, gdzie API jest w cenie). Jeśli potrzebujesz sprawdzić API przed decyzją, > napisz na czacie w aplikacji do supportu — takie przypadki rozpatrywane są indywidualnie. Sama > dokumentacja jest w całości publiczna: przykłady żądań i odpowiedzi zobaczysz > na stronach endpointów bez logowania. > **Informacja:** > **Dodatek API daje dostęp do tego, co masz w planie** — nie dokłada modułów. Konto bez dodatku Analiza > SERP nie wywoła endpointów SERP także przez API. W obrębie wykupionych modułów zakres jest identyczny > w każdym pakiecie (te same endpointy, ta sama dokumentacja), różnią się **limity**: każde konto > konsumuje własne pule, a ich wysokość zależy od pakietu (patrz [Limity zapytań](/rate-limits)). ## Kiedy zacznie działać **Od razu po zakupie.** Nie ma okresu oczekiwania ani ręcznej aktywacji po stronie Senuto — zaraz po opłaceniu dodatku możesz pobrać token i wysłać pierwsze żądanie. Token wygenerujesz w panelu — [Ustawienia konta → Integracje](https://app.senuto.com/user/integrations), kafelek **API** — albo wywołując `POST /api/users/token` z e-mailem i hasłem swojego konta. ## Jak sprawdzić, czy dostęp działa Najprostszy test — pobierz token, a potem zapytaj o dane zalogowanego konta: ```bash # 1. token (ważny 31 dni) curl --location --request POST 'https://api.senuto.com/api/users/token' \ --header 'Content-Type: application/json' \ --data-raw '{ "email": "twoj@email.com", "password": "TWOJE_HASŁO" }' # 2. test tokenu curl --location 'https://api.senuto.com/api/users/whoami' \ --header 'Authorization: Bearer TWOJ_TOKEN' ``` Odpowiedź `{"success": true, "data": {"email": "..."}}` oznacza, że wszystko gra. Co może pójść nie tak: | Objaw | Znaczenie | | ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | `418` + `Invalid username or password` | złe dane logowania (to hasło do aplikacji Senuto, nie osobne API) | | `418` + `Email isnt confrimed` | konto z niepotwierdzonym adresem — kliknij link z maila rejestracyjnego, a gdy go nie masz, poproś support o ponowne wysłanie | | `403` z pustym `message` | konto nie ma aktywnego planu albo dodatku Dostęp do API | | `302` na `…/users/login`, a po przekierowaniu `404` z pustym `message` | token wygasł, jest błędny albo nie wysłałeś nagłówka `Authorization` | Limity, które przysługują Twojemu kontu, odczytasz jednym wywołaniem — [`GET /api/users/getLimits`](/rate-limits) (nie zużywa żadnej jednostki). ## Co dalej - [Autoryzacja](/authorization) - [Pierwsze kroki](/get-started) - [Limity zapytań](/rate-limits) - [API, MCP czy no-code](/api-mcp-nocode) --- # Autoryzacja Każde żądanie do API Senuto musi być uwierzytelnione **tokenem Bearer (JWT)** w nagłówku `Authorization`. Ta strona opisuje cały przepływ: od wymagań konta, przez pozyskanie tokenu, po obsługę błędów autoryzacji. ## Czego potrzebujesz 1. **Konta Senuto** — token jest powiązany z Twoim użytkownikiem i jego planem. 2. **Dostępu do API** w planie konta. Bez niego bezpośrednie wywołania API kończą się statusem `403` z pustą treścią. Jeśli Twój plan nie obejmuje dostępu do API, dokupisz go jako dodatek — [Dostęp do API](/access). ## Pozyskanie tokenu Token uzyskasz, logując się adresem e‑mail i hasłem konta Senuto: ``` POST https://api.senuto.com/api/users/token ``` **cURL** ```bash curl --location --request POST 'https://api.senuto.com/api/users/token' \ --header 'Content-Type: application/json' \ --data-raw '{ "email": "twoj@email.com", "password": "TWOJE_HASŁO" }' ``` **JSON (body)** ```json filename="ciało żądania" { "email": "twoj@email.com", "password": "TWOJE_HASŁO" } ``` Token wygenerujesz też bez wywoływania tego endpointu: panel Senuto, [Ustawienia konta → Integracje](https://app.senuto.com/user/integrations), kafelek **API**. Nie chcesz używać terminala? Zaloguj się poniżej — token trafi prosto do przeglądarki i zostanie zapamiętany w playgroundzie na stronach endpointów: ### Odpowiedź W polu `data.token` dostajesz JWT; pozostałe pola opisują konto: ```ts type TokenResponse = { success: true; data: { /** Token JWT — przekazuj w nagłówku `Authorization: Bearer ` */ token: string; /** ID Twojego użytkownika */ id: number; email: string; /** Język konta, np. "pl-PL" */ lang: string; /** Waluta konta, np. "PLN" */ currency: string; currency_ratio: number; country_id: number; }; } export default TokenResponse ``` > **Ostrzeżenie:** > **Token jest ważny 31 dni.** Nie ma osobnego endpointu odświeżania — po wygaśnięciu (`Token expired`) po prostu pobierz nowy token tym samym żądaniem. Traktuj token jak hasło: nie umieszczaj go w repozytorium ani w kodzie frontendowym; trzymaj w zmiennej środowiskowej lub sejfie sekretów. ## Użycie tokenu Do **każdego** żądania dodaj nagłówek `Authorization`, a przy `POST` także `Content-Type: application/json`: ``` Authorization: Bearer Content-Type: application/json ``` Szybki test poprawności tokenu — endpoint zwracający dane zalogowanego użytkownika: ```bash curl --location 'https://api.senuto.com/api/users/whoami' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' ``` ```json filename="odpowiedź" { "success": true, "data": { "email": "twoj@email.com" } } ``` ## Błędy autoryzacji Klasycznego `401` to API nie zwraca w żadnym z tych przypadków. Status zależy od tego, na którym etapie odpadło żądanie. | Sytuacja | Status | Treść odpowiedzi | | ------------------------------------------------------------------ | ------------------------------ | --------------------------------------------- | | Błąd logowania na `POST /api/users/token` | `418` | koperta błędu z `data.error.type` i `message` | | Zwykły endpoint, token nieważny albo brak nagłówka `Authorization` | `302`, po przekierowaniu `404` | `{"success": false, "message": ""}` | | Konto bez aktywnego planu albo bez dodatku API | `403` | `{"success": false, "message": ""}` | ### `418` — błędy logowania Czytelny komunikat dostajesz wyłącznie z `POST /api/users/token`. Rozpoznawaj go po `data.error.message`. | Komunikat | Kiedy występuje | Co zrobić | | ---------------------------------------------- | -------------------------------------------------- | ---------------------------------------- | | `Invalid username or password` | Błędny e‑mail lub hasło | Sprawdź dane logowania | | `Token expired` | Wysłano tam token, którego nie da się zweryfikować | Loguj się e‑mailem i hasłem, nie tokenem | | `Email isnt confrimed` _(pisownia oryginalna)_ | Konto z niepotwierdzonym adresem e‑mail | Potwierdź e‑mail w panelu Senuto | ```json filename="przykład — błędne dane logowania (HTTP 418)" { "success": false, "data": { "error": { "type": "unauthorized", "message": "Invalid username or password" } } } ``` ### `302` → `404` — nieważny token na zwykłym endpoincie > **Błąd:** > Token po terminie ważności, token uszkodzony i całkowity brak nagłówka `Authorization` dają ten sam > wynik: `302` z nagłówkiem `Location` na `/api//users/login`, a pod tym adresem > `404 {"success": false, "message": ""}`. Który z tych dwóch statusów zobaczysz, zależy od klienta HTTP. `curl -L`, `requests` i `axios` podążają za przekierowaniem i raportują końcowe `404`. Postman z wyłączonym podążaniem za przekierowaniami zatrzyma się na `302`. Oba znaczą to samo i jedno i drugie naprawia świeży token. Jeśli integracja działała miesiąc i nagle „wszystkie endpointy zniknęły", to wygasł token, a nie zmieniło się API (patrz [Błędy → status `404`](/types/errors)). ### `403` — konto bez dostępu do API Token jest ważny, ale konto nie ma aktywnego planu albo dodatku Dostęp do API. Treść odpowiedzi jest pusta (`{"success": false, "message": ""}`), więc rozpoznajesz ten przypadek po samym statusie. Co dokupić: [Dostęp do API](/access). ## Co dalej - [Pierwsze kroki](/get-started) — pierwsze zapytanie krok po kroku. - [Analiza widoczności](/modules/visibility_analysis), [Monitoring](/modules/rank_tracker), [Baza słów kluczowych](/modules/keywords_analysis) — listy endpointów z interaktywnym playgroundem (token wklejasz w pole **API token** na stronie endpointu). - [Błędy](/types/errors) — pełna koperta błędów i status `418`. --- # Limity zapytań Każdy moduł Senuto ma **limit zapytań** rozliczany okresowo. Ta strona opisuje **wspólny model** limitów w całym systemie — zasady są takie same niezależnie od modułu, różni się tylko to, co zwiększa dany licznik i gdzie sprawdzisz jego stan. ## Jak działają limity Reguły wspólne dla wszystkich modułów: - **Licznik per moduł.** Każdy moduł ma własny limit; liczniki nie sumują się między modułami (wyjątek: narzędzia współdzielą jeden — patrz niżej). - **Okres rozliczeniowy: miesięczny, liczony od daty startu pakietu.** Mimo sufiksów `_per_day` i `_daily_` w nazwach kluczy, na aktualnych pakietach liczniki zapytań resetują się **raz na miesiąc**, w miesięczną rocznicę uruchomienia pakietu — nie o północy i nie pierwszego dnia miesiąca. Dobowe liczniki zostały tylko na wycofanych, starszych planach. **Nie zakładaj doby** — czytaj `hours_to_reset` z [`GET /api/users/getLimits`](#podgląd-limitów-przez-api) (typowa wartość dla świeżo odnowionego pakietu to kilkaset godzin). - **Jedna pula na konto — API i aplikacja czerpią z tego samego licznika.** Sprawdzenie nowej domeny klikaniem w panelu zużywa **tę samą** jednostkę co odpowiadające mu wywołanie API; nie ma osobnego „limitu API" obok „limitu aplikacji". Dlatego wartości w panelu (Powiadomienia → Limity) i te z `getLimits` zawsze się zgadzają, a pętla w integracji potrafi wyczerpać limit, którego potrzebujesz do pracy w interfejsie. - **Wspólny dla całego konta.** Limit dotyczy konta, nie pojedynczego tokenu czy użytkownika. Ile godzin zostało do wyzerowania danego licznika, podaje pole `hours_to_reset` w [`GET /api/users/getLimits`](#podgląd-limitów-przez-api). - **Powtórzenie tego samego zapytania nie nalicza się dwa razy.** Naliczenie idzie na **treść zapytania** (domena/fraza + kraj + tryb), a nie na żądanie — powtórka w oknie _Grace Window_ jest darmowa. Szczegóły i wyjątki: [Ponowienia i Grace Window](#ponowienia-i-grace-window). - **Wartość zależy od planu.** Nie zakładaj sztywnych liczb w kodzie — odczytaj wszystkie limity naraz programowo przez [`GET /api/users/getLimits`](#podgląd-limitów-przez-api) lub sprawdź w aplikacji. - **Przekroczenie → status `418`.** To ten sam status co dla błędów walidacji — **nie** jest dedykowanym kodem rate‑limitu. Rozróżniaj przyczyny po treści błędu (patrz [Błędy](/types/errors)); część modułów zwraca czytelny komunikat o limicie. - **Osobno: limity rozmiaru odpowiedzi.** Niezależnie od limitu zapytań obowiązują limity liczby wierszy na raport — przeglądanie kolejnych stron wyników zwykle **nie** zużywa licznika zapytań (patrz [Paginacja](/types/pagination)). > **Informacja:** > Dokładne wartości limitów dla Twojego planu sprawdzisz w aplikacji: **Powiadomienia → zakładka „Limity"** — pokazuje m.in. limit zapytań, liczbę fraz w raportach, projekty w Monitoringu oraz pozostałe zapytania Bazy słów kluczowych ([panel Senuto](https://app.senuto.com)). ## Limity w poszczególnych modułach Każdy moduł liczy limit inną „jednostką" — poniżej, co dokładnie zwiększa licznik i jak rozpoznasz wyczerpanie. | Moduł | Co zwiększa licznik | Podgląd per‑domena | Sygnał po przekroczeniu | | ------------------------ | ----------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------- | | **Analiza widoczności** | Zapytanie o **nową domenę** (unikalna kombinacja `domain` + `fetch_mode` + kraj); raporty i narzędzia | `checkQuery` / `consumeLimit` _(bez osobnej strony)_ | `418` | | **Baza słów kluczowych** | Uruchomienie **nowej analizy** frazy/domeny (`getKeywords`, `getRelated`, `getQuestions`, `getStatistics`…) | — | `418` z `You have reached daily limit of queries. Daily limit value is N` | | **Monitoring** | Ręczne **odświeżenie** pozycji fraz w projekcie (licznik dobowy, na wielu pakietach bez limitu) | — | `418` | | **Analiza SERP** | Uruchomienie **nowej analizy SERP** (nowa fraza + kraj) oraz każdy `refresh` | — | `418` | | **Backlinki** | Zapytanie o dane linków dla **nowej domeny** | `checkQuery` / `consumeLimit` _(bez osobnej strony)_ | `418` | | **Narzędzia (wspólne)** | Uruchomienie narzędzia w dowolnym module (naliczane per zestaw parametrów) | — | `418` | Niuanse liczenia, o których warto wiedzieć: - **Per jednostka, nie per żądanie.** W Analizie widoczności wielokrotne odpytanie **tej samej** domeny (np. pozycje, potem wzrosty i spadki dla `zalando.pl`) to **jedna** pozycja limitu. Analogicznie w innych modułach liczy się nowa jednostka pracy (nowa fraza, nowe odświeżenie, nowa analiza), a nie każde wywołanie — patrz [Ponowienia i Grace Window](#ponowienia-i-grace-window). - **Odczyt już pobranych danych zwykle nie kosztuje.** Np. w Bazie słów kluczowych paginowanie gotowych wyników (`getGroups`, `getResults`, `getGroupKeywords`) podlega tylko limitowi liczby wierszy, nie limitowi zapytań. - **Narzędzia współdzielą jeden licznik.** Rodziny narzędzi w różnych modułach (`tools/*` Analizy widoczności, narzędzia Bazy słów kluczowych, Monitoringu, Analizy SERP) czerpią z **jednej wspólnej** puli uruchomień (`tools_daily_limit`) — planuj je łącznie. ## Ponowienia i Grace Window Ponowienie żądania po timeoucie zwykle nie naliczy limitu drugi raz. Licznik rośnie od treści zapytania, nie od pojedynczego żądania: kolejne pytanie o tę samą jednostkę jest darmowe w oknie zwanym Grace Window. Wyjątkiem jest Monitoring: każde odświeżenie pozycji zwiększa licznik `monitoring_daily_keywords_refreshes`. API nie ma nagłówka `Idempotency-Key`. Przy naliczaniu od treści zapytania nie jest potrzebny. | Moduł / licznik | Klucz naliczenia (co musi się zgadzać, by ponowienie było darmowe) | Grace Window | | --------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | | **Analiza widoczności** (`visibility_analysis_queries_per_day`) | `fetch_mode` + znormalizowana domena + `country_id` | 24 h od naliczenia | | **Baza słów kluczowych** (`keywords_analysis_queries_per_day`) | znormalizowane wejście (fraza/domena) + tryb danych + `country_id` | 24 h od naliczenia | | **Backlinki** (`backlinks_queries`) | znormalizowana domena + `country_id` | 24 h od naliczenia | | **Narzędzia** (`tools_daily_limit`) | pełny zestaw parametrów wywołania narzędzia (np. dla statystyk fraz KA: `country_id`, `keywords`, tryb danych) | 24 h od naliczenia | | **Analiza SERP** (`serp_analysis_daily_limit`) | brak Grace Window w liczniku — ale `create` zwraca **istniejące** zadanie dla tej samej frazy i kraju, wtedy nic nie nalicza | bezterminowo (dopóki zadanie istnieje na koncie) | | **Monitoring** (`monitoring_daily_keywords_refreshes`) | brak — każde odświeżenie zwiększa licznik | — | Praktyczne wnioski: - **Timeout po stronie klienta? Powtórz to samo żądanie.** Dla raportów Analizy widoczności, Bazy słów kluczowych i Backlinków ponowienie w ciągu 24 h nie zużyje drugiej jednostki, niezależnie od tego, czy pierwsze żądanie zdążyło się policzyć po stronie Senuto. - **Ponawiaj identyczny payload.** Dla narzędzi klucz liczony jest z serializowanych parametrów, więc zmiana kolejności fraz w tablicy `keywords` albo dodanie opcjonalnego parametru tworzy **nową** jednostkę. Retry wysyłaj bajt w bajt tak samo jak pierwotne żądanie. - **Chcesz najpierw sprawdzić, czy operacja już się naliczyła?** `GET /api/users/getLimits` pokaże aktualny `usage`, a dla pojedynczej domeny w Analizie widoczności — `checkQuery`. Oba są odczytowe. Nie ma natomiast rejestru pojedynczych wywołań: „czy ten konkretny request przeszedł" sprawdza się właśnie przez `usage` albo przez powtórzenie zapytania. - **Uważaj na `refresh` Analizy SERP i odświeżanie Monitoringu** — te dwie operacje naliczają się za każdym razem, więc retry po timeoucie kosztuje: ustaw wysoki timeout klienta zamiast agresywnego ponawiania. Odświeżanie Monitoringu bywa przy tym darmowe w praktyce — `monitoring_daily_keywords_refreshes` ma na wielu pakietach `limit: -1`, czyli brak limitu. Sprawdź swoje konto w [`getLimits`](#podgląd-limitów-przez-api); to jedyny licznik, który faktycznie resetuje się co dobę. - **Zadania asynchroniczne mają własny, mocniejszy mechanizm** — `task_id` i listę zadań konta. Patrz [Eksporty i zadania async → Ponowienia po timeoucie](/exports-and-tasks#ponowienia-po-timeoucie). ## Ile dokładnie wynoszą limity w moim planie? Zakres API jest ten sam w każdym pakiecie — różnią się **wysokości limitów**. Orientacyjne wartości dla standardowych pakietów podaje [cennik Senuto](https://www.senuto.com/pl/cennik/) (stan na 2026-08-02): | Limit | Lite | Basic | Advanced | Prime | | ----------------------------- | ------- | -------- | ---------- | ---------- | | Analiza widoczności | 100 | 200 | 400 | 30 000 | | Analiza linków (Backlinki) | 100 | 400 | 2 000 | 30 000 | | Baza słów kluczowych | 50 | 100 | 200 | 30 000 | | Narzędzia (wspólna pula) | 10 | 25 | 50 | 3 000 | | Analiza SERP | 200¹ | 200¹ | 200¹ | 6 000 | | Monitoring — projekty / frazy | 5 / 150 | 10 / 300 | 20 / 1 000 | 50 / 5 000 | | Wiersze w raporcie | 500 | 5 000 | 20 000 | 150 000 | ¹ Analiza SERP nie wchodzi w skład pakietów Lite, Basic i Advanced — 200 zapytań dostajesz po dokupieniu dodatku Analiza SERP. Bez niego endpointy `/api/serp_analysis/*` zwrócą błąd niezależnie od dostępu do API. > **Ostrzeżenie:** > **Tabela wyżej to zestawienie handlowe, a nie kontrakt API.** Cennik prezentuje pule w ujęciu > **miesięcznym** — i tak też działają liczniki na aktualnych pakietach, ale miesiąc liczy się > od **daty startu pakietu**, nie od pierwszego dnia kalendarzowego. Nazwy kluczy (`*_queries_per_day`, > `*_daily_limit`) są przy tym mylące: sufiks został po starszych, dobowych planach. W razie rozbieżności > obowiązuje to, co zwróci `getLimits` dla Twojego konta — z `hours_to_reset` jako źródłem prawdy > o momencie resetu. Dlatego **w kodzie nie opieraj się na tabeli**: wartości zależą też od dokupionych rozszerzeń (`extra_limit`) i indywidualnych ustaleń. Zamiast tego: - **Odczytaj swoje limity programowo** — [`GET /api/users/getLimits`](#podgląd-limitów-przez-api) zwraca komplet wartości dla Twojego konta jednym wywołaniem, bez zużywania jednostek. To jedyne źródło, które nigdy się nie zdezaktualizuje. - **W aplikacji** — Powiadomienia → zakładka „Limity". - **Przed zakupem albo przy porównywaniu pakietów** — zestawienie planów znajdziesz w [cenniku Senuto](https://www.senuto.com/pl/cennik/); szczegóły dla większych wolumenów ustala opiekun klienta. > **Informacja:** > W kodzie integracji **nie zaszywaj wartości limitów na sztywno** — odczytaj je z `getLimits` przy starcie > i reaguj na `usage`/`hours_to_reset`. Konto może dostać więcej jednostek w dowolnym momencie. ## Podgląd limitów przez API Stan wszystkich limitów konta zwraca jedno wywołanie `GET /api/users/getLimits`, niezależnie od modułu. Działa nawet bez dostępu do API i nie zużywa żadnej jednostki. **`GET /api/users/getLimits`** Przykładowe żądanie: ```json {} ``` Przykładowa odpowiedź: ```json { "success": true, "data": { "visibility_analysis_queries_per_day": { "limit": 25, "usage": 8, "extra_limit": 0, "hours_to_reset": 673 }, "keywords_analysis_queries_per_day": { "limit": 1000, "usage": 21, "extra_limit": 0, "hours_to_reset": 673 }, "tools_daily_limit": { "limit": 225, "usage": 6, "extra_limit": 0, "hours_to_reset": 673 }, "backlinks_queries": { "limit": 400, "usage": 0, "extra_limit": 0, "hours_to_reset": 673 }, "monitoring_daily_keywords_refreshes": { "limit": -1, "usage": 0, "extra_limit": 0, "hours_to_reset": 1 }, "monitored_keywords": { "limit": 300, "usage": 752, "extra_limit": 0, "hours_to_reset": null }, "projects": { "limit": 10, "usage": 6, "extra_limit": 0, "hours_to_reset": null } } } ``` Odpowiedź to mapa `klucz_limitu → { limit, usage, extra_limit, hours_to_reset }` (zwykle kilkadziesiąt kluczy — wyżej fragment). Jak czytać wartości: - **`limit`** — wartość limitu w Twoim planie. **`-1` oznacza brak limitu** (np. `monitoring_daily_keywords_refreshes` powyżej). Bywa też datą (limity ważności danych). - **`usage`** — bieżące zużycie w trwającym okresie rozliczeniowym; **`null`** dla limitów, które nie są licznikiem „zużywalnym" (np. limity rozmiaru raportu `*_rows_per_report`). - **`extra_limit`** — dodatkowe jednostki ponad plan (dokupione rozszerzenia). - **`hours_to_reset`** — ile godzin do wyzerowania licznika; `null`, gdy limit nie resetuje się cyklicznie. **To jedyny wiarygodny sygnał okresu rozliczeniowego** — wartości rzędu kilkuset godzin (jak `673` w przykładzie wyżej) to normalny cykl miesięczny, mimo `_per_day` w nazwie klucza. Klucze odpowiadają modułom, np. `visibility_analysis_queries_per_day`, `keywords_analysis_queries_per_day`, `serp_analysis_daily_limit`, `backlinks_queries`, `tools_daily_limit`, a także limity ilościowe (`monitored_keywords`, `projects`) i rozmiarowe (`*_rows_per_report`). ## Czy jest rate limit na sekundę/minutę? **Nie — API nie wymusza limitu żądań na sekundę ani na minutę.** Nie ma statusu `429`, nie ma nagłówków `X-RateLimit-*` ani `Retry-After`, a `418` sygnalizuje walidację lub wyczerpany **limit okresowy**, nie tempo wysyłki. Jedyną twardą granicą są liczniki zapytań opisane wyżej. Praktyczne konsekwencje dla integracji: - **Nie zrównoleglaj agresywnie.** Brak throttlingu w aplikacji nie znaczy, że warstwa brzegowa (proxy, WAF, ochrona przed nadużyciami) przepuści dowolny ruch — kilkadziesiąt równoległych połączeń z jednego IP może zostać odrzuconych zanim dotrą do API, i wtedy odpowiedź nie będzie miała koperty `{success, data}`. - **Sekwencyjnie z krótkim odstępem** (rzędu \~1 s) to bezpieczny domyślny tryb pracy — tak działa walidacja tej dokumentacji. - **Wąskim gardłem bywa czas odpowiedzi**, nie liczba żądań: szerokie raporty historyczne liczą się długo (patrz [Eksporty i zadania async](/exports-and-tasks)). - **Retry rób z backoffem** i wyłącznie dla `5xx` oraz timeoutów — ponawianie `418` nic nie zmieni, bo to odrzucone żądanie, a nie chwilowa awaria. Retry po timeoucie jest bezpieczny kosztowo, patrz [Ponowienia i Grace Window](#ponowienia-i-grace-window). ## Jak rozpoznać wyczerpanie limitu - **Zawczasu, programowo** — `GET /api/users/getLimits` pokazuje `usage`/`limit`/`hours_to_reset` dla **wszystkich** modułów naraz. Jest odczytowe — nie zużywa limitu. - **W trakcie** — po przekroczeniu żądanie kończy się statusem **`418`**. Część modułów dołącza czytelny komunikat (np. Baza słów kluczowych: `You have reached daily limit of queries. Daily limit value is N` — komunikat mówi „daily", ale dotyczy limitu okresu rozliczeniowego). - **W aplikacji** — bieżący stan wszystkich limitów: Powiadomienia → zakładka „Limity". > **Błąd:** > `418` **nie jest** dedykowanym kodem rate‑limitu — w tym API sygnalizuje też błędy walidacji i autoryzacji. Nie zakładaj, że każde `418` to wyczerpany limit; zajrzyj do treści błędu (patrz [Błędy](/types/errors)) i — gdzie to możliwe — potwierdź stan przez `getLimits`. ## Dobre praktyki - **Sprawdzaj przed serią** przez `getLimits` — nie zużywa limitu, a chroni przed nieoczekiwanym `418`. - **Cache’uj po stronie klienta** — dane raportów zmieniają się najwyżej raz dziennie; nie powtarzaj tego samego zapytania. - **Rób odstępy między żądaniami** — przy większej liczbie wywołań dodaj krótkie opóźnienie (rzędu \~1 s). - **Paginuj zamiast wielkich `limit`** — przeglądanie stron zwykle nie zużywa licznika zapytań (patrz [Paginacja](/types/pagination)). - **Planuj narzędzia łącznie** — wszystkie narzędzia dzielą jedną pulę. - **Retry po timeoucie wysyłaj identycznie** — ten sam payload trafia w Grace Window i nie kosztuje drugiej jednostki (patrz [Ponowienia i Grace Window](#ponowienia-i-grace-window)). - **Nie zakładaj resetu o północy** — planując harmonogram integracji, odczytuj `hours_to_reset`. ## Powiązane strony - [Autoryzacja](/authorization) — token Bearer i błędy autoryzacji. - [Błędy](/types/errors) — koperta błędu i znaczenie statusu `418`. - [Paginacja](/types/pagination) — `limit`/`page` i limity rozmiaru odpowiedzi. --- # Eksporty i zadania asynchroniczne Trzy różne sposoby wyciągania danych z API — mylenie ich to najczęstsza przyczyna timeoutów przy „dużych pobraniach". | Tryb | Kiedy | Co dostajesz | | -------------------------- | -------------------------------------------------- | ---------------------------------------------------------------- | | **Raport synchroniczny** | domyślnie, większość endpointów | JSON w odpowiedzi, stronicowany ([paginacja](/types/pagination)) | | **Zadanie asynchroniczne** | narzędzia SERP (crawl SERP-a, TOP100, listy URL-i) | `task_id` od razu, dane po zakończeniu zadania | | **Eksport plikowy** | gdy chcesz plik zamiast JSON-a | CSV/plik do pobrania, bez koperty `{success, data}` | > **Ostrzeżenie:** > **Nie ma ogólnego mechanizmu „zleć raport i odbierz później".** Zwykłe raporty > (np. `positions/getData`, `dashboard/getDomainStatistics`) liczą się w trakcie żądania — > nie zwracają identyfikatora zadania i nie da się ich zakolejkować. Duże pobranie dzielisz > **paginacją i zakresem dat**, a nie zleceniem w tle. Kolejkowanie istnieje wyłącznie > w rodzinach `tasks/management/*` opisanych niżej. ## Jak rozpoznać, w którym trybie działa Twój endpoint Tryb rozpoznasz po ścieżce i wymaganych parametrach: | Sygnał w endpoincie | Tryb | Co to znaczy | | ------------------------------------------------ | -------------------------- | ----------------------------------------------------------------- | | ścieżka zaczyna się od `/api/tasks/management/…` | **zadanie asynchroniczne** | `create` zwraca `task_id`, wyniki odbierasz po `check` | | endpoint **wymaga `task_id`** w parametrach | odbiór wyników zadania | zadanie musi być już zakończone | | w ścieżce jest segment **`exports`** | **eksport plikowy** | odpowiedzią jest plik (CSV), nie JSON | | nic z powyższych | **raport synchroniczny** | dane liczą się w trakcie żądania; brak job ID i brak kolejkowania | Innymi słowy: **job ID istnieje wyłącznie w trzech rodzinach `tasks/management/*`** wymienionych niżej. Jeśli Twojego endpointu tam nie ma i nie przyjmuje `task_id`, to nie da się go „zlecić i odebrać później" — zostaje paginacja, zawężenie zakresu albo eksport plikowy. --- ## 1. Duże pobranie z raportu synchronicznego Gdy raport przerywa się timeoutem (`data.error.type = "timeout"` — patrz [Błędy](/types/errors)), zawężaj żądanie, zamiast je ponawiać w tej samej postaci: - **Stronicuj.** `limit` + `page`; łączną liczbę wierszy podaje `pagination.count`, a `pagination.has_next_page` mówi, kiedy przestać. Maksymalny `limit` zależy od endpointu (często 100) — wyższa wartość to `418`, nie szybsze pobranie. - **Tnij zakres dat.** Raporty historyczne (`date_min` / `date_max`) kosztują tym więcej, im szerszy zakres i im większa domena. Miesiąc po miesiącu przechodzi tam, gdzie rok naraz się wywraca. - **Ogranicz zbiór na wejściu.** `filtering` po stronie API jest tańsze niż pobranie wszystkiego i filtrowanie u siebie. - **Rozważ eksport plikowy** (sekcja 3) — dla tych samych danych zwraca CSV jednym żądaniem, bez składania stron. > **Informacja:** > Odpowiedź `200` z pustym `data: []` to nie timeout ani błąd — dla tej domeny i tego zakresu > po prostu nie ma danych. Sprawdź `fetch_mode`, zakres dat i pisownię domeny. --- ## 2. Zadania asynchroniczne (`tasks/management/*`) Narzędzia, które muszą najpierw zebrać dane z Google, działają w cyklu **`create` → `check` (polling) → odbiór wyników**. `create` zwraca `task_id` natychmiast; dane pojawiają się dopiero, gdy zadanie się zakończy. | Rodzina | `create` | `check` | Wyniki | | ---------------- | --------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | **Analiza SERP** | [`POST /api/tasks/management/serp_analysis/create`](/modules/serp_analysis/serp-task-create) | [`GET …/serp_analysis/check`](/modules/serp_analysis/serp-task-check) | raporty [`serp_analysis/reports/*`](/modules/serp_analysis) | | **TOP100 crawl** | [`POST /api/tasks/management/top100_crawl/create`](/modules/serp_analysis/serp-tool-top100-create) | [`GET …/top100_crawl/check`](/modules/serp_analysis/serp-tool-top100-check) | [`top100_crawl/getData`](/modules/serp_analysis/serp-tool-top100-getData) lub [eksport CSV](/modules/serp_analysis/serp-export-top100) | | **URLs crawl** | [`POST /api/tasks/management/urls_crawl/create`](/modules/serp_analysis/serp-tool-urlscrawl-create) | [`GET …/urls_crawl/check`](/modules/serp_analysis/serp-tool-urlscrawl-check) | [`urls_crawl/getList`](/modules/serp_analysis/serp-tool-urlscrawl-getList) lub [eksport CSV](/modules/serp_analysis/serp-export-urlscrawl) | ### Skąd wiadomo, że zadanie jest gotowe `check` przyjmuje `task_id` **w query stringu** i zwraca obiekt zadania. Sygnał gotowości różni się między rodzinami — dlatego czytaj konkretne pole, a nie „jakikolwiek status": - **Analiza SERP** — `progress.has_serp_data = true` (crawl SERP gotowy) oraz `progress.has_keywords_analysis_data = true` (dane Bazy słów gotowe). Odpytanie raportu wcześniej zwraca błąd walidacji `Unfinished task`. - **TOP100 / URLs crawl** — `status = "completed"` (świeżo utworzone zadanie ma `waiting`) oraz `progress = {all, finished, unfinished}`, po którym widać postęp. Czas realizacji zależy od rodzaju i wielkości zadania — od minut po godziny. **Nie zakładaj stałej wartości**: odpytuj `check` w odstępach (np. co 30–60 s) i przerwij po własnym limicie czasu. ### Przechowuj `task_id` `create` zwraca identyfikator tylko raz — **zapisz go po swojej stronie**. Zadanie nie wygasa: raporty zakończonego zadania odpytasz również następnego dnia, bez tworzenia nowego i bez konsumpcji limitu. Zgubiony `task_id` oznacza zlecenie analizy od nowa, czyli kolejną jednostkę limitu. ### Szkic pętli **Python** ```python filename="polling.py" import time, requests H = {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"} BASE = "https://api.senuto.com" # 1. zlecenie zadania — zużywa limit narzędzi (tools_daily_limit) task = requests.post(f"{BASE}/api/tasks/management/top100_crawl/create", headers=H, json={"keywords": ["buty"], "country_id": 1}).json() task_id = task["data"]["id"] # 2. polling — task_id w QUERY STRINGU, nie w ciele deadline = time.time() + 3600 while time.time() < deadline: state = requests.get(f"{BASE}/api/tasks/management/top100_crawl/check", headers=H, params={"task_id": task_id}).json()["data"] if state["status"] == "completed": break time.sleep(60) else: raise TimeoutError(f"zadanie {task_id} nie zdążyło — sprawdź je później przez /list") # 3. odbiór wyników rows = requests.post(f"{BASE}/api/serp_analysis/tools/top100_crawl/getData", headers=H, json={"task_id": task_id, "limit": 100, "page": 1}).json() ``` **Pseudokod** ```text filename="cykl zadania" task_id = create(parametry) → status "waiting" powtarzaj: stan = check(task_id) → task_id w query stringu jeśli gotowe (patrz wyżej) → przerwij jeśli przekroczono własny limit czasu → przerwij i wróć później przez list() czekaj 30–60 s wyniki = getData/getList(task_id) → albo eksport CSV ``` > **Ostrzeżenie:** > **`create` zużywa limit** — `serp_analysis_daily_limit` dla Analizy SERP, > `tools_daily_limit` (plus limit wejściowy narzędzia) dla crawlów. `check`, `list` i odczyt > wyników limitu nie zużywają, więc polling jest bezpieczny — podobnie jak ponowienie `create` > po timeoucie ([Ponowienia po timeoucie](#ponowienia-po-timeoucie)). Stan liczników: > [`GET /api/users/getLimits`](/rate-limits). --- ## 3. Eksporty plikowe (CSV) Endpointy z segmentem `exports` **zwracają plik**, nie kopertę JSON — nie mają playgroundu i nie parsuj ich jak zwykłej odpowiedzi. Przydają się tam, gdzie alternatywą jest przejście przez kilkadziesiąt stron paginacji. | Moduł | Endpoint | Zawartość | | -------------------- | ------------------------------------------------------------------ | --------------------------- | | Analiza SERP | `POST /api/serp_analysis/tools/exports/top100_crawl/getData` | wyniki zadania TOP100 crawl | | Analiza SERP | `POST /api/serp_analysis/tools/exports/urls_crawl/getList` | wyniki zadania URLs crawl | | Baza słów kluczowych | `POST /api/keywords_analysis/tools/exports/statistics/getKeywords` | frazy z raportu statystyk | > **Ostrzeżenie:** > Eksporty narzędziowe (`tools/exports/*`) **zużywają `tools_daily_limit`** — tak samo jak > wywołanie samego narzędzia. Powtórzenie **tego samego** eksportu w ciągu 24 h już nie nalicza > kolejnej jednostki (patrz [Ponowienia po timeoucie](#ponowienia-po-timeoucie)), ale eksport > z innym zestawem parametrów to nowa jednostka. --- ## Ponowienia po timeoucie Timeout po stronie klienta nie mówi, czy operacja wykonała się po stronie Senuto. **Nie ma nagłówka `Idempotency-Key`** — i nie jest potrzebny, bo każdy z trzech trybów ma własny sposób bezpiecznego ponowienia. Wybierz właściwy dla swojego endpointu: | Tryb | Jak sprawdzić, czy operacja przeszła | Czy ponowienie kosztuje | | ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Raport synchroniczny** | `usage` w [`GET /api/users/getLimits`](/rate-limits#podgląd-limitów-przez-api); dla domeny w Analizie widoczności — `checkQuery` | **Nie** — identyczne zapytanie w oknie 24 h trafia w Grace Window ([szczegóły](/rate-limits#ponowienia-i-grace-window)) | | **Zadanie async** (`tasks/management/*`) | `…/list` — zwraca wszystkie zadania konta z ich `task_id` i statusami | Analiza SERP: **nie** (`create` zwraca istniejące zadanie dla tej samej frazy i kraju). Crawle: powstanie **nowe** zadanie, ale `tools_daily_limit` nie naliczy się drugi raz dla identycznego payloadu w 24 h | | **Narzędzie z `public_id`** (KA `statistics/create`) | brak listy — `public_id` odzyskasz tylko z odpowiedzi, `checkData` sprawdza znany `public_id` | **Nie** dla identycznego payloadu w oknie 24 h, ale wynik dostaniesz pod **nowym** `public_id` | | **Eksport plikowy** | brak — to zwykłe pobranie pliku | **Nie** dla powtórzenia tego samego eksportu w 24 h | Zasady, które warto zaszyć w kliencie: - **Retry wysyłaj bajt w bajt.** Klucz Grace Window liczy się z parametrów żądania, więc zmieniona kolejność fraz w `keywords` albo dorzucony opcjonalny parametr tworzy nową jednostkę limitu. - **Zanim ponowisz `create` zadania — zajrzyj do `list`.** To najtańszy sposób ustalenia, czy zadanie już istnieje, i jedyny sposób odzyskania zgubionego `task_id`. - **`public_id` zapisuj natychmiast po odebraniu odpowiedzi.** Jest generowany losowo i nie ma endpointu, który wylistuje wyniki narzędzi konta — utracony `public_id` oznacza ponowne wywołanie `create` (kosztowo bezpieczne w oknie 24 h, ale wynik dostaniesz pod nowym identyfikatorem). - **Podnieś timeout, zamiast ponawiać agresywnie.** Ciężkie wywołania (`create` z kilkuset frazami, szerokie raporty historyczne) potrafią liczyć się minutami — ustaw timeout klienta rzędu 120 s. - **Dwie operacje naliczają się przy każdym wywołaniu** i nie mają Grace Window: `refresh` w Analizie SERP oraz ręczne odświeżanie pozycji w Monitoringu. Tutaj retry realnie kosztuje. ## Co dalej - [Paginacja](/types/pagination) — `limit`, `page` i pole `pagination`. - [Limity zapytań](/rate-limits) — które liczniki zużywają zadania i eksporty. - [Błędy](/types/errors) — `timeout`, `418` i pozostałe kategorie. - [Analiza SERP](/modules/serp_analysis) — pełny opis rodzin zadań i raportów. --- # API, MCP czy no-code — co wybrać Do danych Senuto prowadzą trzy drogi. Różnią się nie zakresem danych, tylko tym, **kto pisze integrację**. | | REST API | Serwer MCP | Aplikacja w Make | | ---------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------- | ---------------------------------------------------- | | **Dla kogo** | deweloper, własny kod | asystent AI (Claude, inny klient MCP) | osoba budująca automatyzacje bez kodu | | **Zakres** | pełny — 124 udokumentowane endpointy | [26 narzędzi](/mcp#co-potrafi-serwer-mcp) — wybrane raporty, opisane dla modelu | kilkanaście gotowych modułów | | **Kto decyduje, co pobrać** | Ty, w kodzie | model, na podstawie Twojego polecenia | Ty, klikając scenariusz | | **Uwierzytelnienie** | token z `POST /api/users/token` | OAuth przy pierwszym użyciu | połączenie w Make | | **Kiedy to najlepszy wybór** | własny produkt, hurtownia, cykliczne pipeline'y | analiza ad hoc, „zapytaj i pokaż" | przepływy między narzędziami (Sheets, Slack, Notion) | > **Informacja:** > Te drogi się nie wykluczają. Typowy układ: **MCP** do zadawania pytań na bieżąco, **Make** do prostych > automatyzacji, **REST API** tam, gdzie potrzebna jest pełna kontrola i cała powierzchnia danych. ## Serwer MCP — Senuto w Claude i innych klientach AI [MCP](https://modelcontextprotocol.io/) (Model Context Protocol) to standard, którym asystenci AI łączą się z zewnętrznymi źródłami danych. Senuto udostępnia **zdalny serwer MCP**, więc nie musisz niczego instalować lokalnie: ``` https://mcp.senuto.com/mcp ``` Podłączenie zajmuje minutę — w Claude Code jedną komendą: ```bash claude mcp add senuto --transport http https://mcp.senuto.com/mcp ``` Przy pierwszym użyciu klient przeprowadzi Cię przez **logowanie OAuth**; **nie pobierasz żadnego tokena ręcznie** i nie używasz tu tokena REST API. Pełny opis — instrukcje dla pozostałych klientów, lista 26 narzędzi wraz z odpowiednikami w REST API, rozliczanie limitów i różnice wobec API — jest na stronie **[Serwer MCP](/mcp)**. > **Ostrzeżenie:** > Dostęp do serwera MCP to **osobne uprawnienie niż dodatek API**: mają go plany Advanced i Prime, > dodatek „Dostęp do API + MCP" oraz tańszy dodatek „Dostęp do MCP" (sam MCP, bez bezpośredniego > REST API). Szczegóły: [Czego potrzebujesz](/mcp#czego-potrzebujesz). ## Make.com — gotowa aplikacja i tryb HTTP W Make istnieje **natywna aplikacja Senuto** (kilkanaście modułów typu _action_ i _search_: dane domeny, statystyki projektów, wykresy widoczności, konkurenci, kanibalizacje, snippety, lista krajów). Do prostych scenariuszy — „raz w tygodniu wrzuć widoczność do Arkusza" — wystarcza i nie wymaga tokenu API w kodzie. Aplikacja pokrywa jednak **wycinek** tego, co daje REST API. Gdy brakuje Ci modułu — na przykład konkretnego raportu Monitoringu albo eksportu — użyj w Make **modułu HTTP** i wywołaj endpoint wprost: - URL: `https://api.senuto.com/api////` - nagłówki: `Authorization: Bearer ` oraz `Content-Type: application/json` - treść: JSON dokładnie taki, jak na stronie danego endpointu Tak samo robi się to w **n8n**, Zapierze i każdym innym narzędziu z krokiem HTTP — API nie wymaga niczego poza zwykłym żądaniem z nagłówkiem. > **Informacja:** > Spotkasz w sieci starsze przykłady ze ścieżką `/api/integromat/...` — to **historyczny alias** z czasów, > gdy Make nazywał się Integromat. Nadal działa, ale w nowych integracjach używaj kanonicznej ścieżki > `/api//…`, którą opisuje ta dokumentacja. ## Awarie i status usługi Bieżące informacje o pracach i awariach: **[komunikat.senuto.com](https://komunikat.senuto.com)** (strona tymczasowa). Zanim tam zajrzysz, warto wykluczyć problem po swojej stronie — [jak odróżnić awarię od własnego błędu](/types/errors#500--503--awaria-czy-mój-błąd). ## Co dalej - [Dostęp do API](/access) - [Pierwsze kroki](/get-started) - [Serwer MCP](/mcp) --- # Serwer MCP Senuto [MCP](https://modelcontextprotocol.io/) (Model Context Protocol) to standard, którym asystenci AI łączą się z zewnętrznymi źródłami danych. **Serwer MCP Senuto** wystawia dane Senuto jako narzędzia, po które model sięga sam: pytasz w Claude, ChatGPT czy Cursorze o widoczność domeny, a asystent pobiera liczby z Senuto, zamiast je zgadywać. Serwer jest **zdalny** — nie instalujesz niczego lokalnie: ``` https://mcp.senuto.com/mcp ``` Transport: **HTTP**. Uwierzytelnienie: **OAuth przy pierwszym użyciu** — nie pobierasz i nie wklejasz żadnego tokena. > **Informacja:** > MCP prowadzi do **tych samych danych** co [REST API](/get-started) — różni się tym, kto pisze > integrację: przy REST Ty w kodzie, przy MCP model na podstawie Twojego polecenia. Porównanie trzech > dróg (API, MCP, no-code): [API, MCP czy no-code](/api-mcp-nocode). ## Czego potrzebujesz MCP i REST API to **osobne uprawnienia** na koncie: | Plan / dodatek | REST API | Serwer MCP | | ------------------------------------------------------- | ------------- | ------------- | | **Advanced**, **Prime** | w cenie planu | w cenie planu | | Dodatek **„Dostęp do API + MCP"** (Basic, Lite) | tak | tak | | Dodatek **„Dostęp do MCP"** — 69 zł/mies. (Basic, Lite) | **nie** | tak | Dodatek włączasz samodzielnie w aplikacji: **[app.senuto.com/user/user-package](https://app.senuto.com/user/user-package)**. Ceny: stan na 2026-09-06, aktualne w [cenniku Senuto](https://www.senuto.com/pl/cennik/). Warunki dodatku API (rozliczenie miesięczne, rezygnacja przez nieprzedłużenie) opisuje strona [Dostęp do API](/access) — dodatek MCP działa tak samo. > **Ostrzeżenie:** > **Sam dodatek MCP nie odblokowuje REST API.** Serwer MCP uwierzytelnia się w API Senuto po swojemu; > Twoje własne żądanie wysłane wprost na `api.senuto.com` z takim kontem dostanie `403` z pustą treścią > (`{"success": false, "message": ""}`). Jeśli obok asystenta chcesz pisać własne integracje, potrzebujesz > dodatku obejmującego API. ## Podłączenie ### Dodaj serwer w swoim kliencie **Claude Code** ```bash claude mcp add senuto --transport http https://mcp.senuto.com/mcp ``` **Claude (aplikacja)** _Ustawienia → Konektory → Dodaj własny konektor_ — jako adres podaj `https://mcp.senuto.com/mcp`. Działa tak samo w aplikacji desktopowej i w wersji przeglądarkowej. **ChatGPT** _Settings → Connectors → Advanced → Developer mode → Create_ — jako adres podaj `https://mcp.senuto.com/mcp`. **Cursor / VS Code** Cursor — `.cursor/mcp.json` w projekcie albo w konfiguracji globalnej: ```json filename=".cursor/mcp.json" { "mcpServers": { "senuto": { "url": "https://mcp.senuto.com/mcp" } } } ``` VS Code (Copilot) — sekcja `mcp` w `settings.json`: ```json filename="settings.json" { "mcp": { "servers": { "senuto": { "type": "http", "url": "https://mcp.senuto.com/mcp" } } } } ``` **Gemini CLI** `~/.gemini/settings.json`: ```json filename="~/.gemini/settings.json" { "mcpServers": { "senuto": { "httpUrl": "https://mcp.senuto.com/mcp" } } } ``` Dla Codex CLI (`~/.codex/config.toml`) i pozostałych klientów aktualne fragmenty konfiguracji trzyma [mcp.senuto.com](https://mcp.senuto.com). ### Zaloguj się przez OAuth Klient wyświetli monit przy pierwszym użyciu i przeprowadzi Cię przez logowanie do Senuto. **Nie generujesz tu żadnego tokena ręcznie** i nie używasz tokena REST API — to dwie różne rzeczy. ### Sprawdź, że działa Poproś asystenta o coś, czego nie wie z głowy — np. _„sprawdź w Senuto widoczność zalando.pl"_. Powinien wywołać narzędzie `get_domain_statistics` i pokazać liczby. Jeśli odpowiada bez wywołania narzędzia, patrz [Gdy coś nie działa](#gdy-coś-nie-działa). ## Co potrafi serwer MCP **26 narzędzi** pokrywających cztery moduły plus odczyt limitów. Poniżej mapa: narzędzie MCP → odpowiadający mu endpoint REST, gdybyś chciał to samo zrobić we własnym kodzie. > **Informacja:** > Stan na **2026-09-15**. Źródłem prawdy jest lista narzędzi, którą serwer zgłasza Twojemu klientowi — > zestaw może się zmieniać częściej niż ta strona. ### Analiza widoczności | Narzędzie MCP | Co zwraca | Odpowiednik w REST API | | ------------------------------ | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | | `get_domain_statistics` | widoczność domeny, TOP3/10/50, ranking, ekwiwalent Ads oraz liczba fraz z AI Overview i cytowań domeny w nim | [`dashboard/getDomainStatistics`](/modules/visibility_analysis/va-dashboard-getDomainStatistics) | | `get_positions_data` | frazy, na które rankuje domena, z pozycjami i widocznością | [`positions/getData`](/modules/visibility_analysis/positions) | | `get_competitors` | konkurenci domeny z porównaniem metryk | [`competitors/getData`](/modules/visibility_analysis/va-competitors-getData) | | `get_cannibalization_keywords` | frazy, o które konkuruje kilka własnych URL-i | [`cannibalization/getKeywords`](/modules/visibility_analysis/va-cannibalization-getKeywords) | | `get_characteristics_table` | rozkład fraz wg cech (długość, trendy, wyszukiwania, trudność) | [`keywords/getCharacteristicsTable`](/modules/visibility_analysis/va-keywords-getCharacteristicsTable) | | `get_keyword_history` | historia pozycji pojedynczej frazy | [`positions/getKeywordHistory`](/modules/visibility_analysis/va-positions-getKeywordHistory) | | `get_positions_history_chart` | historia rozkładu pozycji (dane do wykresu) | — (poza zakresem tej dokumentacji) | | `get_subdomains` | widoczność w podziale na subdomeny | [`sections/getSubdomains`](/modules/visibility_analysis/va-sections-getSubdomains) | | `get_urls` | pojedyncze URL-e rankujące w wynikach | [`sections/getUrls`](/modules/visibility_analysis/va-sections-getUrls) | | `suggest_domains` | podpowiedzi domen do wyszukiwarki | — (poza zakresem tej dokumentacji) | | `get_countries_list` | lista obsługiwanych rynków | [Kraje i `country_id`](/countries) | ### Baza słów kluczowych | Narzędzie MCP | Co zwraca | Odpowiednik w REST API | | ------------------------ | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `get_keyword_statistics` | wyszukiwania, CPC i trend dokładnie tych fraz, które podasz — do 500 na wywołanie | [`tools/statistics/create`](/modules/keywords_analysis/ka-statistics-create) → [`checkData`](/modules/keywords_analysis/ka-statistics-checkData) → [`getKeywords`](/modules/keywords_analysis/ka-statistics-getKeywords) | | `get_keywords` | propozycje fraz dla frazy, domeny albo URL-a | [`keywords/getKeywords`](/modules/keywords_analysis/ka-keywords-getKeywords) | | `get_groups` | grupy semantyczne dla frazy | [`keywords/getGroups`](/modules/keywords_analysis/ka-keywords-getGroups) | | `get_questions` | frazy pytające powiązane z frazą | [`keywords/getQuestions`](/modules/keywords_analysis/ka-keywords-getQuestions) | > **Ostrzeżenie:** > `get_keywords` nie zwraca wyszukiwań frazy, którą podasz — zwraca inne frazy do niej dopasowane, > a sam seed często nie ma w wynikach własnego wiersza. Wyszukiwania konkretnych fraz daje > `get_keyword_statistics`: jeden wiersz na frazę, `found: false`, gdy Senuto nie ma dla niej danych, > i `searches: 0`, gdy fraza jest znana, ale bez wolumenu. Narzędzia Bazy słów kluczowych obsługują `country_id`: `1` (PL), `50` (CZ), `53` (DK), `82` (HU), `134` (NL), `153` (RO), `160` (SE), `164` (SK). ### Monitoring (Rank Tracker) | Narzędzie MCP | Co zwraca | Odpowiednik w REST API | | ---------------------------- | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | | `rt_get_active_projects` | Twoje aktywne projekty | [`projects/getMyActiveProjects`](/modules/rank_tracker/rt-projects-getMyActiveProjects) | | `rt_get_projects_list` | lista projektów z rozszerzonymi danymi | [`projects/getListWithExtendedData`](/modules/rank_tracker/rt-projects-getListWithExtendedData) | | `rt_list_groups` | wszystkie grupy fraz w projekcie (pobiera każdą stronę) | [`groups/list`](/modules/rank_tracker/rt-groups-list) | | `rt_get_project_keywords` | frazy monitorowane w projekcie (id + treść, bez pozycji) | [`keywords/getProjectKeywords`](/modules/rank_tracker/rt-keywords-getProjectKeywords) | | `rt_get_position_data` | dzienna historia pozycji fraz w zakresie dat | [`positions/getData`](/modules/rank_tracker/rt-positions-getData) | | `rt_get_snippets_statistics` | statystyki snippetów i funkcji SERP w projekcie | [`snippets/getStatistics`](/modules/rank_tracker/rt-snippets-getStatistics) | ### Analiza SERP Ten sam przepływ asynchroniczny co w REST API ([Eksporty i zadania async](/exports-and-tasks)): zlecasz analizę, odpytujesz o status, dopiero potem czytasz raporty. | Narzędzie MCP | Co robi | Odpowiednik w REST API | | ----------------- | -------------------------------------------------------------------- | ----------------------------------------------------------------- | | `serp_create` | zleca analizę SERP dla frazy — **zużywa limit** | [`serp_analysis/create`](/modules/serp_analysis/serp-task-create) | | `serp_check` | status zadania (`has_serp_data`, `has_keywords_analysis_data`) | [`serp_analysis/check`](/modules/serp_analysis/serp-task-check) | | `serp_list` | Twoje zadania SERP, od najnowszych | — (poza zakresem tej dokumentacji) | | `serp_get_report` | raport z gotowego zadania (9 udokumentowanych rodzajów, patrz niżej) | strony raportów w [module SERP](/modules/serp_analysis) | Raporty dostępne w `serp_get_report` (parametr `report`): [`urls`](/modules/serp_analysis/serp-urls-getList), [`content_statistics`](/modules/serp_analysis/serp-content-getStatistics), [`keyword_stats`](/modules/serp_analysis/serp-keyword-getStatistics), [`competitors_number`](/modules/serp_analysis/serp-keyword-getCompetitorsNumber), [`topic_leaders`](/modules/serp_analysis/serp-keyword-getTopicLeaders), [`groups`](/modules/serp_analysis/serp-keyword-getGroups), [`questions`](/modules/serp_analysis/serp-keyword-getQuestions), [`related_keywords`](/modules/serp_analysis/serp-keyword-getRelatedKeywords), [`keywords_propositions`](/modules/serp_analysis/serp-keyword-getKeywordsPropositions). Parametr `report` przyjmuje jeszcze `titles`, ale ten raport zwraca pustą listę na każdym zadaniu. Tytuły stron rankujących znajdziesz w raporcie `urls`. ### Konto | Narzędzie MCP | Co zwraca | Odpowiednik w REST API | | ------------- | ----------------------------------------------------------- | -------------------------------------------------------------------- | | `get_limits` | stan wszystkich limitów konta naraz; nie zużywa żadnej puli | [`GET /api/users/getLimits`](/rate-limits#podgląd-limitów-przez-api) | ## Limity — jedna pula na konto Wywołania przez MCP **czerpią z tych samych liczników co REST API i klikanie w aplikacji**. Nie ma osobnego „limitu MCP": zapytanie asystenta o nową domenę zużywa tę samą jednostkę co odpowiadające mu żądanie REST. Zasady, okresy rozliczeniowe i _Grace Window_ opisuje strona [Limity zapytań](/rate-limits). W praktyce, przy pracy z asystentem: - **`get_limits` jest darmowe** — możesz kazać modelowi sprawdzać stan puli, ile chcesz. - **`serp_create` kosztuje** jednostkę `serp_analysis_daily_limit`. Zanim zlecisz nową analizę, warto sprawdzić `serp_list` — raporty **istniejącego, zakończonego** zadania dla tej samej frazy pobierzesz bez naliczenia. - **Czytanie gotowych danych zwykle nie kosztuje** — paginacja i kolejne raporty tego samego zadania podlegają limitom liczby wierszy, nie limitowi zapytań. - **`get_keyword_statistics` i `get_keywords` czerpią z osobnych pul.** Pierwsze liczy się do `tools_daily_limit` i kosztuje jedną jednostkę na wywołanie, choćby niosło 500 fraz. Drugie liczy się do `keywords_analysis_queries_per_day`, po jednostce za każdy seed — pięć seedów to pięć jednostek, w jednym wywołaniu czy w pięciu. - **Pętla w rozmowie potrafi wyczerpać limit**, którego potrzebujesz do pracy w panelu — to ta sama pula. ## Czym MCP różni się od REST API | | Serwer MCP | REST API | | -------------------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | | **Zakres** | 26 narzędzi, wybrane raporty | 124 udokumentowane endpointy | | **Rynki (Analiza widoczności)** | `1` (PL 1.0), `50` (CZ), `164` (SK), `200` (PL 2.0) | wszystkie [obsługiwane rynki](/modules/visibility_analysis#obsługiwane-rynki-bazy-krajowe) | | **Rynki (Baza słów kluczowych)** | `1`, `50`, `53`, `82`, `134`, `153`, `160`, `164` — dla Polski `1`, bez `200` | jak wyżej | | **Wielkość odpowiedzi** | `detail_level`: `summary` / `standard` / `extended` | pełna odpowiedź + [paginacja](/types/pagination) | | **Strona wyników** | do 100 rekordów (`rt_get_position_data`: do 50) | limity per endpoint | | **Zapis** | tylko `serp_create` (zlecenie analizy) — reszta to odczyt | pełna powierzchnia odczytowa modułów | | **Kto ustala parametry** | model, na podstawie Twojego polecenia | Ty, w kodzie | > **Ostrzeżenie:** > **Baza 2.0 dla Polski (`country_id: 200`) nie działa w narzędziach Bazy słów kluczowych** — tam użyj > `country_id: 1`. W Analizie widoczności `200` jest dostępne i to nadal > [zalecana baza](/modules/visibility_analysis#obsługiwane-rynki-bazy-krajowe). > **Informacja:** > Parametry dobiera model, więc **warto je weryfikować** — zwłaszcza rynek (`country_id`) i tryb domeny > (`fetch_mode`: `topLevelDomain` czy `subdomain`). Najprościej podać je wprost w poleceniu: > _„…dla `senuto.com`, cała domena, baza 2.0"_. Do powtarzalnych, rozliczanych raportów zamiast MCP > użyj REST API — tam parametry są w Twoim kodzie, nie w interpretacji promptu. ## Gdy coś nie działa | Objaw | Przyczyna i co zrobić | | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | | Logowanie OAuth się nie kończy albo klient nie łączy się z serwerem | sprawdź, czy Twoje konto ma dostęp do MCP (plan Advanced/Prime albo dodatek); przy wątpliwościach napisz na czacie do supportu | | Klient widzi serwer, ale nie ma żadnych narzędzi | serwer podłączony bez zakończonej autoryzacji — usuń konektor i dodaj go ponownie, przechodząc OAuth do końca | | Asystent odpowiada „z głowy", bez wywołania narzędzia | poproś wprost: _„użyj narzędzia Senuto"_ i wskaż raport; sprawdź też, czy konektor jest włączony w tej rozmowie | | `serp_get_report` zwraca `Unfinished task` | crawl jeszcze trwa — odpytuj `serp_check`, aż `progress.has_serp_data` będzie `true` | | Błąd `418` w trakcie rozmowy | najczęściej wyczerpany limit modułu — sprawdź `get_limits` ([Limity zapytań](/rate-limits)) | | `403` z pustą treścią przy własnych żądaniach `curl`-em | masz dostęp do MCP, ale nie do REST API — patrz [Czego potrzebujesz](#czego-potrzebujesz) | Bieżące informacje o awariach: **[komunikat.senuto.com](https://komunikat.senuto.com)**. ## Co dalej - [API, MCP czy no-code](/api-mcp-nocode) - [Limity zapytań](/rate-limits) - [Dostęp do API](/access) - [Instrukcja podłączenia (mcp.senuto.com)](https://mcp.senuto.com) --- # Kraje i `country_id` (`countries/getList`) **`GET /api/countries/getList`** Przykładowe żądanie: ```json {} ``` Przykładowa odpowiedź: ```json { "success": true, "data": [ { "id": 1, "name": "Polska", "code": "pl", "code_lang": "pl-PL", "key": "pl-PL", "label": "Polska", "phone_country_code": 48, "name_with_phone_country_code": "Polska +48" } ] } ``` Zwraca **słownik krajów** obsługiwanych przez Senuto. `id` to `country_id` wymagany w wielu endpointach (Baza słów, SERP, Analiza widoczności, projekty). Zawiera też kody językowe i telefoniczne. | country_id | Nazwa | Kod | Locale | Kod tel. | | --- | --- | --- | --- | --- | | 1 | Polska | pl | pl-PL | 48 | | 3 | Andora | ad | ca-AD | 376 | --- ## Żądanie `GET` `/api/countries/getList` ## Odpowiedź ```ts type CountriesGetListResponse = { success: boolean; data: Array<{ id: number; name: string; code: string; code_lang: string; key: string; label: string; phone_country_code: number; name_with_phone_country_code: string; }>; } export default CountriesGetListResponse ``` ## Powiązane strony - [Kraje i rynki Analizy widoczności](/modules/visibility_analysis#obsługiwane-rynki-bazy-krajowe) — ten moduł działa na 8 rynkach (9 baz), a nie na całej liście z `getList`; bez `country_id` bierze Polskę z bazy 1.0 - [`visibility_analysis/app/getCountriesList`](/modules/visibility_analysis#obsługiwane-rynki-bazy-krajowe) — węższa lista, tylko bazy krajowe Analizy widoczności - [Baza słów kluczowych](/modules/keywords_analysis) i [Analiza SERP](/modules/serp_analysis) — przyjmują `country_id` z tej listy bez ograniczenia do 8 rynków - [Słownik lokalizacji Monitoringu](/modules/rank_tracker/rt-dictionary-getLocalizations) — regiony i miasta w obrębie kraju --- --- title: Moduły sidebarTitle: Moduły asIndexPage: true ----------------- # Moduły Endpointy raportowe pogrupowane tak samo jak moduły w aplikacji Senuto. Każdy moduł ma własną pulę limitu zapytań ([Limity zapytań](/rate-limits)). - [**Analiza widoczności**](/modules/visibility_analysis) — frazy, pozycje, konkurenci i historia dowolnej domeny. - [**Monitoring**](/modules/rank_tracker) — codzienne pozycje fraz w Twoich projektach. - [**Baza słów kluczowych**](/modules/keywords_analysis) — frazy, pytania i trendy niezależnie od domeny. - [**Analiza SERP**](/modules/serp_analysis) — wyniki Google, treści i adresy URL dla wybranej frazy. - [**Użytkownik**](/modules/user) — dane konta, dostęp do modułów i stan limitów. Wspólne mechanizmy: [Filtrowanie](/types/filter) · [Paginacja](/types/pagination) · [Błędy i status `418`](/types/errors). --- --- title: Analiza widoczności sidebarTitle: Analiza widoczności asIndexPage: true ----------------- # Analiza widoczności Moduł **Analizy widoczności** bada obecność dowolnej domeny w organicznych wynikach Google na podstawie indeksu słów kluczowych Senuto (ok. 18 mln fraz na rynek — patrz [obsługiwane rynki](#obsługiwane-rynki-bazy-krajowe) niżej). Większość raportów przyjmuje parę **`domain` + `fetch_mode`** oraz `country_id`; szczegóły, metoda (GET/POST) i przykłady są na stronie każdego endpointu. Wspólne mechanizmy: [Filtrowanie (`filtering`)](/types/filter) · [Paginacja](/types/pagination) · [Błędy i status `418`](/types/errors). ## Obsługiwane rynki (bazy krajowe) Analiza widoczności działa na **8 rynkach** (9 baz — Polska ma dwie). Bazę wybierasz parametrem **`country_id`**: | Rynek | `country_id` | `code` | Uwagi | | ---------------------- | ------------ | -------- | -------------------------------------------------------------- | | 🇵🇱 Polska — baza 2.0 | `200` | `pl_new` | **Zalecana** — najświeższe i najdokładniejsze dane | | 🇵🇱 Polska — baza 1.0 | `1` | `pl` | **Domyślna**, gdy nie podasz `country_id`; najdłuższa historia | | 🇨🇿 Czechy | `50` | `cz` | | | 🇸🇰 Słowacja | `164` | `sk` | | | 🇭🇺 Węgry | `82` | `hu` | | | 🇷🇴 Rumunia | `153` | `ro` | | | 🇩🇰 Dania | `53` | `dk` | Dane historyczne (do 13.09.2025) | | 🇳🇱 Holandia | `134` | `nl` | Dane historyczne (do 13.09.2025) | | 🇸🇪 Szwecja | `160` | `se` | Dane historyczne (do 13.09.2025) | > **Ostrzeżenie:** > **Bez jawnego `country_id` raporty używają Polski z bazy 1.0 (`country_id: 1`)** — starszej bazy z 2015 r. Jeśli zależy Ci na aktualnych danych dla Polski, zawsze przekazuj **`country_id: 200`** (baza 2.0). **Baza 1.0 vs 2.0 (Polska).** Obie zawierają ok. 18 mln fraz, ale baza 2.0 (wdrożona w październiku 2023) ma \~3× więcej unikalnych fraz, mniej duplikatów (warianty typu „kredyt gotówkowy" / „gotówkowy kredyt" to jedna fraza) i świeższą zawartość — 75% fraz nie występowało w bazie 1.0. Baza 1.0 (z 2015 r.) ma za to najdłuższą historię widoczności — używaj jej do analizy wieloletnich trendów. Szczegóły: [artykuł na wiki Senuto](https://wiki.senuto.com/pl/articles/108190-analiza-widocznosci-nowa-baza-dla-polski). Aktualną listę rynków zwraca publiczny endpoint (nie wymaga tokenu; zweryfikowany na produkcji): ```bash curl 'https://api.senuto.com/api/visibility_analysis/app/getCountriesList' ``` ```json filename="odpowiedź (fragment)" { "success": true, "data": [ { "id": 200, "name": "Poland (database 2.0)", "code": "pl_new", "code_lang": "pl-PL" }, { "id": 50, "name": "Czechia", "code": "cz", "code_lang": "cs-CZ" } ] } ``` Pozostałe moduły — [Baza słów kluczowych](/modules/keywords_analysis) i [Monitoring](/modules/rank_tracker) — nie mają tego ograniczenia i działają globalnie (dowolny kraj). Zobacz też: [Jakie kraje są dostępne w Senuto? (wiki)](https://wiki.senuto.com/pl/articles/180688-jakie-kraje-sa-dostepne-w-senuto). ## Pozycje i frazy - [Pozycje — bieżące](/modules/visibility_analysis/positions) · [wzrosty](/modules/visibility_analysis/va-positions-getWins) · [spadki](/modules/visibility_analysis/va-positions-getLosses) · [historia frazy](/modules/visibility_analysis/va-positions-getKeywordHistory) - [Cechy fraz — wykres](/modules/visibility_analysis/va-keywords-getCharacteristicsChart) · [tabela](/modules/visibility_analysis/va-keywords-getCharacteristicsTable) - [Kanibalizacja — frazy](/modules/visibility_analysis/va-cannibalization-getKeywords) · [sekcje](/modules/visibility_analysis/va-cannibalization-getSections) ## Historia widoczności - [Historia fraz](/modules/visibility_analysis/va-history-keywords) · [wzrosty](/modules/visibility_analysis/va-history-keywords-getWins) · [spadki](/modules/visibility_analysis/va-history-keywords-getLosses) · [pozyskane](/modules/visibility_analysis/va-history-keywords-getAcquired) · [utracone](/modules/visibility_analysis/va-history-keywords-getLost) · [dostępne daty](/modules/visibility_analysis/va-history-keywords-getDates) - [Historia URL-i](/modules/visibility_analysis/va-history-urls-getData) · [wzrosty](/modules/visibility_analysis/va-history-urls-getWins) · [spadki](/modules/visibility_analysis/va-history-urls-getLosses) · [pozyskane](/modules/visibility_analysis/va-history-urls-getAcquired) · [utracone](/modules/visibility_analysis/va-history-urls-getLost) ## Struktura domeny - [Sekcje domeny](/modules/visibility_analysis/va-sections-getSections) · [Subdomeny](/modules/visibility_analysis/va-sections-getSubdomains) · [Adresy URL](/modules/visibility_analysis/va-sections-getUrls) - Dashboard: [statystyki domeny](/modules/visibility_analysis/va-dashboard-getDomainStatistics) · [dane domeny](/modules/visibility_analysis/va-dashboard-getDomainData) · [technologie](/modules/visibility_analysis/va-dashboard-getTechnologies) ## Konkurencja - [Konkurenci (raport)](/modules/visibility_analysis/va-competitors-getData) - [Analiza konkurentów](/modules/visibility_analysis/va-competitors-analysis-getData) ## AI Overviews - [Słowa kluczowe](/modules/visibility_analysis/va-ai-overviews-getKeywords) · [statystyki](/modules/visibility_analysis/va-ai-overviews-getStatistics) · [rozkład](/modules/visibility_analysis/va-ai-overviews-getDistribution) · [konkurenci](/modules/visibility_analysis/va-ai-overviews-getCompetitors) · [wyniki frazy](/modules/visibility_analysis/va-ai-overviews-getKeywordResults) · [intencje](/modules/visibility_analysis/va-ai-overviews-getKeywordsIntents) · [szanse](/modules/visibility_analysis/va-ai-overviews-getOpportunities) ## Narzędzia - [Ranking domen](/modules/visibility_analysis/va-domains-ranking-getRankingData) - [Limity zapytań](/rate-limits) --- # Bieżące pozycje (`getData`) **`POST /api/visibility_analysis/reports/positions/getData`** Zwraca frazy kluczowe, na które rankuje domena, wraz ze statystykami dla każdej frazy (pozycja, widoczność, URL, CPC, liczba wyszukiwań, trendy, trudność, snippety SERP). Bez parametru `order` wyniki są posortowane po `keyword_id` rosnąco — o kolejności decyduje wyłącznie poprawnie podany `order` (patrz [Parametry](#parametry)). ## Żądanie `POST` `/api/visibility_analysis/reports/positions/getData` Nagłówki: `Authorization: Bearer `, `Content-Type: application/json`. Wymagane pola: **`domain`** (domena) i **`fetch_mode`** (zakres analizy, np. `topLevelDomain`). ### Struktura żądania **Podstawowy** ```jsonc filename="żądanie-podstawowe.jsonc" { "domain": "zalando.pl", "fetch_mode": "topLevelDomain" } ``` **Rozszerzony** ```jsonc filename="żądanie-rozszerzone.jsonc" { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "limit": 10, "page": 1, "with_history": true, "order": { "prop": "statistics.visibility.current", "dir": "desc" }, "filtering": [ { "filters": [ { "key": "statistics.position.current", "match": "lte", "value": 10 }, { "key": "keywords", "items": [{ "match": "contain", "value": "buty" }] } ] } ] } ``` **cURL** ```bash curl --location --request POST 'https://api.senuto.com/api/visibility_analysis/reports/positions/getData' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' \ --data-raw '{ "domain": "zalando.pl", "fetch_mode": "topLevelDomain" }' ``` ### Parametry ```ts type PositionsGetDataRequest = { /** * **Wymagane**. Domena, subdomena, katalog lub URL do analizy — interpretowane zgodnie z `fetch_mode`. */ domain: string; /** * **Wymagane**. Sposób interpretacji `domain`. * - `topLevelDomain` — cała domena (najczęstszy przypadek; "domena") * - `subdomain` — pojedyncza subdomena * - `catalog` — ścieżka/katalog * - `url` — dokładny URL */ fetch_mode: 'topLevelDomain' | 'subdomain' | 'catalog' | 'url'; /** * Liczba wierszy na stronę. Nieujemna liczba całkowita. * @default 10 */ limit?: number; /** * Numer strony. Nieujemna liczba całkowita. * @default 1 */ page?: number; /** * Dołącz do odpowiedzi mapę historii pozycji (`history`) dla każdej frazy. * @default true */ with_history?: boolean; /** * Sortowanie wyników — **pojedynczy obiekt**, nie tablica. * Dozwolone `prop`: `statistics.position.current|previous|diff`, * `statistics.visibility.current|previous|diff`, `statistics.searches.current`, * `statistics.cpc.current`, `statistics.difficulty.current`, * `statistics.url.is_change`, `words_count`. * Zły kształt lub nieznany klucz NIE zwraca błędu — API po cichu wraca * do sortu domyślnego (`keyword_id` rosnąco). */ order?: { prop: string; dir: 'asc' | 'desc' }; /** * Filtrowanie — tablica **grup**. Pusta tablica = brak filtrowania. * Filtry w obrębie jednej grupy łączone są operatorem AND. * Pełna lista dostępnych kluczy i przykłady: sekcja "Filtrowanie" poniżej. */ filtering?: { filters: ( | { key: string; match: 'eq' | 'gt' | 'gte' | 'lt' | 'lte'; value: number | string } | { key: 'keywords'; items: { match: 'contain' | 'containsWord' | 'startsWith' | 'endsWith' | 'notContain'; value: string }[] } )[]; }[]; } export default PositionsGetDataRequest ``` > **Ostrzeżenie:** > Nazwy parametrów różnią się od starej dokumentacji: jest to **`filtering`** (nie `filters`) oraz **`order`** (nie `sort_by` / `sort_order`). Uwaga na kształt `order`: to **obiekt `{ "prop": …, "dir": … }`** — forma tablicowa `[{ "field": …, "direction": … }]` jest przez API **ignorowana po cichu** (zwraca 200 z sortem domyślnym po `keyword_id`). > **Ostrzeżenie:** > Zarówno **`domain`**, jak i **`fetch_mode`** są **wymagane**; pominięcie `fetch_mode` zwraca `418` z `invalid_data`. ## Filtrowanie Parametr `filtering` odpowiada polu **Filtry** nad tabelą w raporcie pozycji. To **tablica grup**; każda grupa ma klucz `filters` z listą warunków. Warunki w obrębie jednej grupy łączone są operatorem **AND**. ```jsonc filename="kształt-filtering.jsonc" "filtering": [ { "filters": [ // filtr liczbowy: { key, match, value } { "key": "statistics.position.current", "match": "lte", "value": 10 }, // filtr tekstowy fraz: { key: "keywords", items: [{ match, value }] } { "key": "keywords", "items": [{ "match": "contain", "value": "buty" }] } ] } ] ``` ### Dostępne klucze (`positions/getData`) | Klucz | Typ | Operatory (`match`) | | ------------------------------------------------------------------------------------------------------------------- | ------------------------ | ----------------------------------------------------------------- | | `keywords` | tekstowy (przez `items`) | `contain`, `containsWord`, `startsWith`, `endsWith`, `notContain` | | `statistics.position.current` · `.previous` · `.diff` | liczbowy | `eq`, `gt`, `gte`, `lt`, `lte` | | `statistics.visibility.current` · `.previous` · `.diff` | liczbowy | `eq`, `gt`, `gte`, `lt`, `lte` | | `cpc` · `statistics.cpc.current` | liczbowy | `eq`, `gt`, `gte`, `lt`, `lte` | | `statistics.searches.current` | liczbowy | `eq`, `gt`, `gte`, `lt`, `lte` | | `statistics.difficulty.current` | liczbowy | `eq`, `gt`, `gte`, `lt`, `lte` | | `words_count` | liczbowy | `eq`, `gt`, `gte`, `lt`, `lte` | | `is_change` · `statistics.url.is_change` | logiczny | `eq` | | `statistics.url.current` · `.previous` | URL | dopasowanie po adresie URL | | `statistics.snippets.current` | snippety SERP | filtr po typach snippetów | | `statistics.intentions.primary_intent` · `.main_intent` · `.action_type` · `.journey_stage` · `.content_timeliness` | intencje | dostępne tylko dla krajów wspierających intencje | > **Informacja:** > Przykłady dla `zalando.pl` (2026-07-03; baseline bez filtra: `count` = 291 325 — indeks jest odświeżany, więc liczby dryfują z dnia na dzień): > > - `statistics.position.current` `lte` `3` → `count` = 46 360 (frazy w TOP3), > - `keywords` `contain` `"buty"` → `count` = 20 325, > - oba w jednej grupie (AND) → `count` = 2 832, > - `lte` `10` + `"buty"` (AND) → `count` = 7 936. Filtry liczbowe, tekstowe i logiczne opisuje też wspólna strona [`Filter`](/types/filter). ## Przykładowe dane i zastosowania Dane pochodzą z bazy słów kluczowych Senuto (indeksowane pozycje w organicznych wynikach Google) i aktualizowane są cyklicznie — im wyższa popularność frazy (liczba wyszukiwań/mies.), tym częstsza aktualizacja; to inny model niż w Monitoringu (Rank Tracker), gdzie dane liczone są codziennie od dnia założenia projektu ([źródło](https://wiki.senuto.com/pl/articles/71799-jak-czesto-aktualizowane-sa-dane-w-analizie-widocznosci-i-monitoringu)). Pole `statistics.visibility` to **nie** realny ruch z Google Analytics — to szacowany miesięczny ruch organiczny, liczony na bazie widoczności frazy w TOP10, średniej liczby wyszukiwań i CTR wg pozycji ([źródło](https://wiki.senuto.com/en/articles/19052-metrics-in-senuto)). Tak wyglądają **realne wiersze zwrócone przez API** (5 pierwszych wyników dla `zalando.pl`), rozpisane w tabeli. | Fraza | Pozycja | Zmiana pozycji | Wyszukiwania/mies. | Widoczność | URL bieżący | ID frazy | KID | Domena | Liczba słów | Pozycja poprz. | Wzrosty | Spadki | Bez zmian | Widoczność poprz. | Δ widoczności | Widoczność % | URL poprzedni | URL zmiana | CPC | Trend (12 mies.) | Szczyt trendu | Trudność | Snippety SERP | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | zalando | 1 | 0 | 1830000 | 651480 | zalando.pl/ | 13624651 | b8ac304f24a9864f46f86cbebc0820f1 | zalando.pl | 1 | 1 | 0 | 0 | 0 | 651480 | 0 | 0 | zalando.pl/ | 0 | 2.03 | [1830000,1830000,1830000,1830000,1830000,2240000,2240000,1830000,1830000,1500000,2240000,1830000] | | 71 | ["video_thumbs"] | | zalando lounge | 2 | 1 | 823000 | 144189.6 | zalando.pl/ | 3429087 | 2e720c7d15dc72dd3c9643ca1b16ed9c | zalando.pl | 2 | 1 | 0 | 1 | 0 | 292988 | -148798.4 | -0.5079 | zalando.pl/ | 0 | 0.24 | [1000000,823000,823000,823000,823000,1000000,1000000,823000,823000,823000,1000000,823000] | | 41 | [] | | breska | 2 | 0 | 673000 | 117909.6 | zalando.pl/bershka/ | 8961793 | 799275d6ef5fd71542b0775885f3a11a | zalando.pl | 1 | 2 | 0 | 0 | 0 | 117909.6 | 0 | 0 | zalando.pl/bershka/ | 0 | 0.06 | [673000,550000,550000,550000,550000,550000,550000,550000,673000,673000,673000,673000] | | 62 | ["image_thumbs","spell"] | | bershka | 2 | 0 | 550000 | 96360 | zalando.pl/bershka/ | 2843536 | 2684ef5a9bef4d8a830698ab1a5cb1a4 | zalando.pl | 1 | 2 | 0 | 0 | 1 | 96360 | 0 | 0 | zalando.pl/bershka/ | 0 | 0.52 | [450000,450000,550000,550000,550000,550000,550000,550000,550000,450000,550000,550000] | | 77 | ["wiki_right"] | | bersh a | 2 | 0 | 550000 | 96360 | zalando.pl/bershka/ | 16598459 | e0da27950676b2b5df5c8fa3c959e68e | zalando.pl | 2 | 2 | 0 | 0 | 0 | 96360 | 0 | 0 | zalando.pl/bershka/ | 0 | 0.32 | [673000,450000,550000,550000,450000,450000,550000,550000,550000,550000,673000,673000] | | 44 | ["ai_overview","image_thumbs","spell"] | _zalando.pl · 2026-07-05, limit: 5, sort: widoczność malejąco. Wszystkie pola wiersza (poza mapą historii `statistics.position.history` o zmiennych kluczach-datach — jest w JSON i sekcji „Struktura odpowiedzi”)._ Cztery gotowe zastosowania — każda karta pokazuje realne wiersze z żywego API (zalando.pl, 2026-07-03). „Wypróbuj” ładuje kompletny payload do playgroundu na dole i od razu pokazuje pełny wynik. **Quick wins — frazy tuż za TOP10** Pozycje 11–20 z wolumenem ≥ 100: kandydaci do dopchnięcia na 1. stronę Google. _12 614 fraz spełnia ten filtr_ ```json { "filtering": [ { "filters": [ { "key": "statistics.position.current", "match": "gte", "value": 11 }, { "key": "statistics.position.current", "match": "lte", "value": 20 }, { "key": "statistics.searches.current", "match": "gte", "value": 100 } ] } ], "order": { "prop": "statistics.searches.current", "dir": "desc" }, "limit": 10 } ``` | Fraza | Pozycja | Wyszukiwania/mies. | | --- | --- | --- | | sdidas | 11 | 550 000 | | C&A | 12 | 450 000 | | deeze | 20 | 450 000 | [Wiki: quick wins](https://wiki.senuto.com/l/pl/poradniki/jak-znalezc-quick-wins-czyli-frazy-ktorych-pozycje-mozna-latwo-zwiekszyc) **Twoje najsilniejsze frazy (TOP3)** Frazy, na których domena rankuje najwyżej — do pilnowania pozycji i budowy contentu wokół nich. _46 360 fraz w TOP3_ ```json { "filtering": [ { "filters": [ { "key": "statistics.position.current", "match": "lte", "value": 3 } ] } ], "order": { "prop": "statistics.visibility.current", "dir": "desc" }, "limit": 10 } ``` | Fraza | Pozycja | Wyszukiwania/mies. | | --- | --- | --- | | zalando | 1 | 1 830 000 | | zalando lounge | 2 | 823 000 | | breska | 2 | 673 000 | [Wiki: TOP10](https://wiki.senuto.com/en/articles/18043-for-which-keywords-is-your-website-ranking-in-the-top-10) **Tematyczny wycinek („buty”)** Frazy zawierające konkretne słowo (produkt, kategoria) — analiza widoczności w niszy. _20 325 fraz z „buty”_ ```json { "filtering": [ { "filters": [ { "key": "keywords", "items": [ { "match": "contain", "value": "buty" } ] } ] } ], "order": { "prop": "statistics.visibility.current", "dir": "desc" }, "limit": 10 } ``` | Fraza | Pozycja | Wyszukiwania/mies. | | --- | --- | --- | | uggs buty | 1 | 60 500 | | buty zimowe | 1 | 49 500 | | buty zi | 1 | 49 500 | **Frazy, które tracą widoczność** Największe spadki szacowanego ruchu względem poprzedniego pomiaru — lista do pilnej interwencji. _cała domena (291 325 fraz), sortowana po spadku_ ```json { "order": { "prop": "statistics.visibility.diff", "dir": "asc" }, "limit": 10 } ``` | Fraza | Pozycja | Δ widoczności | | --- | --- | --- | | zalando lounge | 2 | −148 798 | | stradivarius | 7 | −66 105 | | bluzę | 4 | −31 350 | ## Odpowiedź Po pomyślnym żądaniu otrzymujesz `data` (tablicę wierszy fraz) oraz `pagination`. ### Surowy JSON **Skrócona** ```json filename="przykładowa-odpowiedź (skrócona)" { "success": true, "data": [ { "keyword_id": 184, "keyword": "toni and paul", "statistics": { "position": { "current": 29 } /* … */ } } ], "pagination": { "page_count": 97041, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 291121, "limit": 10 } } ``` **Pełna** ```json filename="przykładowa-odpowiedź (200)" { "success": true, "data": [ { "keyword_id": 184, "kid": "0000a7260c00bd42f07edcce28f7c7fa", "domain": "zalando.pl", "keyword": "toni and paul", "words_count": 3, "statistics": { "position": { "current": 29, "previous": 29, "diff": 0, "changes": { "wins": 0, "losses": 0, "no_changes": 0 }, "history": { "2026-05-28": { "position": 29, "has_serp": true } } }, "visibility": { "current": 0, "previous": 0, "diff": 0, "percent": 0, "history": null }, "url": { "current": "zalando.pl/obuwie-meskie/toni-pons/", "previous": "zalando.pl/obuwie-meskie/toni-pons/", "is_change": 0 }, "cpc": { "current": 0 }, "searches": { "current": 10 }, "trends": { "history": [0, 10, 10, 0, 0, 0, 0, 0, 0, 0, 0, 10], "peak": null }, "difficulty": { "current": 33 }, "snippets": { "current": ["image_thumbs", "people_also_ask"] } } } ], "pagination": { "page_count": 97041, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 291121, "limit": 10 } } ``` ### Struktura odpowiedzi ```ts type PositionsResponse = { /** `true` przy powodzeniu; przy błędzie `false` i koperta z `error` */ success: boolean; /** Zwrócone wiersze fraz */ data: PositionRow[]; /** Metadane paginacji */ pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; limit: number; }; } type PositionRow = { keyword_id: number; kid: string; domain: string; keyword: string; words_count: number; statistics: { position: { current: number; previous: number; diff: number; changes: { wins: number; losses: number; no_changes: number }; history: Record | [] }; visibility: { current: number; previous: number; diff: number; percent: number; history: null }; url: { current: string; previous: string; is_change: 0 | 1 }; cpc: { current: number }; searches: { current: number }; trends: { history: number[]; peak: number | null }; difficulty: { current: number }; snippets: { current: string[] }; }; } export default PositionsResponse ``` ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data, invalid_filtering, unreachable_data, timeout, database, unknown */ type: string; message: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > **`418`** jest zwracane również w przypadku błędów walidacji — nie tylko przy ograniczaniu liczby żądań. Brak `fetch_mode` → > `{"success":false,"data":{"error":{"type":"invalid_data","params":{"fetch_mode":{"_required":"This field is required"}}}}}`. ## Powiązane akcje - `getData` — bieżące pozycje (ta strona) - `getWins` / `getLosses` — frazy, które zyskały / straciły pozycje (taki sam kształt żądania) - `getKeywordHistory` — pełna historia pozycji dla pojedynczej frazy (`keyword_id` + `kid` + `domain` + `fetch_mode`) --- # Pozycje: wzrosty (`getWins`) **`POST /api/visibility_analysis/reports/positions/getWins`** Zwraca frazy kluczowe, na których pozycja domeny **wzrosła** w analizowanym okresie (tryb pracy = `increase`). Dla każdej frazy zwracany jest ten sam zestaw statystyk co w `getData` (pozycja, widoczność, URL, CPC, liczba wyszukiwań, trendy, trudność, snippety SERP), ograniczony do fraz z poprawą pozycji. Domyślnie posortowane według wielkości zmiany. > **Ostrzeżenie:** > **Okres jest zaszyty na sztywno — ok. tygodnia.** Ta akcja nie przyjmuje żadnych parametrów dat. Porównywany jest najświeższy dostępny snapshot pozycji z najstarszym snapshotem z ostatniego tygodnia. Jeśli potrzebujesz własnego zakresu, użyj [`history/keywords/getWins`](/modules/visibility_analysis/va-history-keywords-getWins) z `date_min`/`date_max`. > > **Wyniki zawierają też frazy nowo pozyskane.** Fraza, na którą domena wcześniej nie rankowała, trafia tutaj jako skok z pozycji `51` (sentinel „poza TOP50") — patrz przykładowa odpowiedź powyżej: `{"current": 26, "previous": 51, "diff": -25}`. Aby zawęzić wynik do ruchu **wewnątrz** TOP50, odrzuć wiersze z `statistics.position.previous === 51`. > > Siostrzana [`getLosses`](/modules/visibility_analysis/va-positions-getLosses) zachowuje się **odwrotnie** — frazy utracone są z niej wykluczone, a rodzina `positions/*` nie ma odpowiednika `getLost`, więc w ogóle ich stąd nie pobierzesz. Porównywanie `count` obu akcji zestawia dwie różnie zdefiniowane wielkości. Zachowanie może się w przyszłości ujednolicić — na dziś traktuj powyższe jako obowiązujący kontrakt. | Fraza | ID frazy | KID | Domena | Liczba słów | Pozycja | Pozycja poprz. | Zmiana pozycji | Wzrosty | Spadki | Bez zmian | Widoczność | Widoczność poprz. | Δ widoczności | Widoczność % | URL bieżący | URL poprzedni | URL zmiana | CPC | Wyszukiwania/mies. | Trend (12 mies.) | Szczyt trendu | Trudność | Snippety SERP | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | vans zamszowe | 225329 | 030d9b596c2058c07801e2ec87e85a37 | zalando.pl | 2 | 1 | 15 | -14 | 0 | 0 | 0 | 39.16 | 0 | 39.16 | 1 | zalando.pl/wszystkie/vans/?q=zamsz | zalando.pl/obuwie-damskie-tenisowki-trampki/vans/ | 1 | 0.77 | 110 | [140,110,110,140,170,90,70,50,70,70,170,140] | | 49 | ["image_thumbs"] | | lakierowane balerinki | 7856664 | 6a890673b2ee1ab050e233749ab4d170 | zalando.pl | 2 | 1 | 2 | -1 | 0 | 0 | 0 | 60.52 | 29.78 | 30.74 | 1.0322 | zalando.pl/obuwie-damskie-baleriny/?q=baleriny+lakierowane | zalando.pl/obuwie-damskie-baleriny/?q=baleriny+lakierowane | 0 | 0.94 | 170 | [170,210,320,260,260,140,140,140,210,170,110,90] | | 52 | ["image_thumbs","spell"] | | śniegowce sorel | 6508374 | 583abba99a976ff8f6276ef42ebab4c7 | zalando.pl | 2 | 1 | 3 | -2 | 1 | 0 | 0 | 676.4 | 204.25 | 472.15 | 2.3116 | zalando.pl/buty-zimowe/sorel/ | zalando.pl/buty-zimowe/sorel/ | 0 | 1.31 | 1900 | [2900,1900,390,140,110,90,140,720,1900,1900,5400,8100] | | 43 | ["image_thumbs","people_also_ask"] | | żółty sweterek rozpinany | 7993203 | 6c5f97d43b98e882ad45d9108af77e43 | zalando.pl | 3 | 1 | 2 | -1 | 0 | 0 | 0 | 60.52 | 24.53 | 35.99 | 1.4672 | zalando.pl/kardigany/_zolty/ | zalando.pl/kardigany/_zolty/ | 0 | 1.08 | 170 | [0,0,0,0,0,0,0,0,0,0,0,0] | | 42 | ["image_thumbs"] | | quiksilver t-shirt | 8270210 | 702193e9391832d6a07401b1c1fdff47 | zalando.pl | 2 | 1 | 2 | -1 | 0 | 0 | 0 | 92.56 | 45.55 | 47.01 | 1.0321 | zalando.pl/odziez-meska-koszulki/quiksilver/ | zalando.pl/odziez-meska-koszulki/quiksilver/ | 0 | 0.1 | 260 | [210,210,260,260,260,390,320,320,170,110,170,170] | | 41 | ["image_thumbs","people_also_ask"] | _zalando.pl · 2026-07-04, limit: 5, sort: pozycja rosnąco — frazy, które zyskały pozycje. Wszystkie pola wiersza (poza mapą historii pozycji `statistics.position.history`, która ma zmienne klucze-daty — jest w JSON i sekcji „Struktura odpowiedzi”)._ --- ## Żądanie `POST` `/api/visibility_analysis/reports/positions/getWins` Nagłówki: `Authorization: Bearer `, `Content-Type: application/json`. ### Struktura żądania **Podstawowy** ```jsonc filename="żądanie-podstawowe.jsonc" { "domain": "zalando.pl", "fetch_mode": "topLevelDomain" } ``` **Rozszerzony** ```jsonc filename="żądanie-rozszerzone.jsonc" { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "limit": 10, "page": 1, "order": { "prop": "statistics.position.current", "dir": "asc" }, "filtering": [] } ``` **cURL** ```bash curl --location --request POST 'https://api.senuto.com/api/visibility_analysis/reports/positions/getWins' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' \ --data-raw '{ "domain": "zalando.pl", "fetch_mode": "topLevelDomain" }' ``` ### Parametry ```ts type PositionsGetWinsRequest = { /** * **Wymagane**. Domena, subdomena, katalog lub URL do analizy — interpretowane zgodnie z `fetch_mode`. */ domain: string; /** * **Wymagane**. Sposób interpretacji `domain`. * - `topLevelDomain` — cała domena (najczęstszy przypadek; "domena") * - `subdomain` — pojedyncza subdomena * - `catalog` — ścieżka/katalog * - `url` — dokładny URL */ fetch_mode: 'topLevelDomain' | 'subdomain' | 'catalog' | 'url'; /** * Liczba wierszy na stronę. Nieujemna liczba całkowita. * @default 10 */ limit?: number; /** * Numer strony. Nieujemna liczba całkowita. * @default 1 */ page?: number; /** * Sortowanie wyników — **pojedynczy obiekt**, nie tablica. * Dozwolone `prop`: `statistics.position.current|previous|diff`, * `statistics.visibility.current|previous|diff`, `statistics.searches.current`, * `statistics.cpc.current`, `statistics.difficulty.current`, * `statistics.url.is_change`, `words_count`. * Zły kształt lub nieznany klucz NIE zwraca błędu — API po cichu wraca * do sortu domyślnego (`keyword_id` rosnąco). */ order?: { prop: string; dir: 'asc' | 'desc' }; /** * Dyrektywy filtrowania. Pusta tablica = brak filtrowania. */ filtering?: unknown[]; } export default PositionsGetWinsRequest ``` > **Ostrzeżenie:** > Nazwy parametrów różnią się od starej dokumentacji: jest to **`filtering`** (nie `filters`) oraz **`order`** (nie `sort_by` / `sort_order`). Uwaga na kształt `order`: to **obiekt `{ "prop": …, "dir": … }`** — forma tablicowa `[{ field, direction }]` jest przez API **ignorowana po cichu** (zwraca 200 z sortem domyślnym po `keyword_id`). > **Ostrzeżenie:** > Zarówno **`domain`**, jak i **`fetch_mode`** są **wymagane**; pominięcie `fetch_mode` zwraca `418` z `invalid_data`. ## Odpowiedź Po pomyślnym żądaniu otrzymujesz `data` (tablicę fraz, które zyskały na pozycji) oraz `pagination`. Ujemna wartość `position.diff` oznacza poprawę — fraza przesunęła się w górę SERP (mniejszy numer pozycji). **Skrócona** ```json filename="przykładowa-odpowiedź (skrócona)" { "success": true, "data": [ { "keyword_id": 1097, "keyword": "my secret", "statistics": { "position": { "current": 26, "previous": 51, "diff": -25 } /* … */ } } ], "pagination": { "page_count": 3182, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 6363, "limit": 2 } } ``` **Pełna** ```json filename="przykładowa-odpowiedź (200)" { "success": true, "data": [ { "keyword_id": 1097, "kid": "0003d04b8e93ae73189ea88a01b6a0b5", "domain": "zalando.pl", "keyword": "my secret", "words_count": 2, "statistics": { "position": { "current": 26, "previous": 51, "diff": -25, "changes": { "wins": 0, "losses": 0, "no_changes": 0 }, "history": { "2026-06-25": { "position": 26, "has_serp": true } } }, "visibility": { "current": 0, "previous": 0, "diff": 0, "percent": 0, "history": null }, "url": { "current": "zalando.pl/kobiety/my-white-secret/", "previous": "", "is_change": 1 }, "cpc": { "current": 0.64 }, "searches": { "current": 480 }, "trends": { "history": [480, 390, 480, 390, 390, 390, 390, 480, 590, 390, 480, 720], "peak": null }, "difficulty": { "current": 45 }, "snippets": { "current": ["image_thumbs", "people_also_ask"] } } } ], "pagination": { "page_count": 3182, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 6363, "limit": 2 } } ``` ### Struktura odpowiedzi ```ts type PositionsResponse = { /** `true` przy powodzeniu; przy błędzie `false` i koperta z `error` */ success: boolean; /** Zwrócone wiersze fraz (z poprawą pozycji) */ data: PositionRow[]; /** Metadane paginacji */ pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; limit: number; }; } type PositionRow = { keyword_id: number; kid: string; domain: string; keyword: string; words_count: number; statistics: { position: { current: number; previous: number; diff: number; changes: { wins: number; losses: number; no_changes: number }; history: Record | [] }; visibility: { current: number; previous: number; diff: number; percent: number; history: null }; url: { current: string; previous: string; is_change: 0 | 1 }; cpc: { current: number }; searches: { current: number }; trends: { history: number[]; peak: number | null }; difficulty: { current: number }; snippets: { current: string[] }; }; } export default PositionsResponse ``` ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data, invalid_filtering, unreachable_data, timeout, database, unknown */ type: string; message: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > **`418`** jest zwracane również w przypadku błędów walidacji — nie tylko przy ograniczaniu liczby żądań. Brak `fetch_mode` → > `{"success":false,"data":{"error":{"type":"invalid_data","params":{"fetch_mode":{"_required":"This field is required"}}}}}`. ## Powiązane akcje - `getData` — bieżące pozycje fraz (taki sam kształt żądania) - `getWins` — frazy, które zyskały pozycje (ta strona) - `getLosses` — frazy, które straciły pozycje (taki sam kształt żądania) - `getKeywordHistory` — pełna historia pozycji dla pojedynczej frazy (`keyword_id` + `kid` + `domain` + `fetch_mode`) --- # Pozycje: spadki (`getLosses`) **`POST /api/visibility_analysis/reports/positions/getLosses`** Zwraca frazy kluczowe, dla których domena **straciła pozycje** w wybranym okresie (working mode = `decrease`). Każdy wiersz zawiera te same statystyki co `getData` (pozycja, widoczność, URL, CPC, liczba wyszukiwań, trendy, trudność, snippety SERP), ale zbiór jest ograniczony do fraz, których pozycja się pogorszyła — wartość `diff` w `position` odzwierciedla zmianę na gorsze. > **Ostrzeżenie:** > **Okres jest zaszyty na sztywno — ok. tygodnia.** Ta akcja nie przyjmuje żadnych parametrów dat. Porównywany jest najświeższy dostępny snapshot pozycji z najstarszym snapshotem z ostatniego tygodnia. Jeśli potrzebujesz własnego zakresu, użyj [`history/keywords/getLosses`](/modules/visibility_analysis/va-history-keywords-getLosses) z `date_min`/`date_max`. > > **Frazy utracone nie są tutaj widoczne.** Fraza, która wypadła poza TOP50, nie pojawi się w tej akcji, mimo że formalnie „spadła" — zwracany jest wyłącznie ruch **wewnątrz** TOP50. W rodzinie `positions/*` **nie ma odpowiednika `getLost`**, więc utraconych fraz nie da się stąd pobrać w ogóle; sięgnij po [`history/keywords/getLost`](/modules/visibility_analysis/va-history-keywords-getLost) i podaj daty ręcznie. > > Siostrzana [`getWins`](/modules/visibility_analysis/va-positions-getWins) zachowuje się **odwrotnie** — zawiera frazy nowo pozyskane (jako skok z pozycji `51`). Porównywanie `count` obu akcji zestawia więc dwie różnie zdefiniowane wielkości. Zachowanie może się w przyszłości ujednolicić — na dziś traktuj powyższe jako obowiązujący kontrakt. | Fraza | ID frazy | KID | Domena | Liczba słów | Pozycja | Pozycja poprz. | Zmiana pozycji | Wzrosty | Spadki | Bez zmian | Widoczność | Widoczność poprz. | Δ widoczności | Widoczność % | URL bieżący | URL poprzedni | URL zmiana | CPC | Wyszukiwania/mies. | Trend (12 mies.) | Szczyt trendu | Trudność | Snippety SERP | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | dresy damskie 4f | 2864 | 0009edce5b257ad4363766e56bef5c74 | zalando.pl | 3 | 17 | 15 | 2 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | zalando.pl/odziez-damska-spodnie-treningowe/4f/ | zalando.pl/odziez-damska-spodnie-treningowe/4f/ | 0 | 0.47 | 1600 | [2400,2400,1900,2400,1300,1000,880,1300,1600,1600,1600,1600] | | 30 | ["image_thumbs","people_also_ask"] | _zalando.pl — fraza, która straciła pozycję. Dodatni „diff” oznacza spadek. Wszystkie pola wiersza (poza mapą historii `statistics.position.history`, która ma zmienne klucze-daty — jest w JSON i sekcji „Struktura odpowiedzi”)._ --- ## Żądanie `POST` `/api/visibility_analysis/reports/positions/getLosses` Nagłówki: `Authorization: Bearer `, `Content-Type: application/json`. ### Struktura żądania **Podstawowy** ```jsonc filename="żądanie-podstawowe.jsonc" { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "limit": 2 } ``` **Rozszerzony** ```jsonc filename="żądanie-rozszerzone.jsonc" { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "limit": 10, "page": 1, "order": { "prop": "statistics.position.current", "dir": "desc" }, "filtering": [] } ``` **cURL** ```bash curl --location --request POST 'https://api.senuto.com/api/visibility_analysis/reports/positions/getLosses' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' \ --data-raw '{ "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "limit": 2 }' ``` ### Parametry ```ts type PositionsGetLossesRequest = { /** * **Wymagane**. Domena, subdomena, katalog lub URL do analizy — interpretowane zgodnie z `fetch_mode`. */ domain: string; /** * **Wymagane**. Sposób interpretacji `domain`. * - `topLevelDomain` — cała domena (najczęstszy przypadek; "domena") * - `subdomain` — pojedyncza subdomena * - `catalog` — ścieżka/katalog * - `url` — dokładny URL */ fetch_mode: 'topLevelDomain' | 'subdomain' | 'catalog' | 'url'; /** * Liczba wierszy na stronę. Nieujemna liczba całkowita. * @default 10 */ limit?: number; /** * Numer strony. Nieujemna liczba całkowita. * @default 1 */ page?: number; /** * Sortowanie wyników — **pojedynczy obiekt**, nie tablica. * Dozwolone `prop`: `statistics.position.current|previous|diff`, * `statistics.visibility.current|previous|diff`, `statistics.searches.current`, * `statistics.cpc.current`, `statistics.difficulty.current`, * `statistics.url.is_change`, `words_count`. * Zły kształt lub nieznany klucz NIE zwraca błędu — API po cichu wraca * do sortu domyślnego (`keyword_id` rosnąco). */ order?: { prop: string; dir: 'asc' | 'desc' }; /** * Dyrektywy filtrowania. Pusta tablica = brak filtrowania. */ filtering?: unknown[]; } export default PositionsGetLossesRequest ``` > **Ostrzeżenie:** > Nazwy parametrów różnią się od starej dokumentacji: jest to **`filtering`** (nie `filters`) oraz **`order`** (nie `sort_by` / `sort_order`). Uwaga na kształt `order`: to **obiekt `{ "prop": …, "dir": … }`** — forma tablicowa `[{ field, direction }]` jest przez API **ignorowana po cichu** (zwraca 200 z sortem domyślnym po `keyword_id`). > **Ostrzeżenie:** > Zarówno **`domain`**, jak i **`fetch_mode`** są **wymagane**; pominięcie `fetch_mode` zwraca `418` z `invalid_data`. ## Odpowiedź Po pomyślnym żądaniu otrzymujesz `data` (tablicę fraz, które straciły pozycje) oraz `pagination`. W polu `position`: `previous` to pozycja wcześniejsza, `current` — bieżąca, a dodatni `diff` oznacza spadek (wyższa liczba = gorsza pozycja). **Skrócona** ```json filename="przykładowa-odpowiedź (skrócona)" { "success": true, "data": [ { "keyword_id": 2864, "keyword": "dresy damskie 4f", "statistics": { "position": { "current": 17, "previous": 15, "diff": 2 } /* … */ } } ], "pagination": { "page_count": 2057, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 4114, "limit": 2 } } ``` **Pełna** ```json filename="przykładowa-odpowiedź (200)" { "success": true, "data": [ { "keyword_id": 2864, "kid": "0009edce5b257ad4363766e56bef5c74", "domain": "zalando.pl", "keyword": "dresy damskie 4f", "words_count": 3, "statistics": { "position": { "current": 17, "previous": 15, "diff": 2, "changes": { "wins": 0, "losses": 1, "no_changes": 0 }, "history": { "2026-05-24": { "position": 15, "has_serp": true }, "2026-06-25": { "position": 17, "has_serp": true } } }, "visibility": { "current": 0, "previous": 0, "diff": 0, "percent": 0, "history": null }, "url": { "current": "zalando.pl/odziez-damska-spodnie-treningowe/4f/", "previous": "zalando.pl/odziez-damska-spodnie-treningowe/4f/", "is_change": 0 }, "cpc": { "current": 0.47 }, "searches": { "current": 1600 }, "trends": { "history": [2400, 2400, 1900, 2400, 1300, 1000, 880, 1300, 1600, 1600, 1600, 1600], "peak": null }, "difficulty": { "current": 30 }, "snippets": { "current": ["image_thumbs", "people_also_ask"] } } } ], "pagination": { "page_count": 2057, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 4114, "limit": 2 } } ``` ### Struktura odpowiedzi ```ts type PositionsResponse = { /** `true` przy powodzeniu; przy błędzie `false` i koperta z `error` */ success: boolean; /** Zwrócone wiersze fraz (spadki) */ data: PositionRow[]; /** Metadane paginacji */ pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; limit: number; }; } type PositionRow = { keyword_id: number; kid: string; domain: string; keyword: string; words_count: number; statistics: { position: { current: number; previous: number; diff: number; changes: { wins: number; losses: number; no_changes: number }; history: Record | [] }; visibility: { current: number; previous: number; diff: number; percent: number; history: null }; url: { current: string; previous: string; is_change: 0 | 1 }; cpc: { current: number }; searches: { current: number }; trends: { history: number[]; peak: number | null }; difficulty: { current: number }; snippets: { current: string[] }; }; } export default PositionsResponse ``` ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data, invalid_filtering, unreachable_data, timeout, database, unknown */ type: string; message: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > **`418`** jest zwracane również w przypadku błędów walidacji — nie tylko przy ograniczaniu liczby żądań. Brak `fetch_mode` → > `{"success":false,"data":{"error":{"type":"invalid_data","params":{"fetch_mode":{"_required":"This field is required"}}}}}`. ## Powiązane akcje - `getData` — bieżące pozycje (taki sam kształt żądania) - `getWins` — frazy, które **zyskały** pozycje (working mode = `increase`) - `getLosses` — frazy, które **straciły** pozycje (ta strona) - `getKeywordHistory` — pełna historia pozycji dla pojedynczej frazy (`keyword_id` + `kid` + `domain` + `fetch_mode`) --- # Pozycje: historia frazy (`getKeywordHistory`) **`POST /api/visibility_analysis/reports/positions/getKeywordHistory`** Zwraca pełną historię pozycji **pojedynczej frazy kluczowej** dla wskazanej domeny. W odpowiedzi otrzymujesz mapę `history_positions`, w której kluczem jest data pomiaru, a wartością pozycja oraz informacja o obecności snippetów SERP w danym dniu. Identyfikatory frazy (`keyword_id` oraz `kid`) pozyskujesz z `positions/getData`. --- ## Żądanie `POST` `/api/visibility_analysis/reports/positions/getKeywordHistory` Nagłówki: `Authorization: Bearer `, `Content-Type: application/json`. ### Struktura żądania **Podstawowy** ```jsonc filename="żądanie-podstawowe.jsonc" { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "keyword_id": null, "kid": null } ``` **Rozszerzony** ```jsonc filename="żądanie-rozszerzone.jsonc" { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "keyword_id": null, "kid": null, "limit": 10, "page": 1 } ``` **cURL** ```bash curl --location --request POST 'https://api.senuto.com/api/visibility_analysis/reports/positions/getKeywordHistory' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' \ --data-raw '{ "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "keyword_id": null, "kid": null }' ``` ### Parametry ```ts type PositionsGetKeywordHistoryRequest = { /** * **Wymagane**. Domena, subdomena, katalog lub URL do analizy — interpretowane zgodnie z `fetch_mode`. */ domain: string; /** * **Wymagane**. Sposób interpretacji `domain`. * - `topLevelDomain` — cała domena (najczęstszy przypadek; "domena") * - `subdomain` — pojedyncza subdomena * - `catalog` — ścieżka/katalog * - `url` — dokładny URL */ fetch_mode: 'topLevelDomain' | 'subdomain' | 'catalog' | 'url'; /** * **Wymagane**. Numeryczny identyfikator frazy. Pozyskaj go z `positions/getData` (pole `keyword_id`). * Realny `keyword_id` (i odpowiadający mu `kid`) pobierzesz z `POST /api/visibility_analysis/reports/positions/getData`. */ keyword_id: number; /** * **Wymagane**. Hash identyfikujący frazę w kontekście domeny. Pozyskaj go z `positions/getData` (pole `kid`). */ kid: string; /** * Liczba wierszy na stronę. Nieujemna liczba całkowita. * @default 10 */ limit?: number; /** * Numer strony. Nieujemna liczba całkowita. * @default 1 */ page?: number; /** * ⚠️ **Bez zastosowania w tym endpoincie.** Odpowiedź to mapa * `history_positions` (klucz = data pomiaru), nie lista — nie ma czego * sortować, a kontroler nie przekazuje `order` do komponentu. * Zweryfikowane na prod: dir asc i desc zwracają identyczną mapę. */ order?: unknown; /** * ⚠️ **Bez zastosowania w tym endpoincie.** Endpoint nie odczytuje `filtering` * (zweryfikowane). Zweryfikowane * na prod: nieznany klucz filtra NIE zwraca `418` (jest po cichu ignorowany), * a wynik jest identyczny jak bez `filtering`. Zawężanie historii rób po stronie klienta. */ filtering?: unknown[]; } export default PositionsGetKeywordHistoryRequest ``` > **Ostrzeżenie:** > Ten endpoint zwraca **mapę** historii pozycji (klucz = data), a nie stronicowaną listę — parametry `order`, `filtering`, `limit` i `page` nie mają tu zastosowania (`filtering` jest przyjmowane, ale ignorowane — nie zwraca nawet `418` na nieznanym kluczu). Wymagane są wyłącznie `domain`, `fetch_mode`, `keyword_id` i `kid`. > **Ostrzeżenie:** > Endpoint przyjmuje wyłącznie metodę **`POST`**. Oprócz `keyword_id` oraz `kid` wymagane są również **`domain`** i **`fetch_mode`** — pominięcie któregokolwiek z parametrów zwraca `418` z `invalid_data`. Identyfikatory `keyword_id` i `kid` muszą pochodzić z `positions/getData` dla tej samej domeny. ## Odpowiedź Po pomyślnym żądaniu otrzymujesz `data` z obiektem `history_positions`. W przeciwieństwie do `getData` ta odpowiedź **nie zawiera `pagination`** — zwracana jest cała mapa historii. Klucze mapy to daty pomiarów (`RRRR-MM-DD`), a wartości opisują pozycję oraz obecność snippetów SERP w danym dniu. **Skrócona** ```json filename="przykładowa-odpowiedź (skrócona)" { "success": true, "data": { "history_positions": { "2026-05-28": { "position": 29, "has_serp": true } } } } ``` **Pełna** ```json filename="przykładowa-odpowiedź (200)" { "success": true, "data": { "history_positions": { "2026-05-28": { "position": 29, "has_serp": true } } } } ``` ### Struktura odpowiedzi ```ts type PositionsKeywordHistoryResponse = { /** `true` przy powodzeniu; przy błędzie `false` i koperta z `error` */ success: boolean; /** Dane historii pozycji frazy */ data: { /** * Mapa historii pozycji: klucz to data pomiaru (RRRR-MM-DD), * wartość opisuje pozycję oraz obecność snippetów SERP w tym dniu. */ history_positions: Record; }; } export default PositionsKeywordHistoryResponse ``` ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data, invalid_filtering, unreachable_data, timeout, database, unknown */ type: string; message: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > **`418`** jest zwracane również w przypadku błędów walidacji — nie tylko przy ograniczaniu liczby żądań. Brak `fetch_mode` → > `{"success":false,"data":{"error":{"type":"invalid_data","params":{"fetch_mode":{"_required":"This field is required"}}}}}`. Analogicznie pominięcie `keyword_id` lub `kid` zwraca `418` z `invalid_data`. ## Powiązane akcje - `getData` — bieżące pozycje fraz (źródło `keyword_id` oraz `kid`) - `getWins` / `getLosses` — frazy, które zyskały / straciły pozycje (taki sam kształt żądania co `getData`) - `getKeywordHistory` — pełna historia pozycji dla pojedynczej frazy (`keyword_id` + `kid` + `domain` + `fetch_mode`) (ta strona) --- # Historia fraz: przegląd (`getData`) **`POST /api/visibility_analysis/reports/history/keywords/getData`** Zwraca frazy, na które domena rankowała w wybranym zakresie dat, wraz ze statystykami dla każdej frazy (pozycja, widoczność, URL, CPC, liczba wyszukiwań, trendy, trudność, snippety SERP) oraz osadzoną mapą `history` z historią pozycji. Użyj jej, aby sprawdzić, jak wyglądał zestaw fraz domeny i jej rankingi w wybranym oknie historycznym. Wyniki są sortowane według pojedynczej dyrektywy sortowania. | Fraza | ID frazy | KID | Domena | Liczba słów | Pozycja | Pozycja poprz. | Zmiana pozycji | Wzrosty | Spadki | Bez zmian | Widoczność | Widoczność poprz. | Δ widoczności | Widoczność % | URL bieżący | URL poprzedni | URL zmiana | CPC | Wyszukiwania/mies. | Trend (12 mies.) | Szczyt trendu | Trudność | Snippety SERP | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | zalando | 13624651 | b8ac304f24a9864f46f86cbebc0820f1 | zalando.pl | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 651480 | 651480 | 0 | 0 | zalando.pl/ | zalando.pl/ | 0 | 2.03 | 1830000 | [1830000,1830000,1830000,1830000,1830000,2240000,2240000,1830000,1830000,1500000,2240000,1830000] | | 71 | ["video_thumbs"] | _zalando.pl, sort: widoczność malejąco. Wszystkie pola wiersza (poza mapą historii `statistics.position.history`, która ma zmienne klucze-daty — jest w JSON i sekcji „Struktura odpowiedzi”)._ --- ## Żądanie `POST` `/api/visibility_analysis/reports/history/keywords/getData` Nagłówki: `Authorization: Bearer `, `Content-Type: application/json`. ### Struktura żądania **Podstawowy** ```jsonc filename="żądanie-podstawowe.jsonc" { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "date_min": "2026-06-20", "date_max": "2026-06-29", "country_id": 1, "order": { "prop": "statistics.visibility.current", "dir": "desc" } } ``` **Rozszerzony** ```jsonc filename="żądanie-rozszerzone.jsonc" { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "date_min": "2026-06-20", "date_max": "2026-06-29", "country_id": 1, "order": { "prop": "statistics.visibility.current", "dir": "desc" }, "limit": 10, "page": 1 } ``` **cURL** ```bash curl --location --request POST 'https://api.senuto.com/api/visibility_analysis/reports/history/keywords/getData' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' \ --data-raw '{ "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "date_min": "2026-06-20", "date_max": "2026-06-29", "country_id": 1, "order": { "prop": "statistics.visibility.current", "dir": "desc" } }' ``` ### Parametry ```ts type HistoryKeywordsGetDataRequest = { /** * **Wymagane**. Domena, subdomena, katalog lub URL do analizy — interpretowane zgodnie z `fetch_mode`. */ domain: string; /** * **Wymagane**. Sposób interpretacji `domain`. * - `topLevelDomain` — cała domena (najczęstszy przypadek; stara wartość "domain" mapuje się tutaj) * - `subdomain` — pojedyncza subdomena * - `catalog` — ścieżka/katalog * - `url` — dokładny URL */ fetch_mode: 'topLevelDomain' | 'subdomain' | 'catalog' | 'url'; /** * **Wymagane**. Początek zakresu dat, `YYYY-MM-DD`. Musi być nie późniejszy niż dzisiaj i nie późniejszy niż `date_max`. */ date_min: string; /** * **Wymagane**. Koniec zakresu dat, `YYYY-MM-DD`. Musi być nie późniejszy niż dzisiaj i nie wcześniejszy niż `date_min`. */ date_max: string; /** * **Wymagane**. Id kraju (bazy danych). Polska = `1`. */ country_id: number; /** * **Wymagane**. **Pojedyncza** dyrektywa sortowania (jeden obiekt — nie tablica). * `prop` to jedna z dozwolonych właściwości sortowalnych; `dir` to kierunek. * * Dozwolone wartości `prop`: * - `keyword` * - `statistics.position.current` * - `statistics.position.previous` * - `statistics.position.diff` * - `statistics.visibility.current` * - `statistics.visibility.previous` * - `statistics.visibility.diff` * - `statistics.difficulty.current` * - `statistics.searches.current` * - `statistics.cpc.current` * - `statistics.url.is_change` */ order: { prop: | 'keyword' | 'statistics.position.current' | 'statistics.position.previous' | 'statistics.position.diff' | 'statistics.visibility.current' | 'statistics.visibility.previous' | 'statistics.visibility.diff' | 'statistics.difficulty.current' | 'statistics.searches.current' | 'statistics.cpc.current' | 'statistics.url.is_change'; dir: 'asc' | 'desc'; }; /** * Liczba wierszy na stronę. * @default 10 */ limit?: number; /** * Numer strony. * @default 1 */ page?: number; } export default HistoryKeywordsGetDataRequest ``` > **Ostrzeżenie:** > **`order` to pojedynczy obiekt** (`{ prop, dir }`) — nie tablica. Używaj ścieżek `prop` z kropkami wymienionych powyżej; dowolne inne pola są odrzucane. > **Ostrzeżenie:** > Pięć parametrów jest **wymaganych**: **`domain`**, **`fetch_mode`**, **`date_min`**, **`date_max`** oraz **`country_id`**, a także pojedynczy obiekt **`order`**. Pominięcie któregokolwiek wymaganego pola zwraca `418` z `invalid_data`. ## Filtrowanie Opcjonalny parametr `filtering` (tablica grup, filtry w grupie łączone operatorem AND) zawęża wyniki. Ten endpoint korzysta z **tego samego mechanizmu i tych samych kluczy filtrów co [Pozycje → Filtrowanie](/modules/visibility_analysis/positions#filtrowanie)** (dzielą komponent danych) — m.in. `keywords` (przez `items`: `contain`/`startsWith`/`endsWith`/`notContain`) oraz liczbowe `statistics.position.current`, `statistics.visibility.current`, `statistics.cpc.current`, `statistics.difficulty.current`, `statistics.searches.current`, `words_count` (`eq`/`gt`/`gte`/`lt`/`lte`). ```jsonc filename="żądanie-z-filtrowaniem.jsonc" { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "date_min": "2026-05-01", "date_max": "2026-06-29", "country_id": 1, "order": { "prop": "statistics.visibility.current", "dir": "desc" }, "filtering": [ { "filters": [ { "key": "statistics.position.current", "match": "lte", "value": 3 }, { "key": "keywords", "items": [{ "match": "contain", "value": "buty" }] } ] } ] } ``` > **Informacja:** > Zwalidowane na żywo (`zalando.pl`): bez filtra `count` = 300 894; z powyższym filtrem (TOP3 + fraza zawiera „buty") → `count` = 2 832. ## Odpowiedź W przypadku powodzenia otrzymujesz `data` (tablicę wierszy z frazami) oraz `pagination`. **Skrócona** ```json filename="przykładowa-odpowiedź (skrócona)" { "success": true, "data": [ { "keyword_id": 13624651, "keyword": "zalando", "statistics": { "position": { "current": 1 }, "visibility": { "current": 651480 } /* … */ } } ], "pagination": { "page_count": 145940, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 291879, "limit": 10 } } ``` **Pełna** ```json filename="przykładowa-odpowiedź (200)" { "success": true, "data": [ { "keyword_id": 13624651, "kid": "b8ac304f24a9864f46f86cbebc0820f1", "domain": "zalando.pl", "keyword": "zalando", "words_count": 1, "statistics": { "position": { "current": 1, "previous": 1, "diff": 0, "changes": { "wins": 0, "losses": 0, "no_changes": 0 }, "history": { "2026-06-02": { "position": 1, "has_serp": true } } }, "visibility": { "current": 651480, "previous": 651480, "diff": 0, "percent": 0, "history": null }, "url": { "current": "zalando.pl/", "previous": "zalando.pl/", "is_change": 0 }, "cpc": { "current": 2.03 }, "searches": { "current": 1830000 }, "trends": { "history": [1830000, 1830000, 1830000, 1830000, 1830000, 2240000, 2240000, 1830000, 1830000, 1500000, 2240000, 1830000], "peak": null }, "difficulty": { "current": 71 }, "snippets": { "current": ["video_thumbs"] } } } ], "pagination": { "page_count": 145940, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 291879, "limit": 10 } } ``` ### Struktura odpowiedzi ```ts type HistoryKeywordsResponse = { /** `true` przy powodzeniu; przy błędzie `false` i koperta z `error` */ success: boolean; /** Zwrócone wiersze z frazami */ data: KeywordRow[]; /** Metadane paginacji */ pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; limit: number; }; } type KeywordRow = { keyword_id: number; kid: string; domain: string; keyword: string; words_count: number; statistics: { position: { current: number; previous: number; diff: number; changes: { wins: number; losses: number; no_changes: number }; history: Record | [] }; visibility: { current: number; previous: number; diff: number; percent: number; history: null }; url: { current: string; previous: string; is_change: 0 | 1 }; cpc: { current: number }; searches: { current: number }; trends: { history: number[]; peak: number | null }; difficulty: { current: number }; snippets: { current: string[] }; }; } export default HistoryKeywordsResponse ``` ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data, invalid_filtering, database, timeout, unknown */ type: string; message: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > **`418`** zwracany jest również dla błędów walidacji — nie tylko przy ograniczeniu liczby żądań (rate limiting). Brak wymaganego pola → > `{"success":false,"data":{"error":{"type":"invalid_data","params":{"country_id":{"_required":"This field is required"}}}}}`. > > **Znany błąd — mylący komunikat.** Gdy zakres dat jest poprawny (`date_min <= date_max`), ale wystąpi naruszenie `DateRangeRules`, zwrócony komunikat brzmi `"date_max must be less or equal than date_min"`. Treść jest błędna (sama logika działa poprawnie) — należy go odczytywać jako niepowodzenie reguły zakresu dat, a nie jako dosłowną instrukcję. ## Powiązane akcje - `getData` — frazy w zakresie dat (`MODE_DATA`, ta strona) - `getWins` / `getLosses` — frazy, które zyskały / straciły pozycje w danym zakresie (taka sama struktura żądania) - `getAcquired` / `getLost` — frazy nowo pozyskane / całkowicie utracone w zakresie (taka sama struktura żądania) - `getDates` — dostępne daty dla zakresu (lżejsze wywołanie, używa osobnego walidatora) --- # Historia fraz: wzrosty (`getWins`) **`POST /api/visibility_analysis/reports/history/keywords/getWins`** Zwraca frazy, których pozycja **poprawiła się** między `date_min` a `date_max` (tryb `MODE_INCREASE` tego samego komponentu danych co `getData`). W danych poprawa oznacza **ujemny** `statistics.position.diff` — np. przejście z pozycji 3 na 1 daje `diff: -2`. Struktura żądania i odpowiedzi jest identyczna jak w `getData`; zmienia się wyłącznie zestaw zwracanych wierszy. > **Ostrzeżenie:** > **Ta akcja zawiera też frazy nowo pozyskane — i nie jest symetryczna wobec `getLosses`.** > > Frazy, na które domena nie rankowała na `date_min`, a rankuje na `date_max`, **trafiają również tutaj** — jako skok z pozycji `51` (sentinel „poza TOP50"), np. `{"current": 26, "previous": 51, "diff": -25}`. Nie są odfiltrowane. Jeśli potrzebujesz wyłącznie ruchu **wewnątrz** TOP50, odrzuć wiersze z `statistics.position.previous === 51`. > > Siostrzana [`getLosses`](/modules/visibility_analysis/va-history-keywords-getLosses) działa **odwrotnie** — frazy utracone są z niej wykluczone i występują tylko w [`getLost`](/modules/visibility_analysis/va-history-keywords-getLost). Konsekwencje przy liczeniu: > > - **Pełny obraz zysków** = samo `getWins`. Doklejenie [`getAcquired`](/modules/visibility_analysis/va-history-keywords-getAcquired) **zdubluje** wiersze — to podzbiór tej akcji. > - **Pełny obraz strat** wymaga sklejenia `getLosses` + `getLost` (te zbiory są rozłączne). > - Zestawianie `count` z `getWins` i `getLosses` porównuje dwie różnie zdefiniowane wielkości — zyski są zawyżone o pozyskane, straty zaniżone o utracone. > > Zachowanie może się w przyszłości ujednolicić — na dziś traktuj powyższe jako obowiązujący kontrakt. | Fraza | ID frazy | KID | Domena | Liczba słów | Pozycja | Pozycja poprz. | Zmiana pozycji | Wzrosty | Spadki | Bez zmian | Widoczność | Widoczność poprz. | Δ widoczności | Widoczność % | URL bieżący | URL poprzedni | URL zmiana | CPC | Wyszukiwania/mies. | Trend (12 mies.) | Szczyt trendu | Trudność | Snippety SERP | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | nie air max | 1421737 | 1344167bcc1aac3b96cfe7420ae6244e | zalando.pl | 3 | 1 | 3 | -2 | 0 | 0 | 0 | 21538 | 11825 | 9713 | 0.8214 | zalando.pl/obuwie/?q=nike+air+max | zalando.pl/obuwie/?q=nike+air+max | 0 | 0.83 | 60500 | [90500,74000,60500,74000,60500,60500,49500,40500,40500,49500,110000,74000] | | 57 | ["image_thumbs","people_also_ask","spell"] | | uggs buty | 5591577 | 4bb5c4ba39dc62e9d6d6e1a03dcb344a | zalando.pl | 2 | 1 | 4 | -3 | 1 | 0 | 0 | 21538 | 4295.5 | 17242.5 | 4.0141 | zalando.pl/obuwie/ugg/ | zalando.pl/obuwie/ugg/ | 0 | 0.58 | 60500 | [0,0,0,0,0,0,0,0,0,0,0,0] | | 52 | ["image_thumbs","people_also_ask"] | _zalando.pl, sort: widoczność malejąco — frazy, których pozycja się poprawiła (ujemny „diff”). Wszystkie pola wiersza (poza mapą historii `statistics.position.history`, która ma zmienne klucze-daty — jest w JSON i sekcji „Struktura odpowiedzi”)._ --- ## Żądanie `POST` `/api/visibility_analysis/reports/history/keywords/getWins` Nagłówki: `Authorization: Bearer `, `Content-Type: application/json`. ### Struktura żądania **Podstawowy** ```jsonc filename="żądanie-podstawowe.jsonc" { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "date_min": "2026-06-20", "date_max": "2026-06-29", "country_id": 1, "order": { "prop": "statistics.visibility.current", "dir": "desc" } } ``` **Rozszerzony** ```jsonc filename="żądanie-rozszerzone.jsonc" { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "date_min": "2026-06-20", "date_max": "2026-06-29", "country_id": 1, "order": { "prop": "statistics.visibility.current", "dir": "desc" }, "limit": 10, "page": 1 } ``` **cURL** ```bash curl --location --request POST 'https://api.senuto.com/api/visibility_analysis/reports/history/keywords/getWins' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' \ --data-raw '{ "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "date_min": "2026-06-20", "date_max": "2026-06-29", "country_id": 1, "order": { "prop": "statistics.visibility.current", "dir": "desc" } }' ``` ### Parametry ```ts type HistoryKeywordsGetWinsRequest = { /** * **Wymagane**. Domena, subdomena, katalog lub URL do analizy — interpretowane zgodnie z `fetch_mode`. */ domain: string; /** * **Wymagane**. Sposób interpretacji `domain`. * - `topLevelDomain` — cała domena (najczęstszy przypadek) * - `subdomain` — pojedyncza subdomena * - `catalog` — ścieżka/katalog * - `url` — dokładny URL * * Wartość `domain` nie istnieje. */ fetch_mode: 'topLevelDomain' | 'subdomain' | 'catalog' | 'url'; /** * **Wymagane**. Początek zakresu dat, `YYYY-MM-DD`. Musi być nie późniejszy niż `date_max`. * Dostępne daty pobierzesz akcją `getDates`. */ date_min: string; /** * **Wymagane**. Koniec zakresu dat, `YYYY-MM-DD`. Musi być nie wcześniejszy niż `date_min`. */ date_max: string; /** * **Wymagane**. Id kraju (bazy danych). Polska = `1`. Nieznana wartość → `418` "Unknown country_id". */ country_id: number; /** * **Wymagane**. **Pojedyncza** dyrektywa sortowania (jeden obiekt — nie tablica). * `prop` to jedna z dozwolonych właściwości sortowalnych; `dir` to kierunek. * * Dozwolone wartości `prop`: * - `keyword` * - `statistics.position.current` * - `statistics.position.previous` * - `statistics.position.diff` * - `statistics.visibility.current` * - `statistics.visibility.previous` * - `statistics.visibility.diff` * - `statistics.difficulty.current` * - `statistics.searches.current` * - `statistics.cpc.current` * - `statistics.url.is_change` */ order: { prop: | 'keyword' | 'statistics.position.current' | 'statistics.position.previous' | 'statistics.position.diff' | 'statistics.visibility.current' | 'statistics.visibility.previous' | 'statistics.visibility.diff' | 'statistics.difficulty.current' | 'statistics.searches.current' | 'statistics.cpc.current' | 'statistics.url.is_change'; dir: 'asc' | 'desc'; }; /** * Liczba wierszy na stronę. * @default 10 */ limit?: number; /** * Numer strony. * @default 1 */ page?: number; } export default HistoryKeywordsGetWinsRequest ``` > **Ostrzeżenie:** > **`order` to pojedynczy obiekt** (`{ prop, dir }`) — nie tablica. Błędna wartość `prop` zwraca `418` `invalid_data` z komunikatem (dosłownym): `This value is not allow. Please use correct colum name`. > **Ostrzeżenie:** > Pięć parametrów jest **wymaganych**: **`domain`**, **`fetch_mode`**, **`date_min`**, **`date_max`** oraz **`country_id`**, a także pojedynczy obiekt **`order`**. Wartość `fetch_mode` musi być jedną z `topLevelDomain` / `subdomain` / `catalog` / `url` — wartość `domain` **nie istnieje**. `order` to **pojedynczy obiekt** `{ prop, dir }`, nie tablica. Pominięcie któregokolwiek wymaganego pola zwraca `418` z `invalid_data`; nieznane `country_id` → `418` z komunikatem `Unknown country_id`. ## Filtrowanie Opcjonalny parametr `filtering` (tablica grup filtrów) zawęża wyniki. Endpoint korzysta z **tego samego mechanizmu i tych samych kluczy filtrów** co `positions/getData` oraz `history/keywords/getData` — szczegóły i pełną listę kluczy znajdziesz na stronie [typów filtrów](/types/filter). ## Odpowiedź W przypadku powodzenia otrzymujesz `data` (tablicę wierszy z frazami, których pozycja się poprawiła) oraz `pagination`. Pozycja `51` to wartość specjalna oznaczająca frazę poza TOP50. **Skrócona** ```json filename="przykładowa-odpowiedź (skrócona)" { "success": true, "data": [ { "keyword_id": 1421737, "keyword": "nie air max", "statistics": { "position": { "current": 1, "previous": 3, "diff": -2 }, "visibility": { "current": 21538 } /* … */ } } ], "pagination": { "page_count": 4115, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 8229, "limit": 2 } } ``` **Pełna** ```json filename="przykładowa-odpowiedź (200)" { "success": true, "data": [ { "keyword_id": 1421737, "kid": "1344167bcc1aac3b96cfe7420ae6244e", "domain": "zalando.pl", "keyword": "nie air max", "words_count": 3, "statistics": { "position": { "current": 1, "previous": 3, "diff": -2, "changes": { "wins": 0, "losses": 0, "no_changes": 0 }, "history": { "2026-06-24": { "position": 1, "has_serp": true } } }, "visibility": { "current": 21538, "previous": 11825, "diff": 9713, "percent": 0.8214, "history": null }, "url": { "current": "zalando.pl/obuwie/?q=nike+air+max", "previous": "zalando.pl/obuwie/?q=nike+air+max", "is_change": 0 }, "cpc": { "current": 0.83 }, "searches": { "current": 60500 }, "trends": { "history": [90500, 74000, 60500, 74000, 60500, 60500, 49500, 40500, 40500, 49500, 110000, 74000], "peak": null }, "difficulty": { "current": 57 }, "snippets": { "current": ["image_thumbs", "people_also_ask", "spell"] } } }, { "keyword_id": 5591577, "kid": "4bb5c4ba39dc62e9d6d6e1a03dcb344a", "domain": "zalando.pl", "keyword": "uggs buty", "words_count": 2, "statistics": { "position": { "current": 1, "previous": 4, "diff": -3, "changes": { "wins": 1, "losses": 0, "no_changes": 0 }, "history": { "2026-05-24": { "position": 4, "has_serp": true }, "2026-06-25": { "position": 1, "has_serp": true } } }, "visibility": { "current": 21538, "previous": 4295.5, "diff": 17242.5, "percent": 4.0141, "history": null }, "url": { "current": "zalando.pl/obuwie/ugg/", "previous": "zalando.pl/obuwie/ugg/", "is_change": 0 }, "cpc": { "current": 0.58 }, "searches": { "current": 60500 }, "trends": { "history": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "peak": null }, "difficulty": { "current": 52 }, "snippets": { "current": ["image_thumbs", "people_also_ask"] } } } ], "pagination": { "page_count": 4115, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 8229, "limit": 2 } } ``` ### Struktura odpowiedzi ```ts type HistoryKeywordsGetWinsResponse = { /** `true` przy powodzeniu; przy błędzie `false` i koperta z `error` */ success: boolean; /** Zwrócone wiersze z frazami, których pozycja się poprawiła */ data: KeywordRow[]; /** Metadane paginacji */ pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; limit: number; }; } type KeywordRow = { keyword_id: number; kid: string; domain: string; keyword: string; words_count: number; statistics: { /** Dla wzrostów `diff` jest ujemny (np. 3 → 1 daje diff = -2). Wartość 51 oznacza pozycję poza TOP50. */ position: { current: number; previous: number; diff: number; changes: { wins: number; losses: number; no_changes: number }; history: Record }; visibility: { current: number; previous: number; diff: number; percent: number; history: null }; /** `is_change` jest liczbowe: 0 lub 1 */ url: { current: string; previous: string; is_change: 0 | 1 }; cpc: { current: number }; searches: { current: number }; trends: { history: number[]; peak: number | null }; difficulty: { current: number }; snippets: { current: string[] }; }; } export default HistoryKeywordsGetWinsResponse ``` ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data */ type: string; message: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > **`418`** zwracany jest również dla błędów walidacji. Brak wymaganego pola → `invalid_data`; nieznane `country_id` → `Unknown country_id`; błędny `order.prop` → `This value is not allow. Please use correct colum name`. > > **Znany błąd — odwrócony komunikat.** Przy `date_min > date_max` komunikat reguły `DateRangeRules` jest odwrócony i brzmi `"date_max must be less or equal than date_min"`. Należy go odczytywać jako niepowodzenie reguły zakresu dat, a nie jako dosłowną instrukcję. ## Powiązane akcje - [`getData`](/modules/visibility_analysis/va-history-keywords) — frazy w zakresie dat (`MODE_DATA`) - `getWins` — frazy, które poprawiły pozycje (ta strona) - [`getLosses`](/modules/visibility_analysis/va-history-keywords-getLosses) — frazy, które straciły pozycje (taka sama struktura żądania) - `getAcquired` / `getLost` — frazy nowo pozyskane / całkowicie utracone w zakresie (taka sama struktura żądania) - `getDates` — dostępne daty (POST, wymaga tylko `country_id`) --- # Historia fraz: spadki (`getLosses`) **`POST /api/visibility_analysis/reports/history/keywords/getLosses`** Zwraca frazy, których pozycja **pogorszyła się** między `date_min` a `date_max` (tryb `MODE_DECREASE` tego samego komponentu danych co `getData`). W danych spadek oznacza **dodatni** `statistics.position.diff` — np. przejście z pozycji 2 na 5 daje `diff: 3`. Struktura żądania i odpowiedzi jest identyczna jak w `getData`; zmienia się wyłącznie zestaw zwracanych wierszy. > **Ostrzeżenie:** > **Ta akcja nie zawiera fraz utraconych — i nie jest symetryczna wobec `getWins`.** > > Frazy, które wypadły poza TOP50 na `date_max`, **nie pojawią się tutaj**, mimo że formalnie „spadły". Znajdziesz je wyłącznie w [`getLost`](/modules/visibility_analysis/va-history-keywords-getLost). Ta akcja pokazuje więc wyłącznie ruch **wewnątrz** TOP50: fraza musi rankować zarówno na `date_min`, jak i na `date_max`. > > Siostrzana [`getWins`](/modules/visibility_analysis/va-history-keywords-getWins) działa **odwrotnie** — zawiera też frazy nowo pozyskane. Konsekwencje przy liczeniu: > > - **Pełny obraz strat** = `getLosses` + `getLost`. Zbiory są rozłączne, więc możesz je bezpiecznie skleić. > - **Pełny obraz zysków** = samo `getWins`. Doklejenie `getAcquired` **zdubluje** wiersze. > - Zestawianie `count` z `getWins` i `getLosses` porównuje dwie różnie zdefiniowane wielkości — zyski są zawyżone o pozyskane, straty zaniżone o utracone. > > Zachowanie może się w przyszłości ujednolicić — na dziś traktuj powyższe jako obowiązujący kontrakt. | Fraza | ID frazy | KID | Domena | Liczba słów | Pozycja | Pozycja poprz. | Zmiana pozycji | Wzrosty | Spadki | Bez zmian | Widoczność | Widoczność poprz. | Δ widoczności | Widoczność % | URL bieżący | URL poprzedni | URL zmiana | CPC | Wyszukiwania/mies. | Trend (12 mies.) | Szczyt trendu | Trudność | Snippety SERP | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | ochnik | 14897112 | c9dba6c1c694aa2bd10bceab8716e8d7 | zalando.pl | 1 | 5 | 2 | 3 | 0 | 1 | 0 | 22635 | 78840 | -56205 | -0.7129 | zalando.pl/ochnik/ | zalando.pl/ochnik/ | 0 | 0.42 | 450000 | [368000,301000,301000,368000,450000,673000,823000,823000,673000,550000,450000,368000] | | 60 | ["map","people_also_ask","wiki_right"] | | pinko torebka | 11672176 | 9e41576fb002d977104a4c8f9fac9015 | zalando.pl | 2 | 3 | 2 | 1 | 0 | 1 | 0 | 5321.25 | 8672.4 | -3351.15 | -0.3864 | zalando.pl/akcesoria-torby-kobiety/pinko/ | zalando.pl/akcesoria-torby-kobiety/pinko/ | 0 | 0.14 | 49500 | [60500,49500,49500,40500,49500,40500,49500,60500,40500,40500,74000,74000] | | 34 | ["image_thumbs","people_also_ask"] | _zalando.pl, sort: widoczność malejąco — frazy, których pozycja się pogorszyła (dodatni „diff”, ujemna Δ widoczności). Wszystkie pola wiersza (poza mapą historii `statistics.position.history`, która ma zmienne klucze-daty — jest w JSON i sekcji „Struktura odpowiedzi”)._ --- ## Żądanie `POST` `/api/visibility_analysis/reports/history/keywords/getLosses` Nagłówki: `Authorization: Bearer `, `Content-Type: application/json`. ### Struktura żądania **Podstawowy** ```jsonc filename="żądanie-podstawowe.jsonc" { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "date_min": "2026-06-20", "date_max": "2026-06-29", "country_id": 1, "order": { "prop": "statistics.visibility.current", "dir": "desc" } } ``` **Rozszerzony** ```jsonc filename="żądanie-rozszerzone.jsonc" { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "date_min": "2026-06-20", "date_max": "2026-06-29", "country_id": 1, "order": { "prop": "statistics.visibility.current", "dir": "desc" }, "limit": 10, "page": 1 } ``` **cURL** ```bash curl --location --request POST 'https://api.senuto.com/api/visibility_analysis/reports/history/keywords/getLosses' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' \ --data-raw '{ "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "date_min": "2026-06-20", "date_max": "2026-06-29", "country_id": 1, "order": { "prop": "statistics.visibility.current", "dir": "desc" } }' ``` ### Parametry ```ts type HistoryKeywordsGetLossesRequest = { /** * **Wymagane**. Domena, subdomena, katalog lub URL do analizy — interpretowane zgodnie z `fetch_mode`. */ domain: string; /** * **Wymagane**. Sposób interpretacji `domain`. * - `topLevelDomain` — cała domena (najczęstszy przypadek) * - `subdomain` — pojedyncza subdomena * - `catalog` — ścieżka/katalog * - `url` — dokładny URL * * Wartość `domain` nie istnieje. */ fetch_mode: 'topLevelDomain' | 'subdomain' | 'catalog' | 'url'; /** * **Wymagane**. Początek zakresu dat, `YYYY-MM-DD`. Musi być nie późniejszy niż `date_max`. * Dostępne daty pobierzesz akcją `getDates`. */ date_min: string; /** * **Wymagane**. Koniec zakresu dat, `YYYY-MM-DD`. Musi być nie wcześniejszy niż `date_min`. */ date_max: string; /** * **Wymagane**. Id kraju (bazy danych). Polska = `1`. Nieznana wartość → `418` "Unknown country_id". */ country_id: number; /** * **Wymagane**. **Pojedyncza** dyrektywa sortowania (jeden obiekt — nie tablica). * `prop` to jedna z dozwolonych właściwości sortowalnych; `dir` to kierunek. * * Dozwolone wartości `prop`: * - `keyword` * - `statistics.position.current` * - `statistics.position.previous` * - `statistics.position.diff` * - `statistics.visibility.current` * - `statistics.visibility.previous` * - `statistics.visibility.diff` * - `statistics.difficulty.current` * - `statistics.searches.current` * - `statistics.cpc.current` * - `statistics.url.is_change` */ order: { prop: | 'keyword' | 'statistics.position.current' | 'statistics.position.previous' | 'statistics.position.diff' | 'statistics.visibility.current' | 'statistics.visibility.previous' | 'statistics.visibility.diff' | 'statistics.difficulty.current' | 'statistics.searches.current' | 'statistics.cpc.current' | 'statistics.url.is_change'; dir: 'asc' | 'desc'; }; /** * Liczba wierszy na stronę. * @default 10 */ limit?: number; /** * Numer strony. * @default 1 */ page?: number; } export default HistoryKeywordsGetLossesRequest ``` > **Ostrzeżenie:** > **`order` to pojedynczy obiekt** (`{ prop, dir }`) — nie tablica. Błędna wartość `prop` zwraca `418` `invalid_data` z komunikatem (dosłownym): `This value is not allow. Please use correct colum name`. > **Ostrzeżenie:** > Pięć parametrów jest **wymaganych**: **`domain`**, **`fetch_mode`**, **`date_min`**, **`date_max`** oraz **`country_id`**, a także pojedynczy obiekt **`order`**. Wartość `fetch_mode` musi być jedną z `topLevelDomain` / `subdomain` / `catalog` / `url` — wartość `domain` **nie istnieje**. `order` to **pojedynczy obiekt** `{ prop, dir }`, nie tablica. Pominięcie któregokolwiek wymaganego pola zwraca `418` z `invalid_data`; nieznane `country_id` → `418` z komunikatem `Unknown country_id`. ## Filtrowanie Opcjonalny parametr `filtering` (tablica grup filtrów) zawęża wyniki. Endpoint korzysta z **tego samego mechanizmu i tych samych kluczy filtrów** co `positions/getData` oraz `history/keywords/getData` — szczegóły i pełną listę kluczy znajdziesz na stronie [typów filtrów](/types/filter). ## Odpowiedź W przypadku powodzenia otrzymujesz `data` (tablicę wierszy z frazami, których pozycja się pogorszyła) oraz `pagination`. Pozycja `51` to wartość specjalna oznaczająca frazę poza TOP50. **Skrócona** ```json filename="przykładowa-odpowiedź (skrócona)" { "success": true, "data": [ { "keyword_id": 14897112, "keyword": "ochnik", "statistics": { "position": { "current": 5, "previous": 2, "diff": 3 }, "visibility": { "current": 22635 } /* … */ } } ], "pagination": { "page_count": 3004, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 6007, "limit": 2 } } ``` **Pełna** ```json filename="przykładowa-odpowiedź (200)" { "success": true, "data": [ { "keyword_id": 14897112, "kid": "c9dba6c1c694aa2bd10bceab8716e8d7", "domain": "zalando.pl", "keyword": "ochnik", "words_count": 1, "statistics": { "position": { "current": 5, "previous": 2, "diff": 3, "changes": { "wins": 0, "losses": 1, "no_changes": 0 }, "history": { "2026-05-17": { "position": 2, "has_serp": true }, "2026-06-21": { "position": 5, "has_serp": true } } }, "visibility": { "current": 22635, "previous": 78840, "diff": -56205, "percent": -0.7129, "history": null }, "url": { "current": "zalando.pl/ochnik/", "previous": "zalando.pl/ochnik/", "is_change": 0 }, "cpc": { "current": 0.42 }, "searches": { "current": 450000 }, "trends": { "history": [368000, 301000, 301000, 368000, 450000, 673000, 823000, 823000, 673000, 550000, 450000, 368000], "peak": null }, "difficulty": { "current": 60 }, "snippets": { "current": ["map", "people_also_ask", "wiki_right"] } } }, { "keyword_id": 11672176, "kid": "9e41576fb002d977104a4c8f9fac9015", "domain": "zalando.pl", "keyword": "pinko torebka", "words_count": 2, "statistics": { "position": { "current": 3, "previous": 2, "diff": 1, "changes": { "wins": 0, "losses": 1, "no_changes": 0 }, "history": { "2026-05-25": { "position": 2, "has_serp": true }, "2026-06-26": { "position": 3, "has_serp": true } } }, "visibility": { "current": 5321.25, "previous": 8672.4, "diff": -3351.15, "percent": -0.3864, "history": null }, "url": { "current": "zalando.pl/akcesoria-torby-kobiety/pinko/", "previous": "zalando.pl/akcesoria-torby-kobiety/pinko/", "is_change": 0 }, "cpc": { "current": 0.14 }, "searches": { "current": 49500 }, "trends": { "history": [60500, 49500, 49500, 40500, 49500, 40500, 49500, 60500, 40500, 40500, 74000, 74000], "peak": null }, "difficulty": { "current": 34 }, "snippets": { "current": ["image_thumbs", "people_also_ask"] } } } ], "pagination": { "page_count": 3004, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 6007, "limit": 2 } } ``` ### Struktura odpowiedzi ```ts type HistoryKeywordsGetLossesResponse = { /** `true` przy powodzeniu; przy błędzie `false` i koperta z `error` */ success: boolean; /** Zwrócone wiersze z frazami, których pozycja się pogorszyła */ data: KeywordRow[]; /** Metadane paginacji */ pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; limit: number; }; } type KeywordRow = { keyword_id: number; kid: string; domain: string; keyword: string; words_count: number; statistics: { /** Dla spadków `diff` jest dodatni (np. 2 → 5 daje diff = 3). Wartość 51 oznacza pozycję poza TOP50. */ position: { current: number; previous: number; diff: number; changes: { wins: number; losses: number; no_changes: number }; history: Record }; visibility: { current: number; previous: number; diff: number; percent: number; history: null }; /** `is_change` jest liczbowe: 0 lub 1 */ url: { current: string; previous: string; is_change: 0 | 1 }; cpc: { current: number }; searches: { current: number }; trends: { history: number[]; peak: number | null }; difficulty: { current: number }; snippets: { current: string[] }; }; } export default HistoryKeywordsGetLossesResponse ``` ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data */ type: string; message: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > **`418`** zwracany jest również dla błędów walidacji. Brak wymaganego pola → `invalid_data`; nieznane `country_id` → `Unknown country_id`; błędny `order.prop` → `This value is not allow. Please use correct colum name`. > > **Znany błąd — odwrócony komunikat.** Przy `date_min > date_max` komunikat reguły `DateRangeRules` jest odwrócony i brzmi `"date_max must be less or equal than date_min"`. Należy go odczytywać jako niepowodzenie reguły zakresu dat, a nie jako dosłowną instrukcję. ## Powiązane akcje - [`getData`](/modules/visibility_analysis/va-history-keywords) — frazy w zakresie dat (`MODE_DATA`) - [`getWins`](/modules/visibility_analysis/va-history-keywords-getWins) — frazy, które poprawiły pozycje (taka sama struktura żądania) - `getLosses` — frazy, które straciły pozycje (ta strona) - `getAcquired` / `getLost` — frazy nowo pozyskane / całkowicie utracone w zakresie (taka sama struktura żądania) - `getDates` — dostępne daty (POST, wymaga tylko `country_id`) --- # Historia fraz: pozyskane (`getAcquired`) **`POST /api/visibility_analysis/reports/history/keywords/getAcquired`** Zwraca frazy **pozyskane** w zadanym zakresie dat: na `date_min` domena nie rankowała w TOP50, a na `date_max` już rankuje (tryb `MODE_GAIN` tego samego komponentu danych co `getData`). W zwracanych wierszach `statistics.position.previous` ma zawsze wartość sentinela `51` (poza TOP50), `statistics.url.previous` jest puste, `statistics.url.is_change` = `1`, a `statistics.visibility.percent` = `1` (100% wzrostu z zera). Struktura żądania jest identyczna jak w `getData`. > **Ostrzeżenie:** > **Te wiersze pojawiają się również w [`getWins`](/modules/visibility_analysis/va-history-keywords-getWins).** Fraza pozyskana ma `position.previous = 51`, więc jej `diff` jest ujemny i spełnia także warunek wzrostu. `getAcquired` jest podzbiorem `getWins` — **sklejanie obu list zdubluje wiersze**. > > Uwaga: w drugą stronę jest inaczej. [`getLost`](/modules/visibility_analysis/va-history-keywords-getLost) i [`getLosses`](/modules/visibility_analysis/va-history-keywords-getLosses) są rozłączne i tam sklejenie jest poprawne. Ta asymetria może się w przyszłości ujednolicić. | Fraza | ID frazy | KID | Domena | Liczba słów | Pozycja | Pozycja poprz. | Zmiana pozycji | Wzrosty | Spadki | Bez zmian | Widoczność | Widoczność poprz. | Δ widoczności | Widoczność % | URL bieżący | URL poprzedni | URL zmiana | CPC | Wyszukiwania/mies. | Trend (12 mies.) | Szczyt trendu | Trudność | Snippety SERP | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | plecak nike | 12682481 | abe87dec4fc0e9b7386f2718c0092aa2 | zalando.pl | 2 | 5 | 51 | -46 | 0 | 1 | 0 | 2489.85 | 0 | 2489.85 | 1 | zalando.pl/akcesoria-plecaki/nike/ | | 1 | 0.53 | 49500 | [27100,33100,90500,201000,49500,22200,27100,27100,22200,22200,27100,22200] | | 60 | ["image_thumbs"] | | air force 1 mid | 16378096 | dde117ca968b0e7a3cfc124ca6660b30 | zalando.pl | 4 | 2 | 51 | -49 | 0 | 0 | 0 | 946.08 | 0 | 946.08 | 1 | zalando.pl/obuwie/?q=air+force+1+mid | | 1 | 0.8 | 5400 | [5400,3600,4400,2900,2400,1900,2400,5400,8100,9900,9900,8100] | | 46 | ["image_thumbs"] | _zalando.pl, sort: widoczność malejąco — frazy pozyskane w zakresie. Poprzednia pozycja `51` to sentinel „poza TOP50”. Wszystkie pola wiersza (poza mapą historii `statistics.position.history`, która ma zmienne klucze-daty — jest w JSON i sekcji „Struktura odpowiedzi”)._ --- ## Żądanie `POST` `/api/visibility_analysis/reports/history/keywords/getAcquired` Nagłówki: `Authorization: Bearer `, `Content-Type: application/json`. ### Struktura żądania **Podstawowy** ```jsonc filename="żądanie-podstawowe.jsonc" { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "date_min": "2026-06-20", "date_max": "2026-06-29", "country_id": 1, "order": { "prop": "statistics.visibility.current", "dir": "desc" } } ``` **Rozszerzony** ```jsonc filename="żądanie-rozszerzone.jsonc" { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "date_min": "2026-06-20", "date_max": "2026-06-29", "country_id": 1, "order": { "prop": "statistics.visibility.current", "dir": "desc" }, "limit": 2, "page": 1 } ``` **cURL** ```bash curl --location --request POST 'https://api.senuto.com/api/visibility_analysis/reports/history/keywords/getAcquired' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' \ --data-raw '{ "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "date_min": "2026-06-20", "date_max": "2026-06-29", "country_id": 1, "order": { "prop": "statistics.visibility.current", "dir": "desc" } }' ``` ### Parametry ```ts type HistoryKeywordsGetAcquiredRequest = { /** * **Wymagane**. Domena, subdomena, katalog lub URL do analizy — interpretowane zgodnie z `fetch_mode`. */ domain: string; /** * **Wymagane**. Sposób interpretacji `domain`. * - `topLevelDomain` — cała domena (najczęstszy przypadek; wartość "domain" nie istnieje) * - `subdomain` — pojedyncza subdomena * - `catalog` — ścieżka/katalog * - `url` — dokładny URL */ fetch_mode: 'topLevelDomain' | 'subdomain' | 'catalog' | 'url'; /** * **Wymagane**. Początek zakresu dat, `YYYY-MM-DD`. Na tę datę domena **nie** rankowała w TOP50 na zwracane frazy. * Dostępne daty pobierzesz akcją `getDates`. */ date_min: string; /** * **Wymagane**. Koniec zakresu dat, `YYYY-MM-DD`. Na tę datę domena rankuje na zwracane frazy. */ date_max: string; /** * **Wymagane**. Id kraju (bazy danych). Polska = `1`. Nieznana wartość → `418` "Unknown country_id". */ country_id: number; /** * **Wymagane**. **Pojedyncza** dyrektywa sortowania (jeden obiekt — nie tablica). * `prop` to jedna z dozwolonych właściwości sortowalnych; `dir` to kierunek. * * Dozwolone wartości `prop`: * - `keyword` * - `statistics.position.current` * - `statistics.position.previous` * - `statistics.position.diff` * - `statistics.visibility.current` * - `statistics.visibility.previous` * - `statistics.visibility.diff` * - `statistics.difficulty.current` * - `statistics.searches.current` * - `statistics.cpc.current` * - `statistics.url.is_change` * * Błędny `prop` → `418` `invalid_data` z komunikatem "This value is not allow. Please use correct colum name". */ order: { prop: | 'keyword' | 'statistics.position.current' | 'statistics.position.previous' | 'statistics.position.diff' | 'statistics.visibility.current' | 'statistics.visibility.previous' | 'statistics.visibility.diff' | 'statistics.difficulty.current' | 'statistics.searches.current' | 'statistics.cpc.current' | 'statistics.url.is_change'; dir: 'asc' | 'desc'; }; /** * Liczba wierszy na stronę. * @default 10 */ limit?: number; /** * Numer strony. * @default 1 */ page?: number; /** * Tablica grup filtrów — ten sam mechanizm i te same klucze co w `positions/getData` * oraz `history/keywords/getData`. Szczegóły: [typy filtrów](/types/filter). */ filtering?: FilterGroup[]; } export default HistoryKeywordsGetAcquiredRequest ``` > **Ostrzeżenie:** > **`order` to pojedynczy obiekt** (`{ prop, dir }`) — nie tablica. Używaj ścieżek `prop` z kropkami wymienionych powyżej; inne wartości zwracają `418` z komunikatem `"This value is not allow. Please use correct colum name"`. > **Ostrzeżenie:** > Pięć parametrów jest **wymaganych**: **`domain`**, **`fetch_mode`**, **`date_min`**, **`date_max`** oraz **`country_id`**, a także pojedynczy obiekt **`order`** (`{ prop, dir }` — nie tablica). Wartość `fetch_mode` = `"domain"` **nie istnieje** — używaj `topLevelDomain`. Nieznane `country_id` zwraca `418` z komunikatem `"Unknown country_id"`. Pozycja `51` to sentinel oznaczający „poza TOP50" — nie rzeczywistą pozycję w SERP. ## Odpowiedź W przypadku powodzenia otrzymujesz `data` (tablicę wierszy z frazami pozyskanymi) oraz `pagination`. Charakterystyka trybu `MODE_GAIN`: `position.previous` = `51` (sentinel „poza TOP50"), `url.previous` = `""`, `url.is_change` = `1` (liczbowo), `visibility.previous` = `0`, `visibility.percent` = `1`. **Skrócona** ```json filename="przykładowa-odpowiedź (skrócona)" { "success": true, "data": [ { "keyword_id": 12682481, "keyword": "plecak nike", "statistics": { "position": { "current": 5, "previous": 51 }, "visibility": { "current": 2489.85, "previous": 0, "percent": 1 } /* … */ } } ], "pagination": { "page_count": 1812, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 3623, "limit": 2 } } ``` **Pełna** ```json filename="przykładowa-odpowiedź (200)" { "success": true, "data": [ { "keyword_id": 12682481, "kid": "abe87dec4fc0e9b7386f2718c0092aa2", "domain": "zalando.pl", "keyword": "plecak nike", "words_count": 2, "statistics": { "position": { "current": 5, "previous": 51, "diff": -46, "changes": { "wins": 0, "losses": 1, "no_changes": 0 }, "history": { "2026-05-25": { "position": 0, "has_serp": true }, "2026-06-26": { "position": 5, "has_serp": true } } }, "visibility": { "current": 2489.85, "previous": 0, "diff": 2489.85, "percent": 1, "history": null }, "url": { "current": "zalando.pl/akcesoria-plecaki/nike/", "previous": "", "is_change": 1 }, "cpc": { "current": 0.53 }, "searches": { "current": 49500 }, "trends": { "history": [27100, 33100, 90500, 201000, 49500, 22200, 27100, 27100, 22200, 22200, 27100, 22200], "peak": null }, "difficulty": { "current": 60 }, "snippets": { "current": ["image_thumbs"] } } }, { "keyword_id": 16378096, "kid": "dde117ca968b0e7a3cfc124ca6660b30", "domain": "zalando.pl", "keyword": "air force 1 mid", "words_count": 4, "statistics": { "position": { "current": 2, "previous": 51, "diff": -49, "changes": { "wins": 0, "losses": 0, "no_changes": 0 }, "history": { "2026-06-26": { "position": 2, "has_serp": true } } }, "visibility": { "current": 946.08, "previous": 0, "diff": 946.08, "percent": 1, "history": null }, "url": { "current": "zalando.pl/obuwie/?q=air+force+1+mid", "previous": "", "is_change": 1 }, "cpc": { "current": 0.8 }, "searches": { "current": 5400 }, "trends": { "history": [5400, 3600, 4400, 2900, 2400, 1900, 2400, 5400, 8100, 9900, 9900, 8100], "peak": null }, "difficulty": { "current": 46 }, "snippets": { "current": ["image_thumbs"] } } } ], "pagination": { "page_count": 1812, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 3623, "limit": 2 } } ``` ### Struktura odpowiedzi ```ts type HistoryKeywordsGetAcquiredResponse = { /** `true` przy powodzeniu; przy błędzie `false` i koperta z `error` */ success: boolean; /** Zwrócone wiersze z frazami pozyskanymi */ data: AcquiredKeywordRow[]; /** Metadane paginacji */ pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; limit: number; }; } type AcquiredKeywordRow = { keyword_id: number; kid: string; domain: string; keyword: string; words_count: number; statistics: { /** `previous` = 51 — sentinel "poza TOP50" (fraza pozyskana) */ position: { current: number; previous: 51; diff: number; changes: { wins: number; losses: number; no_changes: number }; history: Record }; /** `previous` = 0, `percent` = 1 (100% wzrostu z zera) */ visibility: { current: number; previous: 0; diff: number; percent: 1; history: null }; /** `previous` puste, `is_change` = 1 (liczbowo 0/1) */ url: { current: string; previous: ''; is_change: 1 }; cpc: { current: number }; searches: { current: number }; trends: { history: number[]; peak: number | null }; difficulty: { current: number }; snippets: { current: string[] }; }; } export default HistoryKeywordsGetAcquiredResponse ``` ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data */ type: string; message: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > **`418`** zwracany jest dla błędów walidacji. Brak wymaganego pola → `invalid_data`; nieznane `country_id` → `"Unknown country_id"`; błędny `order.prop` → `"This value is not allow. Please use correct colum name"`. > > **Znany błąd — odwrócony komunikat.** Przy `date_min > date_max` komunikat reguły `DateRangeRules` brzmi `"date_max must be less or equal than date_min"` — treść jest odwrócona; należy go odczytywać jako niepowodzenie reguły zakresu dat, a nie jako dosłowną instrukcję. ## Powiązane akcje - [`getData`](/modules/visibility_analysis/va-history-keywords) — frazy w zakresie dat (`MODE_DATA`) - `getWins` / `getLosses` — frazy, które zyskały / straciły pozycje w danym zakresie (taka sama struktura żądania) - `getAcquired` — frazy nowo pozyskane w zakresie (ta strona) - [`getLost`](/modules/visibility_analysis/va-history-keywords-getLost) — frazy całkowicie utracone w zakresie (taka sama struktura żądania) - `getDates` — dostępne daty dla zakresu (POST, wymaga tylko `country_id`) --- # Historia fraz: utracone (`getLost`) **`POST /api/visibility_analysis/reports/history/keywords/getLost`** Zwraca frazy **utracone** w zadanym zakresie dat: na `date_min` domena rankowała w TOP50, a na `date_max` już nie rankuje (tryb `MODE_LOSE` tego samego komponentu danych co `getData`). W zwracanych wierszach `statistics.position.current` ma zawsze wartość sentinela `51` (poza TOP50), `statistics.url.current` jest puste, `statistics.url.is_change` = `1`, a `statistics.visibility` ma `current`, `diff` i `percent` równe `0`. Struktura żądania jest identyczna jak w `getData`. > **Ostrzeżenie:** > **Tych wierszy nie ma w [`getLosses`](/modules/visibility_analysis/va-history-keywords-getLosses).** Mimo że fraza utracona formalnie „spadła", jest z tamtej akcji wykluczona — `getLosses` pokazuje wyłącznie ruch wewnątrz TOP50. Zbiory są rozłączne, więc **pełny obraz strat uzyskasz, sklejając `getLosses` + `getLost`**, bez ryzyka duplikatów. > > Uwaga: w drugą stronę jest inaczej. [`getAcquired`](/modules/visibility_analysis/va-history-keywords-getAcquired) jest podzbiorem [`getWins`](/modules/visibility_analysis/va-history-keywords-getWins) i tam sklejenie zdubluje wiersze. Ta asymetria może się w przyszłości ujednolicić. | Fraza | ID frazy | KID | Domena | Liczba słów | Pozycja | Pozycja poprz. | Zmiana pozycji | Wzrosty | Spadki | Bez zmian | Widoczność | Widoczność poprz. | Δ widoczności | Widoczność % | URL bieżący | URL poprzedni | URL zmiana | CPC | Wyszukiwania/mies. | Trend (12 mies.) | Szczyt trendu | Trudność | Snippety SERP | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | borussia moenchengladbach logo | 11657796 | 9e1033c838a972be0305152154779f8b | zalando.pl | 3 | 51 | 31 | 20 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | | zalando.pl/puma-borussia-moenchengladbach-auswaert-t-shirt-z-nadrukiem-green-warm-white-pu126g03c-m11.html | 1 | 0 | 10 | [10,10,10,10,10,0,10,10,10,10,10,10] | | 27 | ["ai_overview","image_thumbs","people_also_ask","spell"] | | anglomania facebook | 11647848 | 9dedff1fff760c052111f045eb9fba0c | zalando.pl | 2 | 51 | 30 | 21 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | | zalando.pl/vivienne-westwood-anglomania-sweter-blue-zir02g5tv-001.html | 1 | 0 | 10 | [0,10,0,0,0,0,0,0,0,10,0,0] | | 29 | ["image_thumbs"] | _zalando.pl, sort: widoczność malejąco — frazy utracone w zakresie. Bieżąca pozycja `51` to sentinel „poza TOP50”, a poprzedni URL pochodzi z ostatniej daty, na którą domena rankowała. Wszystkie pola wiersza (poza mapą historii `statistics.position.history`, która ma zmienne klucze-daty — jest w JSON i sekcji „Struktura odpowiedzi”)._ --- ## Żądanie `POST` `/api/visibility_analysis/reports/history/keywords/getLost` Nagłówki: `Authorization: Bearer `, `Content-Type: application/json`. ### Struktura żądania **Podstawowy** ```jsonc filename="żądanie-podstawowe.jsonc" { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "date_min": "2026-06-20", "date_max": "2026-06-29", "country_id": 1, "order": { "prop": "statistics.visibility.current", "dir": "desc" } } ``` **Rozszerzony** ```jsonc filename="żądanie-rozszerzone.jsonc" { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "date_min": "2026-06-20", "date_max": "2026-06-29", "country_id": 1, "order": { "prop": "statistics.visibility.current", "dir": "desc" }, "limit": 2, "page": 1 } ``` **cURL** ```bash curl --location --request POST 'https://api.senuto.com/api/visibility_analysis/reports/history/keywords/getLost' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' \ --data-raw '{ "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "date_min": "2026-06-20", "date_max": "2026-06-29", "country_id": 1, "order": { "prop": "statistics.visibility.current", "dir": "desc" } }' ``` ### Parametry ```ts type HistoryKeywordsGetLostRequest = { /** * **Wymagane**. Domena, subdomena, katalog lub URL do analizy — interpretowane zgodnie z `fetch_mode`. */ domain: string; /** * **Wymagane**. Sposób interpretacji `domain`. * - `topLevelDomain` — cała domena (najczęstszy przypadek; wartość "domain" nie istnieje) * - `subdomain` — pojedyncza subdomena * - `catalog` — ścieżka/katalog * - `url` — dokładny URL */ fetch_mode: 'topLevelDomain' | 'subdomain' | 'catalog' | 'url'; /** * **Wymagane**. Początek zakresu dat, `YYYY-MM-DD`. Na tę datę domena rankowała w TOP50 na zwracane frazy. * Dostępne daty pobierzesz akcją `getDates`. */ date_min: string; /** * **Wymagane**. Koniec zakresu dat, `YYYY-MM-DD`. Na tę datę domena już **nie** rankuje na zwracane frazy. */ date_max: string; /** * **Wymagane**. Id kraju (bazy danych). Polska = `1`. Nieznana wartość → `418` "Unknown country_id". */ country_id: number; /** * **Wymagane**. **Pojedyncza** dyrektywa sortowania (jeden obiekt — nie tablica). * `prop` to jedna z dozwolonych właściwości sortowalnych; `dir` to kierunek. * * Dozwolone wartości `prop`: * - `keyword` * - `statistics.position.current` * - `statistics.position.previous` * - `statistics.position.diff` * - `statistics.visibility.current` * - `statistics.visibility.previous` * - `statistics.visibility.diff` * - `statistics.difficulty.current` * - `statistics.searches.current` * - `statistics.cpc.current` * - `statistics.url.is_change` * * Błędny `prop` → `418` `invalid_data` z komunikatem "This value is not allow. Please use correct colum name". */ order: { prop: | 'keyword' | 'statistics.position.current' | 'statistics.position.previous' | 'statistics.position.diff' | 'statistics.visibility.current' | 'statistics.visibility.previous' | 'statistics.visibility.diff' | 'statistics.difficulty.current' | 'statistics.searches.current' | 'statistics.cpc.current' | 'statistics.url.is_change'; dir: 'asc' | 'desc'; }; /** * Liczba wierszy na stronę. * @default 10 */ limit?: number; /** * Numer strony. * @default 1 */ page?: number; /** * Tablica grup filtrów — ten sam mechanizm i te same klucze co w `positions/getData` * oraz `history/keywords/getData`. Szczegóły: [typy filtrów](/types/filter). */ filtering?: FilterGroup[]; } export default HistoryKeywordsGetLostRequest ``` > **Ostrzeżenie:** > **`order` to pojedynczy obiekt** (`{ prop, dir }`) — nie tablica. Używaj ścieżek `prop` z kropkami wymienionych powyżej; inne wartości zwracają `418` z komunikatem `"This value is not allow. Please use correct colum name"`. > **Ostrzeżenie:** > Pięć parametrów jest **wymaganych**: **`domain`**, **`fetch_mode`**, **`date_min`**, **`date_max`** oraz **`country_id`**, a także pojedynczy obiekt **`order`** (`{ prop, dir }` — nie tablica). Wartość `fetch_mode` = `"domain"` **nie istnieje** — używaj `topLevelDomain`. Nieznane `country_id` zwraca `418` z komunikatem `"Unknown country_id"`. Pozycja `51` to sentinel oznaczający „poza TOP50" — nie rzeczywistą pozycję w SERP. ## Odpowiedź W przypadku powodzenia otrzymujesz `data` (tablicę wierszy z frazami utraconymi) oraz `pagination`. Charakterystyka trybu `MODE_LOSE`: `position.current` = `51` (sentinel „poza TOP50"), `url.current` = `""`, `url.is_change` = `1` (liczbowo), a `visibility.current`, `visibility.diff` i `visibility.percent` = `0`. **Skrócona** ```json filename="przykładowa-odpowiedź (skrócona)" { "success": true, "data": [ { "keyword_id": 11657796, "keyword": "borussia moenchengladbach logo", "statistics": { "position": { "current": 51, "previous": 31 }, "visibility": { "current": 0, "percent": 0 } /* … */ } } ], "pagination": { "page_count": 1527, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 3054, "limit": 2 } } ``` **Pełna** ```json filename="przykładowa-odpowiedź (200)" { "success": true, "data": [ { "keyword_id": 11657796, "kid": "9e1033c838a972be0305152154779f8b", "domain": "zalando.pl", "keyword": "borussia moenchengladbach logo", "words_count": 3, "statistics": { "position": { "current": 51, "previous": 31, "diff": 20, "changes": { "wins": 0, "losses": 0, "no_changes": 0 }, "history": { "2026-06-21": { "position": 0, "has_serp": true } } }, "visibility": { "current": 0, "previous": 0, "diff": 0, "percent": 0, "history": null }, "url": { "current": "", "previous": "zalando.pl/puma-borussia-moenchengladbach-auswaert-t-shirt-z-nadrukiem-green-warm-white-pu126g03c-m11.html", "is_change": 1 }, "cpc": { "current": 0 }, "searches": { "current": 10 }, "trends": { "history": [10, 10, 10, 10, 10, 0, 10, 10, 10, 10, 10, 10], "peak": null }, "difficulty": { "current": 27 }, "snippets": { "current": ["ai_overview", "image_thumbs", "people_also_ask", "spell"] } } }, { "keyword_id": 11647848, "kid": "9dedff1fff760c052111f045eb9fba0c", "domain": "zalando.pl", "keyword": "anglomania facebook", "words_count": 2, "statistics": { "position": { "current": 51, "previous": 30, "diff": 21, "changes": { "wins": 0, "losses": 0, "no_changes": 0 }, "history": { "2026-06-21": { "position": 0, "has_serp": true } } }, "visibility": { "current": 0, "previous": 0, "diff": 0, "percent": 0, "history": null }, "url": { "current": "", "previous": "zalando.pl/vivienne-westwood-anglomania-sweter-blue-zir02g5tv-001.html", "is_change": 1 }, "cpc": { "current": 0 }, "searches": { "current": 10 }, "trends": { "history": [0, 10, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0], "peak": null }, "difficulty": { "current": 29 }, "snippets": { "current": ["image_thumbs"] } } } ], "pagination": { "page_count": 1527, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 3054, "limit": 2 } } ``` ### Struktura odpowiedzi ```ts type HistoryKeywordsGetLostResponse = { /** `true` przy powodzeniu; przy błędzie `false` i koperta z `error` */ success: boolean; /** Zwrócone wiersze z frazami utraconymi */ data: LostKeywordRow[]; /** Metadane paginacji */ pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; limit: number; }; } type LostKeywordRow = { keyword_id: number; kid: string; domain: string; keyword: string; words_count: number; statistics: { /** `current` = 51 — sentinel "poza TOP50" (fraza utracona) */ position: { current: 51; previous: number; diff: number; changes: { wins: number; losses: number; no_changes: number }; history: Record }; /** `current`, `diff` i `percent` = 0 (fraza poza TOP50 nie ma widoczności) */ visibility: { current: 0; previous: number; diff: 0; percent: 0; history: null }; /** `current` puste, `is_change` = 1 (liczbowo 0/1) */ url: { current: ''; previous: string; is_change: 1 }; cpc: { current: number }; searches: { current: number }; trends: { history: number[]; peak: number | null }; difficulty: { current: number }; snippets: { current: string[] }; }; } export default HistoryKeywordsGetLostResponse ``` ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data */ type: string; message: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > **`418`** zwracany jest dla błędów walidacji. Brak wymaganego pola → `invalid_data`; nieznane `country_id` → `"Unknown country_id"`; błędny `order.prop` → `"This value is not allow. Please use correct colum name"`. > > **Znany błąd — odwrócony komunikat.** Przy `date_min > date_max` komunikat reguły `DateRangeRules` brzmi `"date_max must be less or equal than date_min"` — treść jest odwrócona; należy go odczytywać jako niepowodzenie reguły zakresu dat, a nie jako dosłowną instrukcję. ## Powiązane akcje - [`getData`](/modules/visibility_analysis/va-history-keywords) — frazy w zakresie dat (`MODE_DATA`) - `getWins` / `getLosses` — frazy, które zyskały / straciły pozycje w danym zakresie (taka sama struktura żądania) - [`getAcquired`](/modules/visibility_analysis/va-history-keywords-getAcquired) — frazy nowo pozyskane w zakresie (taka sama struktura żądania) - `getLost` — frazy całkowicie utracone w zakresie (ta strona) - `getDates` — dostępne daty dla zakresu (POST, wymaga tylko `country_id`) --- # Historia fraz: dostępne daty (`getDates`) **`POST /api/visibility_analysis/reports/history/keywords/getDates`** Zwraca listę dostępnych punktów czasowych (dat) dla raportów historii fraz w danym kraju. Wywołaj tę akcję **przed** raportami historii (`getData`, `getWins`, `getLosses`, `getAcquired`, `getLost`) — wartości `value` z odpowiedzi nadają się wprost do pól `date_min` / `date_max`. Lista dat jest wspólna dla całej bazy danego kraju, dlatego żądanie nie wymaga `domain` ani `fetch_mode`. --- ## Żądanie `POST` `/api/visibility_analysis/reports/history/keywords/getDates` Nagłówki: `Authorization: Bearer `, `Content-Type: application/json`. ### Struktura żądania **Podstawowy** ```jsonc filename="żądanie-podstawowe.jsonc" { "country_id": 1 } ``` **cURL** ```bash curl --location --request POST 'https://api.senuto.com/api/visibility_analysis/reports/history/keywords/getDates' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' \ --data-raw '{ "country_id": 1 }' ``` ### Parametry ```ts type HistoryKeywordsGetDatesRequest = { /** * **Wymagane**. Id kraju (bazy danych). Polska = `1`. * Liczba całkowita większa od zera; musi istnieć w bazie krajów — * nieznana wartość zwraca `418` z komunikatem `"Unknown country_id"`. */ country_id: number; } export default HistoryKeywordsGetDatesRequest ``` > **Ostrzeżenie:** > Wymagane jest **tylko `country_id`** (walidator `GetDatesValidator` używa wyłącznie `CountryRules`) — w odróżnieniu od pozostałych akcji tego kontrolera **nie podawaj** `domain` ani `fetch_mode`. `country_id` musi być liczbą całkowitą większą od zera i istnieć w bazie krajów; nieznana wartość zwraca `418` z komunikatem `"Unknown country_id"`. Uwaga na granulację listy: historia jest **miesięczna** (pierwszy dzień miesiąca), a tylko ostatni tydzień ma punkty **dzienne**. ## Odpowiedź W przypadku powodzenia `data` jest **obiektem** (nie tablicą) zawierającym listę dostępnych dat (`data.data`), sugerowane zakresy (`data.config`) oraz pola `default_min_date` / `default_max_date`. Lista `data.data` miała **78 pozycji**: zaczyna się od punktów miesięcznych (od `"2020-01-01"` / „Styczeń 2020”), a kończy punktami dziennymi z ostatniego tygodnia (np. `"2026-07-01"` / „Wczoraj”). Etykiety `label` i `label_short` są po polsku. ### Struktura odpowiedzi ```ts type HistoryKeywordsGetDatesResponse = { /** `true` przy powodzeniu; przy błędzie `false` i koperta z `error` */ success: boolean; data: { /** * Dostępne punkty czasowe — granulacja miesięczna dla historii * oraz dzienna dla ostatnich dni. W zwalidowanej odpowiedzi: 78 pozycji. */ data: DateEntry[]; /** Sugerowane zakresy dat do użycia w raportach historii */ config: { /** Domyślny zakres (w zwalidowanej odpowiedzi: ostatnie 7 dni) */ default: { date_min: DateEntry; date_max: DateEntry }; /** Najświeższy zakres (w zwalidowanej odpowiedzi: ostatnie 2 dni) */ recent: { date_min: DateEntry; date_max: DateEntry }; }; /** W tym raporcie zawsze null */ default_min_date: string | null; /** W tym raporcie zawsze null */ default_max_date: string | null; }; } type DateEntry = { /** Data `YYYY-MM-DD` — nadaje się wprost do `date_min` / `date_max` raportów historii */ value: string; /** Etykieta po polsku, np. "Styczeń 2020" lub "Wczoraj" */ label: string; /** Skrócona etykieta po polsku */ label_short: string; } export default HistoryKeywordsGetDatesResponse ``` ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data, database, timeout, unknown */ type: string; message: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > **`418`** zwracany jest również dla błędów walidacji — nie tylko przy ograniczeniu liczby żądań (rate limiting). Brak wymaganego pola → > `{"success":false,"data":{"error":{"type":"invalid_data","params":{"country_id":{"_required":"This field is required"}}}}}`. > Nieznany `country_id` → `418` z komunikatem `"Unknown country_id"`. ## Powiązane akcje - [`getData`](/modules/visibility_analysis/va-history-keywords) — frazy w zakresie dat - `getWins` / `getLosses` — frazy, które zyskały / straciły pozycje w danym zakresie - `getAcquired` / `getLost` — frazy nowo pozyskane / całkowicie utracone w zakresie - `getDates` — dostępne daty dla zakresu (ta strona) --- # Historia URL-i: przegląd (`getData`) **`POST /api/visibility_analysis/reports/history/urls/getData`** Zwraca pełną listę adresów URL domeny wraz z porównaniem statystyk widoczności między `date_min` a `date_max`. W odróżnieniu od raportu historii fraz, ten raport agreguje dane **po adresach URL** — dla każdego adresu otrzymujesz liczbę fraz (`keywords_count`) oraz zestaw statystyk `{current, previous, diff, percent}`: liczbę fraz w TOP3/TOP10/TOP50, szacowany ruch (`visibility`), średnią pozycję (`position`), sumę pozycji (`summary_position`) oraz liczbę fraz, które zyskały (`wins`) i straciły (`losses`). Wyniki są sortowane według pojedynczej dyrektywy sortowania. | URL | Frazy | TOP3 | TOP3 poprz. | TOP3 Δ | TOP3 % | TOP10 | TOP10 poprz. | TOP10 Δ | TOP10 % | TOP50 | TOP50 poprz. | TOP50 Δ | TOP50 % | Widoczność | Widoczność poprz. | Widoczność Δ | Widoczność % | Śr. pozycja | Śr. pozycja poprz. | Śr. pozycja Δ | Suma pozycji | Suma pozycji poprz. | Suma pozycji Δ | Suma pozycji % | Wzrosty (fraz) | Spadki (fraz) | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | zalando.pl/ | 3084 | 251 | 116 | 135 | 1.1638 | 104 | 108 | -4 | -0.037 | 2729 | 2860 | -131 | -0.0458 | 963643.78 | 963614.83 | 28.95 | 0 | 30 | 32 | -2 | 94624 | 99195 | -4571 | -0.0461 | 156 | 12 | | zalando.pl/bershka/ | 149 | 29 | 27 | 2 | 0.0741 | 40 | 44 | -4 | -0.0909 | 80 | 78 | 2 | 0.0256 | 311727.2 | 311733.03 | -5.83 | 0 | 15 | 15 | 0 | 2326 | 2342 | -16 | -0.0068 | 5 | 5 | _zalando.pl (2026-06-20 → 2026-06-29). Wszystkie pola wiersza (poza mapami o kluczach-datach — są w JSON). Uwaga: keywords_count, wins i losses przychodzą jako stringi._ --- ## Żądanie `POST` `/api/visibility_analysis/reports/history/urls/getData` Nagłówki: `Authorization: Bearer `, `Content-Type: application/json`. ### Struktura żądania **Podstawowy** ```jsonc filename="żądanie-podstawowe.jsonc" { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "country_id": 1, "date_min": "2026-06-20", "date_max": "2026-06-29", "order": { "prop": "statistics.visibility.current", "dir": "desc" } } ``` **Rozszerzony** ```jsonc filename="żądanie-rozszerzone.jsonc" { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "country_id": 1, "date_min": "2026-06-20", "date_max": "2026-06-29", "order": { "prop": "statistics.visibility.current", "dir": "desc" }, "limit": 2, "page": 1 } ``` **cURL** ```bash curl --location --request POST 'https://api.senuto.com/api/visibility_analysis/reports/history/urls/getData' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' \ --data-raw '{ "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "country_id": 1, "date_min": "2026-06-20", "date_max": "2026-06-29", "order": { "prop": "statistics.visibility.current", "dir": "desc" } }' ``` ### Parametry ```ts type HistoryUrlsGetDataRequest = { /** * **Wymagane**. Domena, subdomena, katalog lub URL do analizy — interpretowane zgodnie z `fetch_mode`. */ domain: string; /** * **Wymagane**. Sposób interpretacji `domain`. * - `topLevelDomain` — cała domena (najczęstszy przypadek; wartość "domain" nie istnieje) * - `subdomain` — pojedyncza subdomena * - `catalog` — ścieżka/katalog * - `url` — dokładny URL */ fetch_mode: 'topLevelDomain' | 'subdomain' | 'catalog' | 'url'; /** * **Wymagane**. Id kraju (bazy danych). Polska = `1`. Nieznana wartość → `418` z komunikatem `Unknown country_id`. */ country_id: number; /** * **Wymagane**. Początek zakresu dat, `YYYY-MM-DD`. Nie może być późniejszy niż `date_max`. */ date_min: string; /** * **Wymagane**. Koniec zakresu dat, `YYYY-MM-DD`. Nie może być wcześniejszy niż `date_min`. */ date_max: string; /** * **Wymagane**. **Pojedyncza** dyrektywa sortowania (jeden obiekt — nie tablica). * * Dozwolone wartości `prop` (tylko 4 — inaczej niż w raporcie historii fraz): * - `statistics.visibility.current` * - `statistics.visibility.previous` * - `statistics.visibility.diff` * - `statistics.visibility.percent` * * Błędny `prop` → `418` `invalid_data` z komunikatem "This value is not allow. Please use correct colum name". */ order: { prop: | 'statistics.visibility.current' | 'statistics.visibility.previous' | 'statistics.visibility.diff' | 'statistics.visibility.percent'; dir: 'asc' | 'desc'; }; /** * Liczba wierszy na stronę. * @default 10 */ limit?: number; /** * Numer strony. * @default 1 */ page?: number; } export default HistoryUrlsGetDataRequest ``` > **Ostrzeżenie:** > **`order` to pojedynczy obiekt** (`{ prop, dir }`) — nie tablica — i akceptuje wyłącznie 4 właściwości z gałęzi `statistics.visibility.*` wymienione powyżej. Kontroler przyjmuje też opcjonalny parametr `filtering`, jednak zestaw dozwolonych kluczy filtrów dla tego raportu **nie został jeszcze zweryfikowany na żywo** — nie należy zakładać, że filtry znane z innych raportów zadziałają tutaj tak samo. > **Ostrzeżenie:** > Pięć parametrów jest **wymaganych**: **`domain`**, **`fetch_mode`**, **`country_id`**, **`date_min`**, **`date_max`**, a także pojedynczy obiekt **`order`** (`{ prop, dir }` — nie tablica). Wartość `fetch_mode: "domain"` **nie istnieje** — użyj `topLevelDomain`. Dozwolone są **tylko 4** wartości `order.prop` (wszystkie z gałęzi `statistics.visibility.*`) — to mniej niż w raporcie historii fraz. Uwaga na typy: część pól liczbowych przychodzi jako **stringi** (`keywords_count`, `statistics.wins.current`, `statistics.losses.current`). ## Odpowiedź W przypadku powodzenia otrzymujesz `data` (tablicę wierszy z adresami URL) oraz `pagination`. **Skrócona** ```json filename="przykładowa-odpowiedź (skrócona)" { "success": true, "data": [ { "url": "zalando.pl/", "keywords_count": "3084", "statistics": { "visibility": { "current": 963643.78 }, "top3": { "current": 251 } /* … */ } } ], "pagination": { "page_count": 36677, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 73353, "limit": 2 } } ``` **Pełna** ```json filename="przykładowa-odpowiedź (200)" { "success": true, "data": [ { "url": "zalando.pl/", "keywords_count": "3084", "statistics": { "top3": { "current": 251, "previous": 116, "diff": 135, "percent": 1.1638 }, "top10": { "current": 104, "previous": 108, "diff": -4, "percent": -0.037 }, "top50": { "current": 2729, "previous": 2860, "diff": -131, "percent": -0.0458 }, "visibility": { "current": 963643.78, "previous": 963614.83, "diff": 28.95, "percent": 0 }, "position": { "current": 30, "previous": 32, "diff": -2 }, "summary_position": { "current": 94624, "previous": 99195, "diff": -4571, "percent": -0.0461 }, "wins": { "current": "156" }, "losses": { "current": "12" } } }, { "url": "zalando.pl/bershka/", "keywords_count": "149", "statistics": { "top3": { "current": 29, "previous": 27, "diff": 2, "percent": 0.0741 }, "top10": { "current": 40, "previous": 44, "diff": -4, "percent": -0.0909 }, "top50": { "current": 80, "previous": 78, "diff": 2, "percent": 0.0256 }, "visibility": { "current": 311727.2, "previous": 311733.03, "diff": -5.83, "percent": 0 }, "position": { "current": 15, "previous": 15, "diff": 0 }, "summary_position": { "current": 2326, "previous": 2342, "diff": -16, "percent": -0.0068 }, "wins": { "current": "5" }, "losses": { "current": "5" } } } ], "pagination": { "page_count": 36677, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 73353, "limit": 2 } } ``` ### Struktura odpowiedzi ```ts type HistoryUrlsResponse = { /** `true` przy powodzeniu; przy błędzie `false` i koperta z `error` */ success: boolean; /** Zwrócone wiersze z adresami URL */ data: UrlRow[]; /** Metadane paginacji */ pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; limit: number; }; } type UrlRow = { /** Adres URL (bez protokołu) */ url: string; /** Liczba fraz przypisanych do URL-a — UWAGA: zwracana jako string, np. "3084" */ keywords_count: string; statistics: { /** Liczba fraz URL-a na pozycjach 1–3 */ top3: { current: number; previous: number; diff: number; percent: number }; /** Liczba fraz URL-a na pozycjach 4–10 */ top10: { current: number; previous: number; diff: number; percent: number }; /** Liczba fraz URL-a na pozycjach 11–50 */ top50: { current: number; previous: number; diff: number; percent: number }; /** Szacowany ruch (widoczność) URL-a */ visibility: { current: number; previous: number; diff: number; percent: number }; /** Średnia pozycja fraz URL-a (bez pola percent) */ position: { current: number; previous: number; diff: number }; /** Suma pozycji fraz URL-a */ summary_position: { current: number; previous: number; diff: number; percent: number }; /** Liczba fraz, które zyskały pozycje — UWAGA: string, np. "156" */ wins: { current: string }; /** Liczba fraz, które straciły pozycje — UWAGA: string, np. "12" */ losses: { current: string }; }; } export default HistoryUrlsResponse ``` ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data, database, timeout, unknown */ type: string; message: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > **`418`** zwracany jest również dla błędów walidacji — nie tylko przy ograniczeniu liczby żądań (rate limiting). Nieznane `country_id` → `418` z komunikatem `Unknown country_id`. Niedozwolony `order.prop` → `418` `invalid_data` z komunikatem `"This value is not allow. Please use correct colum name"` (pisownia oryginalna). > > **Znany błąd — odwrócony komunikat.** Przy `date_min > date_max` walidator `DateRangeRules` zwraca komunikat `"date_max must be less or equal than date_min"` — treść jest odwrócona (to `date_min` musi być nie późniejszy niż `date_max`). Należy go odczytywać jako niepowodzenie reguły zakresu dat, a nie jako dosłowną instrukcję. ## Powiązane akcje - `getData` — pełna lista URL-i w zakresie dat (ta strona) - [`getWins`](/modules/visibility_analysis/va-history-urls-getWins) — URL-e, których widoczność wzrosła w danym zakresie (taka sama struktura żądania) - `getLosses` — URL-e, których widoczność spadła w danym zakresie - `getAcquired` — URL-e nowo pozyskane w zakresie - `getLost` — URL-e całkowicie utracone w zakresie --- # Historia URL-i: wzrosty (`getWins`) **`POST /api/visibility_analysis/reports/history/urls/getWins`** Zwraca adresy URL domeny, których widoczność **wzrosła** między `date_min` a `date_max`. To wariant `MODE_INCREASE` tego samego komponentu co [pełny raport historii URL-i (`getData`)](/modules/visibility_analysis/va-history-urls-getData) — struktura żądania i wierszy odpowiedzi jest identyczna, różni się jedynie zbiór zwracanych URL-i (tylko te z rosnącą widocznością). Dla każdego adresu otrzymujesz liczbę fraz (`keywords_count`) oraz statystyki `{current, previous, diff, percent}`: liczbę fraz w TOP3/TOP10/TOP50, szacowany ruch (`visibility`), średnią pozycję (`position`), sumę pozycji (`summary_position`) oraz liczbę fraz, które zyskały (`wins`) i straciły (`losses`). | URL | Frazy | TOP3 | TOP3 poprz. | TOP3 Δ | TOP3 % | TOP10 | TOP10 poprz. | TOP10 Δ | TOP10 % | TOP50 | TOP50 poprz. | TOP50 Δ | TOP50 % | Widoczność | Widoczność poprz. | Widoczność Δ | Widoczność % | Śr. pozycja | Śr. pozycja poprz. | Śr. pozycja Δ | Suma pozycji | Suma pozycji poprz. | Suma pozycji Δ | Suma pozycji % | Wzrosty (fraz) | Spadki (fraz) | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | zalando.pl/bershka/ | 150 | 28 | 28 | 0 | 0 | 41 | 42 | -1 | -0.0238 | 81 | 80 | 1 | 0.0125 | 311733.14 | 311732.88 | 0.27 | 0 | 15 | 15 | 0 | 2345 | 2380 | -35 | -0.0147 | 6 | 5 | | zalando.pl/obuwie/ugg/ | 198 | 56 | 56 | 0 | 0 | 43 | 43 | 0 | 0 | 99 | 99 | 0 | 0 | 103112.67 | 77149.7 | 25962.97 | 0.3365 | 15 | 15 | 0 | 3120 | 3110 | 10 | 0.0032 | 5 | 1 | _zalando.pl (2026-06-20 → 2026-06-29) — URL-e ze wzrostem widoczności. Wszystkie pola wiersza (poza mapami o kluczach-datach — są w JSON). Uwaga: keywords_count, wins i losses przychodzą jako stringi._ --- ## Żądanie `POST` `/api/visibility_analysis/reports/history/urls/getWins` Nagłówki: `Authorization: Bearer `, `Content-Type: application/json`. ### Struktura żądania **Podstawowy** ```jsonc filename="żądanie-podstawowe.jsonc" { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "country_id": 1, "date_min": "2026-06-20", "date_max": "2026-06-29", "order": { "prop": "statistics.visibility.current", "dir": "desc" } } ``` **Rozszerzony** ```jsonc filename="żądanie-rozszerzone.jsonc" { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "country_id": 1, "date_min": "2026-06-20", "date_max": "2026-06-29", "order": { "prop": "statistics.visibility.current", "dir": "desc" }, "limit": 2, "page": 1 } ``` **cURL** ```bash curl --location --request POST 'https://api.senuto.com/api/visibility_analysis/reports/history/urls/getWins' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' \ --data-raw '{ "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "country_id": 1, "date_min": "2026-06-20", "date_max": "2026-06-29", "order": { "prop": "statistics.visibility.current", "dir": "desc" } }' ``` ### Parametry ```ts type HistoryUrlsGetWinsRequest = { /** * **Wymagane**. Domena, subdomena, katalog lub URL do analizy — interpretowane zgodnie z `fetch_mode`. */ domain: string; /** * **Wymagane**. Sposób interpretacji `domain`. * - `topLevelDomain` — cała domena (najczęstszy przypadek; wartość "domain" nie istnieje) * - `subdomain` — pojedyncza subdomena * - `catalog` — ścieżka/katalog * - `url` — dokładny URL */ fetch_mode: 'topLevelDomain' | 'subdomain' | 'catalog' | 'url'; /** * **Wymagane**. Id kraju (bazy danych). Polska = `1`. Nieznana wartość → `418` z komunikatem `Unknown country_id`. */ country_id: number; /** * **Wymagane**. Początek zakresu dat, `YYYY-MM-DD`. Nie może być późniejszy niż `date_max`. */ date_min: string; /** * **Wymagane**. Koniec zakresu dat, `YYYY-MM-DD`. Nie może być wcześniejszy niż `date_min`. */ date_max: string; /** * **Wymagane**. **Pojedyncza** dyrektywa sortowania (jeden obiekt — nie tablica). * * Dozwolone wartości `prop` (tylko 4 — inaczej niż w raporcie historii fraz): * - `statistics.visibility.current` * - `statistics.visibility.previous` * - `statistics.visibility.diff` * - `statistics.visibility.percent` * * Błędny `prop` → `418` `invalid_data` z komunikatem "This value is not allow. Please use correct colum name". */ order: { prop: | 'statistics.visibility.current' | 'statistics.visibility.previous' | 'statistics.visibility.diff' | 'statistics.visibility.percent'; dir: 'asc' | 'desc'; }; /** * Liczba wierszy na stronę. * @default 10 */ limit?: number; /** * Numer strony. * @default 1 */ page?: number; } export default HistoryUrlsGetWinsRequest ``` > **Ostrzeżenie:** > **`order` to pojedynczy obiekt** (`{ prop, dir }`) — nie tablica — i akceptuje wyłącznie 4 właściwości z gałęzi `statistics.visibility.*` wymienione powyżej. Kontroler przyjmuje też opcjonalny parametr `filtering`, jednak zestaw dozwolonych kluczy filtrów dla tego raportu **nie został jeszcze zweryfikowany na żywo** — nie należy zakładać, że filtry znane z innych raportów zadziałają tutaj tak samo. > **Ostrzeżenie:** > Pięć parametrów jest **wymaganych**: **`domain`**, **`fetch_mode`**, **`country_id`**, **`date_min`**, **`date_max`**, a także pojedynczy obiekt **`order`** (`{ prop, dir }` — nie tablica). Wartość `fetch_mode: "domain"` **nie istnieje** — użyj `topLevelDomain`. Dozwolone są **tylko 4** wartości `order.prop` (wszystkie z gałęzi `statistics.visibility.*`). Uwaga na typy: część pól liczbowych przychodzi jako **stringi** (`keywords_count`, `statistics.wins.current`, `statistics.losses.current`). ## Odpowiedź W przypadku powodzenia otrzymujesz `data` (tablicę wierszy z adresami URL, których widoczność wzrosła) oraz `pagination`. Dla przykładowego zapytania (`zalando.pl`, 2026-06-20 → 2026-06-29) raport zwrócił `count` = 1333 URL-i ze wzrostem — wobec 73 353 wszystkich URL-i w akcji `getData`. **Skrócona** ```json filename="przykładowa-odpowiedź (skrócona)" { "success": true, "data": [ { "url": "zalando.pl/obuwie/ugg/", "keywords_count": "198", "statistics": { "visibility": { "current": 103112.67, "diff": 25962.97 } /* … */ } } ], "pagination": { "page_count": 667, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 1333, "limit": 2 } } ``` **Pełna** ```json filename="przykładowa-odpowiedź (200)" { "success": true, "data": [ { "url": "zalando.pl/bershka/", "keywords_count": "150", "statistics": { "top3": { "current": 28, "previous": 28, "diff": 0, "percent": 0 }, "top10": { "current": 41, "previous": 42, "diff": -1, "percent": -0.0238 }, "top50": { "current": 81, "previous": 80, "diff": 1, "percent": 0.0125 }, "visibility": { "current": 311733.14, "previous": 311732.88, "diff": 0.27, "percent": 0 }, "position": { "current": 15, "previous": 15, "diff": 0 }, "summary_position": { "current": 2345, "previous": 2380, "diff": -35, "percent": -0.0147 }, "wins": { "current": "6" }, "losses": { "current": "5" } } }, { "url": "zalando.pl/obuwie/ugg/", "keywords_count": "198", "statistics": { "top3": { "current": 56, "previous": 56, "diff": 0, "percent": 0 }, "top10": { "current": 43, "previous": 43, "diff": 0, "percent": 0 }, "top50": { "current": 99, "previous": 99, "diff": 0, "percent": 0 }, "visibility": { "current": 103112.67, "previous": 77149.7, "diff": 25962.97, "percent": 0.3365 }, "position": { "current": 15, "previous": 15, "diff": 0 }, "summary_position": { "current": 3120, "previous": 3110, "diff": 10, "percent": 0.0032 }, "wins": { "current": "5" }, "losses": { "current": "1" } } } ], "pagination": { "page_count": 667, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 1333, "limit": 2 } } ``` ### Struktura odpowiedzi ```ts type HistoryUrlsWinsResponse = { /** `true` przy powodzeniu; przy błędzie `false` i koperta z `error` */ success: boolean; /** Zwrócone wiersze z adresami URL, których widoczność wzrosła */ data: UrlRow[]; /** Metadane paginacji */ pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; limit: number; }; } type UrlRow = { /** Adres URL (bez protokołu) */ url: string; /** Liczba fraz przypisanych do URL-a — UWAGA: zwracana jako string, np. "150" */ keywords_count: string; statistics: { /** Liczba fraz URL-a na pozycjach 1–3 */ top3: { current: number; previous: number; diff: number; percent: number }; /** Liczba fraz URL-a na pozycjach 4–10 */ top10: { current: number; previous: number; diff: number; percent: number }; /** Liczba fraz URL-a na pozycjach 11–50 */ top50: { current: number; previous: number; diff: number; percent: number }; /** Szacowany ruch (widoczność) URL-a */ visibility: { current: number; previous: number; diff: number; percent: number }; /** Średnia pozycja fraz URL-a (bez pola percent) */ position: { current: number; previous: number; diff: number }; /** Suma pozycji fraz URL-a */ summary_position: { current: number; previous: number; diff: number; percent: number }; /** Liczba fraz, które zyskały pozycje — UWAGA: string, np. "6" */ wins: { current: string }; /** Liczba fraz, które straciły pozycje — UWAGA: string, np. "5" */ losses: { current: string }; }; } export default HistoryUrlsWinsResponse ``` ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data, database, timeout, unknown */ type: string; message: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > **`418`** zwracany jest również dla błędów walidacji — nie tylko przy ograniczeniu liczby żądań (rate limiting). Nieznane `country_id` → `418` z komunikatem `Unknown country_id`. Niedozwolony `order.prop` → `418` `invalid_data` z komunikatem `"This value is not allow. Please use correct colum name"` (pisownia oryginalna). > > **Znany błąd — odwrócony komunikat.** Przy `date_min > date_max` walidator `DateRangeRules` zwraca komunikat `"date_max must be less or equal than date_min"` — treść jest odwrócona (to `date_min` musi być nie późniejszy niż `date_max`). Należy go odczytywać jako niepowodzenie reguły zakresu dat, a nie jako dosłowną instrukcję. ## Powiązane akcje - [`getData`](/modules/visibility_analysis/va-history-urls-getData) — pełna lista URL-i w zakresie dat (strona siostrzana) - `getWins` — URL-e, których widoczność wzrosła w danym zakresie (`MODE_INCREASE`, ta strona) - `getLosses` — URL-e, których widoczność spadła w danym zakresie - `getAcquired` — URL-e nowo pozyskane w zakresie - `getLost` — URL-e całkowicie utracone w zakresie --- # Historia URL-i: spadki (`getLosses`) **`POST /api/visibility_analysis/reports/history/urls/getLosses`** Zwraca adresy URL domeny, których widoczność **spadła** między `date_min` a `date_max` (tryb `MODE_DECREASE`). W odróżnieniu od raportu historii fraz, ten raport agreguje dane **po adresach URL**, a nie po pojedynczych frazach — każdy wiersz opisuje jeden URL wraz ze statystykami: liczbą fraz, przedziałami pozycji (`top3`/`top10`/`top50`), widocznością (szacowany ruch), średnią i sumaryczną pozycją oraz liczbą fraz, które zyskały (`wins`) i straciły (`losses`). | URL | Frazy | TOP3 | TOP3 poprz. | TOP3 Δ | TOP3 % | TOP10 | TOP10 poprz. | TOP10 Δ | TOP10 % | TOP50 | TOP50 poprz. | TOP50 Δ | TOP50 % | Widoczność | Widoczność poprz. | Widoczność Δ | Widoczność % | Śr. pozycja | Śr. pozycja poprz. | Śr. pozycja Δ | Suma pozycji | Suma pozycji poprz. | Suma pozycji Δ | Suma pozycji % | Wzrosty (fraz) | Spadki (fraz) | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | zalando.pl/bershka/ | 149 | 29 | 27 | 2 | 0.0741 | 40 | 44 | -4 | -0.0909 | 80 | 78 | 2 | 0.0256 | 311727.2 | 311733.03 | -5.83 | 0 | 15 | 15 | 0 | 2326 | 2342 | -16 | -0.0068 | 5 | 5 | | zalando.pl/stradivarius/ | 90 | 38 | 38 | 0 | 0 | 19 | 20 | -1 | -0.05 | 33 | 32 | 1 | 0.0313 | 81090.82 | 83195.79 | -2104.97 | -0.0253 | 11 | 11 | 0 | 1022 | 1018 | 4 | 0.0039 | 4 | 1 | _zalando.pl (2026-06-20 → 2026-06-29) — URL-e ze spadkiem widoczności. Wszystkie pola wiersza (poza mapami o kluczach-datach — są w JSON). Uwaga: keywords_count, wins i losses przychodzą jako stringi._ --- ## Żądanie `POST` `/api/visibility_analysis/reports/history/urls/getLosses` Nagłówki: `Authorization: Bearer `, `Content-Type: application/json`. ### Struktura żądania **Podstawowy** ```jsonc filename="żądanie-podstawowe.jsonc" { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "date_min": "2026-06-20", "date_max": "2026-06-29", "country_id": 1, "order": { "prop": "statistics.visibility.current", "dir": "desc" } } ``` **Rozszerzony** ```jsonc filename="żądanie-rozszerzone.jsonc" { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "date_min": "2026-06-20", "date_max": "2026-06-29", "country_id": 1, "order": { "prop": "statistics.visibility.current", "dir": "desc" }, "limit": 2, "page": 1 } ``` **cURL** ```bash curl --location --request POST 'https://api.senuto.com/api/visibility_analysis/reports/history/urls/getLosses' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' \ --data-raw '{ "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "date_min": "2026-06-20", "date_max": "2026-06-29", "country_id": 1, "order": { "prop": "statistics.visibility.current", "dir": "desc" } }' ``` ### Parametry ```ts type HistoryUrlsGetLossesRequest = { /** * **Wymagane**. Domena, subdomena, katalog lub URL do analizy — interpretowane zgodnie z `fetch_mode`. */ domain: string; /** * **Wymagane**. Sposób interpretacji `domain`. * - `topLevelDomain` — cała domena (najczęstszy przypadek; wartość "domain" **nie istnieje**) * - `subdomain` — pojedyncza subdomena * - `catalog` — ścieżka/katalog * - `url` — dokładny URL */ fetch_mode: 'topLevelDomain' | 'subdomain' | 'catalog' | 'url'; /** * **Wymagane**. Id kraju (bazy danych). Polska = `1`. Nieznana wartość → `418` z komunikatem "Unknown country_id". */ country_id: number; /** * **Wymagane**. Początek zakresu dat, `YYYY-MM-DD`. Musi być nie późniejszy niż `date_max`. */ date_min: string; /** * **Wymagane**. Koniec zakresu dat, `YYYY-MM-DD`. Musi być nie wcześniejszy niż `date_min`. */ date_max: string; /** * **Wymagane**. **Pojedyncza** dyrektywa sortowania (jeden obiekt — nie tablica). * * Dozwolone wartości `prop` (tylko 4 — inaczej niż w raporcie fraz): * - `statistics.visibility.current` * - `statistics.visibility.previous` * - `statistics.visibility.diff` * - `statistics.visibility.percent` * * Inna wartość → `418` `invalid_data` z komunikatem "This value is not allow. Please use correct colum name" (pisownia oryginalna). */ order: { prop: | 'statistics.visibility.current' | 'statistics.visibility.previous' | 'statistics.visibility.diff' | 'statistics.visibility.percent'; dir: 'asc' | 'desc'; }; /** * Liczba wierszy na stronę. * @default 10 */ limit?: number; /** * Numer strony. * @default 1 */ page?: number; /** * Opcjonalne filtrowanie wyników. Kontroler przyjmuje ten parametr (wg źródła), * ale zestaw dozwolonych kluczy filtrów dla raportu URL-i **nie został jeszcze * zweryfikowany na żywo** — używaj ostrożnie. */ filtering?: unknown; } export default HistoryUrlsGetLossesRequest ``` > **Ostrzeżenie:** > **`order` to pojedynczy obiekt** (`{ prop, dir }`) — nie tablica. Ten raport dopuszcza wyłącznie 4 właściwości sortowania (`statistics.visibility.*`) — próba sortowania np. po `keyword` lub `statistics.position.current` zakończy się błędem `418`. > **Ostrzeżenie:** > Wymagane są: **`domain`**, **`fetch_mode`**, **`country_id`**, **`date_min`**, **`date_max`** oraz pojedynczy obiekt **`order`** (`{ prop, dir }` — nie tablica). Dozwolone są **tylko 4** wartości `order.prop` (właściwości `statistics.visibility.*`) — mniej niż w raporcie fraz, który ma ich 11. Wartość `fetch_mode: "domain"` **nie istnieje** — użyj `topLevelDomain`. Uwaga na typy: część pól liczbowych przychodzi jako **stringi** (`keywords_count`, `statistics.wins.current`, `statistics.losses.current`). ## Odpowiedź W przypadku powodzenia otrzymujesz `data` (tablicę wierszy z URL-ami) oraz `pagination`. Dla zapytania (`zalando.pl`, 2026-06-20 → 2026-06-29) raport zwrócił `count` = 2 962 URL-i ze spadkami. **Skrócona** ```json filename="przykładowa-odpowiedź (skrócona)" { "success": true, "data": [ { "url": "zalando.pl/bershka/", "keywords_count": "149", "statistics": { "visibility": { "current": 311727.2, "previous": 311733.03, "diff": -5.83 } /* … */ } } ], "pagination": { "page_count": 1481, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 2962, "limit": 2 } } ``` **Pełna** ```json filename="przykładowa-odpowiedź (200)" { "success": true, "data": [ { "url": "zalando.pl/bershka/", "keywords_count": "149", "statistics": { "top3": { "current": 29, "previous": 27, "diff": 2, "percent": 0.0741 }, "top10": { "current": 40, "previous": 44, "diff": -4, "percent": -0.0909 }, "top50": { "current": 80, "previous": 78, "diff": 2, "percent": 0.0256 }, "visibility": { "current": 311727.2, "previous": 311733.03, "diff": -5.83, "percent": 0 }, "position": { "current": 15, "previous": 15, "diff": 0 }, "summary_position": { "current": 2326, "previous": 2342, "diff": -16, "percent": -0.0068 }, "wins": { "current": "5" }, "losses": { "current": "5" } } }, { "url": "zalando.pl/stradivarius/", "keywords_count": "90", "statistics": { "top3": { "current": 38, "previous": 38, "diff": 0, "percent": 0 }, "top10": { "current": 19, "previous": 20, "diff": -1, "percent": -0.05 }, "top50": { "current": 33, "previous": 32, "diff": 1, "percent": 0.0313 }, "visibility": { "current": 81090.82, "previous": 83195.79, "diff": -2104.97, "percent": -0.0253 }, "position": { "current": 11, "previous": 11, "diff": 0 }, "summary_position": { "current": 1022, "previous": 1018, "diff": 4, "percent": 0.0039 }, "wins": { "current": "4" }, "losses": { "current": "1" } } } ], "pagination": { "page_count": 1481, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 2962, "limit": 2 } } ``` ### Struktura odpowiedzi ```ts type HistoryUrlsGetLossesResponse = { /** `true` przy powodzeniu; przy błędzie `false` i koperta z `error` */ success: boolean; /** Zwrócone wiersze z URL-ami */ data: UrlRow[]; /** Metadane paginacji */ pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; limit: number; }; } type UrlRow = { /** Adres URL (bez protokołu) */ url: string; /** Liczba fraz rankujących na ten URL. **Uwaga: string, nie number.** */ keywords_count: string; statistics: { /** Liczba fraz URL-a w TOP 3 (`{ current, previous, diff, percent }`) */ top3: RangeStat; /** Liczba fraz URL-a w TOP 10 */ top10: RangeStat; /** Liczba fraz URL-a w TOP 50 */ top50: RangeStat; /** Widoczność — szacowany ruch */ visibility: RangeStat; /** Średnia pozycja (bez pola percent) */ position: { current: number; previous: number; diff: number }; /** Suma pozycji */ summary_position: RangeStat; /** Liczba fraz, które zyskały w tym URL-u. **Uwaga: string, nie number.** */ wins: { current: string }; /** Liczba fraz, które straciły w tym URL-u. **Uwaga: string, nie number.** */ losses: { current: string }; }; } type RangeStat = { current: number; previous: number; diff: number; percent: number; } export default HistoryUrlsGetLossesResponse ``` ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data, database, timeout, unknown */ type: string; message: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > **`418`** zwracany jest również dla błędów walidacji — nie tylko przy ograniczeniu liczby żądań (rate limiting). Nieznane `country_id` → `418` z komunikatem "Unknown country\_id". Niedozwolony `order.prop` → `418` `invalid_data` z komunikatem "This value is not allow. Please use correct colum name" (pisownia oryginalna). > > **Znany błąd — odwrócony komunikat.** Gdy `date_min` jest **późniejszy** niż `date_max`, reguła `DateRangeRules` zwraca komunikat `"date_max must be less or equal than date_min"` — treść jest odwrócona; należy go odczytywać jako naruszenie reguły zakresu dat, a nie jako dosłowną instrukcję. ## Powiązane akcje - `getData` — pełna lista URL-i w zakresie dat (taka sama struktura żądania) - `getWins` — URL-e, których widoczność wzrosła w zakresie dat - `getLosses` — URL-e, których widoczność spadła (`MODE_DECREASE`, ta strona) - `getAcquired` — URL-e nowo pozyskane (zaczęły rankować w badanym okresie) - `getLost` — URL-e utracone (przestały rankować w badanym okresie) --- # Historia URL-i: pozyskane (`getAcquired`) **`POST /api/visibility_analysis/reports/history/urls/getAcquired`** Zwraca adresy URL domeny **pozyskane** w badanym okresie — takie, które zaczęły rankować między `date_min` a `date_max` (tryb `MODE_GAIN`). W odróżnieniu od raportu historii fraz, ten raport agreguje dane **po adresach URL**, a nie po pojedynczych frazach — każdy wiersz opisuje jeden URL wraz ze statystykami: liczbą fraz, przedziałami pozycji (`top3`/`top10`/`top50`), widocznością (szacowany ruch), średnią i sumaryczną pozycją oraz liczbą fraz, które zyskały (`wins`) i straciły (`losses`). | URL | Frazy | TOP3 | TOP3 poprz. | TOP3 Δ | TOP3 % | TOP10 | TOP10 poprz. | TOP10 Δ | TOP10 % | TOP50 | TOP50 poprz. | TOP50 Δ | TOP50 % | Widoczność | Widoczność poprz. | Widoczność Δ | Widoczność % | Śr. pozycja | Śr. pozycja poprz. | Śr. pozycja Δ | Suma pozycji | Suma pozycji poprz. | Suma pozycji Δ | Suma pozycji % | Wzrosty (fraz) | Spadki (fraz) | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | zalando.pl/obuwie/ugg/ | 6 | 1 | 1 | 0 | 0 | 3 | 3 | 0 | 0 | 2 | 2 | 0 | 0 | 31198.72 | 5235.75 | 25962.97 | 4.9588 | 10 | 8 | 2 | 62 | 52 | 10 | 0.1923 | 5 | 1 | | zalando.pl/obuwie/?q=nike+air+max | 20 | 5 | 6 | -1 | -0.1667 | 2 | 4 | -2 | -0.5 | 13 | 10 | 3 | 0.3 | 21723.24 | 11914.02 | 9809.23 | 0.8233 | 16 | 17 | -1 | 323 | 349 | -26 | -0.0745 | 15 | 5 | _zalando.pl (2026-06-20 → 2026-06-29) — URL-e pozyskane w okresie. Wszystkie pola wiersza (poza mapami o kluczach-datach — są w JSON). Uwaga: keywords_count, wins i losses przychodzą jako stringi._ --- ## Żądanie `POST` `/api/visibility_analysis/reports/history/urls/getAcquired` Nagłówki: `Authorization: Bearer `, `Content-Type: application/json`. ### Struktura żądania **Podstawowy** ```jsonc filename="żądanie-podstawowe.jsonc" { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "date_min": "2026-06-20", "date_max": "2026-06-29", "country_id": 1, "order": { "prop": "statistics.visibility.current", "dir": "desc" } } ``` **Rozszerzony** ```jsonc filename="żądanie-rozszerzone.jsonc" { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "date_min": "2026-06-20", "date_max": "2026-06-29", "country_id": 1, "order": { "prop": "statistics.visibility.current", "dir": "desc" }, "limit": 2, "page": 1 } ``` **cURL** ```bash curl --location --request POST 'https://api.senuto.com/api/visibility_analysis/reports/history/urls/getAcquired' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' \ --data-raw '{ "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "date_min": "2026-06-20", "date_max": "2026-06-29", "country_id": 1, "order": { "prop": "statistics.visibility.current", "dir": "desc" } }' ``` ### Parametry ```ts type HistoryUrlsGetAcquiredRequest = { /** * **Wymagane**. Domena, subdomena, katalog lub URL do analizy — interpretowane zgodnie z `fetch_mode`. */ domain: string; /** * **Wymagane**. Sposób interpretacji `domain`. * - `topLevelDomain` — cała domena (najczęstszy przypadek; wartość "domain" **nie istnieje**) * - `subdomain` — pojedyncza subdomena * - `catalog` — ścieżka/katalog * - `url` — dokładny URL */ fetch_mode: 'topLevelDomain' | 'subdomain' | 'catalog' | 'url'; /** * **Wymagane**. Id kraju (bazy danych). Polska = `1`. Nieznana wartość → `418` z komunikatem "Unknown country_id". */ country_id: number; /** * **Wymagane**. Początek zakresu dat, `YYYY-MM-DD`. Musi być nie późniejszy niż `date_max`. */ date_min: string; /** * **Wymagane**. Koniec zakresu dat, `YYYY-MM-DD`. Musi być nie wcześniejszy niż `date_min`. */ date_max: string; /** * **Wymagane**. **Pojedyncza** dyrektywa sortowania (jeden obiekt — nie tablica). * * Dozwolone wartości `prop` (tylko 4 — inaczej niż w raporcie fraz): * - `statistics.visibility.current` * - `statistics.visibility.previous` * - `statistics.visibility.diff` * - `statistics.visibility.percent` * * Inna wartość → `418` `invalid_data` z komunikatem "This value is not allow. Please use correct colum name" (pisownia oryginalna). */ order: { prop: | 'statistics.visibility.current' | 'statistics.visibility.previous' | 'statistics.visibility.diff' | 'statistics.visibility.percent'; dir: 'asc' | 'desc'; }; /** * Liczba wierszy na stronę. * @default 10 */ limit?: number; /** * Numer strony. * @default 1 */ page?: number; /** * Opcjonalne filtrowanie wyników. Kontroler przyjmuje ten parametr (wg źródła), * ale zestaw dozwolonych kluczy filtrów dla raportu URL-i **nie został jeszcze * zweryfikowany na żywo** — używaj ostrożnie. */ filtering?: unknown; } export default HistoryUrlsGetAcquiredRequest ``` > **Ostrzeżenie:** > **`order` to pojedynczy obiekt** (`{ prop, dir }`) — nie tablica. Ten raport dopuszcza wyłącznie 4 właściwości sortowania (`statistics.visibility.*`) — próba sortowania np. po `keyword` lub `statistics.position.current` zakończy się błędem `418`. > **Ostrzeżenie:** > Wymagane są: **`domain`**, **`fetch_mode`**, **`country_id`**, **`date_min`**, **`date_max`** oraz pojedynczy obiekt **`order`** (`{ prop, dir }` — nie tablica). Dozwolone są **tylko 4** wartości `order.prop` (właściwości `statistics.visibility.*`) — mniej niż w raporcie fraz, który ma ich 11. Wartość `fetch_mode: "domain"` **nie istnieje** — użyj `topLevelDomain`. Uwaga na typy: część pól liczbowych przychodzi jako **stringi** (`keywords_count`, `statistics.wins.current`, `statistics.losses.current`). ## Odpowiedź W przypadku powodzenia otrzymujesz `data` (tablicę wierszy z URL-ami) oraz `pagination`. Dla zapytania (`zalando.pl`, 2026-06-20 → 2026-06-29) raport zwrócił `count` = 1 527 pozyskanych URL-i. **Skrócona** ```json filename="przykładowa-odpowiedź (skrócona)" { "success": true, "data": [ { "url": "zalando.pl/obuwie/ugg/", "keywords_count": "6", "statistics": { "visibility": { "current": 31198.72, "previous": 5235.75, "diff": 25962.97 } /* … */ } } ], "pagination": { "page_count": 764, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 1527, "limit": 2 } } ``` **Pełna** ```json filename="przykładowa-odpowiedź (200)" { "success": true, "data": [ { "url": "zalando.pl/obuwie/ugg/", "keywords_count": "6", "statistics": { "top3": { "current": 1, "previous": 1, "diff": 0, "percent": 0 }, "top10": { "current": 3, "previous": 3, "diff": 0, "percent": 0 }, "top50": { "current": 2, "previous": 2, "diff": 0, "percent": 0 }, "visibility": { "current": 31198.72, "previous": 5235.75, "diff": 25962.97, "percent": 4.9588 }, "position": { "current": 10, "previous": 8, "diff": 2 }, "summary_position": { "current": 62, "previous": 52, "diff": 10, "percent": 0.1923 }, "wins": { "current": "5" }, "losses": { "current": "1" } } }, { "url": "zalando.pl/obuwie/?q=nike+air+max", "keywords_count": "20", "statistics": { "top3": { "current": 5, "previous": 6, "diff": -1, "percent": -0.1667 }, "top10": { "current": 2, "previous": 4, "diff": -2, "percent": -0.5 }, "top50": { "current": 13, "previous": 10, "diff": 3, "percent": 0.3 }, "visibility": { "current": 21723.24, "previous": 11914.02, "diff": 9809.23, "percent": 0.8233 }, "position": { "current": 16, "previous": 17, "diff": -1 }, "summary_position": { "current": 323, "previous": 349, "diff": -26, "percent": -0.0745 }, "wins": { "current": "15" }, "losses": { "current": "5" } } } ], "pagination": { "page_count": 764, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 1527, "limit": 2 } } ``` ### Struktura odpowiedzi ```ts type HistoryUrlsGetAcquiredResponse = { /** `true` przy powodzeniu; przy błędzie `false` i koperta z `error` */ success: boolean; /** Zwrócone wiersze z URL-ami */ data: UrlRow[]; /** Metadane paginacji */ pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; limit: number; }; } type UrlRow = { /** Adres URL (bez protokołu) */ url: string; /** Liczba fraz rankujących na ten URL. **Uwaga: string, nie number.** */ keywords_count: string; statistics: { /** Liczba fraz URL-a w TOP 3 (`{ current, previous, diff, percent }`) */ top3: RangeStat; /** Liczba fraz URL-a w TOP 10 */ top10: RangeStat; /** Liczba fraz URL-a w TOP 50 */ top50: RangeStat; /** Widoczność — szacowany ruch */ visibility: RangeStat; /** Średnia pozycja (bez pola percent) */ position: { current: number; previous: number; diff: number }; /** Suma pozycji */ summary_position: RangeStat; /** Liczba fraz, które zyskały w tym URL-u. **Uwaga: string, nie number.** */ wins: { current: string }; /** Liczba fraz, które straciły w tym URL-u. **Uwaga: string, nie number.** */ losses: { current: string }; }; } type RangeStat = { current: number; previous: number; diff: number; percent: number; } export default HistoryUrlsGetAcquiredResponse ``` ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data, database, timeout, unknown */ type: string; message: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > **`418`** zwracany jest również dla błędów walidacji — nie tylko przy ograniczeniu liczby żądań (rate limiting). Nieznane `country_id` → `418` z komunikatem "Unknown country\_id". Niedozwolony `order.prop` → `418` `invalid_data` z komunikatem "This value is not allow. Please use correct colum name" (pisownia oryginalna). > > **Znany błąd — odwrócony komunikat.** Gdy `date_min` jest **późniejszy** niż `date_max`, reguła `DateRangeRules` zwraca komunikat `"date_max must be less or equal than date_min"` — treść jest odwrócona; należy go odczytywać jako naruszenie reguły zakresu dat, a nie jako dosłowną instrukcję. ## Powiązane akcje - `getData` — pełna lista URL-i w zakresie dat (taka sama struktura żądania) - `getWins` — URL-e, których widoczność wzrosła w zakresie dat - `getLosses` — URL-e, których widoczność spadła w zakresie dat - `getAcquired` — URL-e nowo pozyskane (`MODE_GAIN`, ta strona) - `getLost` — URL-e utracone (przestały rankować w badanym okresie) --- # Historia URL-i: utracone (`getLost`) **`POST /api/visibility_analysis/reports/history/urls/getLost`** Zwraca adresy URL domeny **utracone** w badanym okresie — takie, które przestały rankować między `date_min` a `date_max` (tryb `MODE_LOSE`). W odróżnieniu od raportu historii fraz, ten raport agreguje dane **po adresach URL**, a nie po pojedynczych frazach — każdy wiersz opisuje jeden URL wraz ze statystykami: liczbą fraz, przedziałami pozycji (`top3`/`top10`/`top50`), widocznością (szacowany ruch), średnią i sumaryczną pozycją oraz liczbą fraz, które zyskały (`wins`) i straciły (`losses`). | URL | Frazy | TOP3 | TOP3 poprz. | TOP3 Δ | TOP3 % | TOP10 | TOP10 poprz. | TOP10 Δ | TOP10 % | TOP50 | TOP50 poprz. | TOP50 Δ | TOP50 % | Widoczność | Widoczność poprz. | Widoczność Δ | Widoczność % | Śr. pozycja | Śr. pozycja poprz. | Śr. pozycja Δ | Suma pozycji | Suma pozycji poprz. | Suma pozycji Δ | Suma pozycji % | Wzrosty (fraz) | Spadki (fraz) | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | zalando.pl/ochnik/ | 5 | 0 | 2 | -2 | -1 | 3 | 2 | 1 | 0.5 | 2 | 1 | 1 | 1 | 22678.5 | 79197.19 | -56518.69 | -0.7136 | 12 | 7 | 5 | 63 | 38 | 25 | 0.6579 | 0 | 5 | | zalando.pl/akcesoria-torby-kobiety/pinko/ | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 5321.25 | 8672.4 | -3351.15 | -0.3864 | 3 | 2 | 1 | 3 | 2 | 1 | 0.5 | 0 | 1 | _zalando.pl (2026-06-20 → 2026-06-29) — URL-e utracone w okresie. Wszystkie pola wiersza (poza mapami o kluczach-datach — są w JSON). Uwaga: keywords_count, wins i losses przychodzą jako stringi._ --- ## Żądanie `POST` `/api/visibility_analysis/reports/history/urls/getLost` Nagłówki: `Authorization: Bearer `, `Content-Type: application/json`. ### Struktura żądania **Podstawowy** ```jsonc filename="żądanie-podstawowe.jsonc" { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "date_min": "2026-06-20", "date_max": "2026-06-29", "country_id": 1, "order": { "prop": "statistics.visibility.current", "dir": "desc" } } ``` **Rozszerzony** ```jsonc filename="żądanie-rozszerzone.jsonc" { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "date_min": "2026-06-20", "date_max": "2026-06-29", "country_id": 1, "order": { "prop": "statistics.visibility.current", "dir": "desc" }, "limit": 2, "page": 1 } ``` **cURL** ```bash curl --location --request POST 'https://api.senuto.com/api/visibility_analysis/reports/history/urls/getLost' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' \ --data-raw '{ "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "date_min": "2026-06-20", "date_max": "2026-06-29", "country_id": 1, "order": { "prop": "statistics.visibility.current", "dir": "desc" } }' ``` ### Parametry ```ts type HistoryUrlsGetLostRequest = { /** * **Wymagane**. Domena, subdomena, katalog lub URL do analizy — interpretowane zgodnie z `fetch_mode`. */ domain: string; /** * **Wymagane**. Sposób interpretacji `domain`. * - `topLevelDomain` — cała domena (najczęstszy przypadek; wartość "domain" **nie istnieje**) * - `subdomain` — pojedyncza subdomena * - `catalog` — ścieżka/katalog * - `url` — dokładny URL */ fetch_mode: 'topLevelDomain' | 'subdomain' | 'catalog' | 'url'; /** * **Wymagane**. Id kraju (bazy danych). Polska = `1`. Nieznana wartość → `418` z komunikatem "Unknown country_id". */ country_id: number; /** * **Wymagane**. Początek zakresu dat, `YYYY-MM-DD`. Musi być nie późniejszy niż `date_max`. */ date_min: string; /** * **Wymagane**. Koniec zakresu dat, `YYYY-MM-DD`. Musi być nie wcześniejszy niż `date_min`. */ date_max: string; /** * **Wymagane**. **Pojedyncza** dyrektywa sortowania (jeden obiekt — nie tablica). * * Dozwolone wartości `prop` (tylko 4 — inaczej niż w raporcie fraz): * - `statistics.visibility.current` * - `statistics.visibility.previous` * - `statistics.visibility.diff` * - `statistics.visibility.percent` * * Inna wartość → `418` `invalid_data` z komunikatem "This value is not allow. Please use correct colum name" (pisownia oryginalna). */ order: { prop: | 'statistics.visibility.current' | 'statistics.visibility.previous' | 'statistics.visibility.diff' | 'statistics.visibility.percent'; dir: 'asc' | 'desc'; }; /** * Liczba wierszy na stronę. * @default 10 */ limit?: number; /** * Numer strony. * @default 1 */ page?: number; /** * Opcjonalne filtrowanie wyników. Kontroler przyjmuje ten parametr (wg źródła), * ale zestaw dozwolonych kluczy filtrów dla raportu URL-i **nie został jeszcze * zweryfikowany na żywo** — używaj ostrożnie. */ filtering?: unknown; } export default HistoryUrlsGetLostRequest ``` > **Ostrzeżenie:** > **`order` to pojedynczy obiekt** (`{ prop, dir }`) — nie tablica. Ten raport dopuszcza wyłącznie 4 właściwości sortowania (`statistics.visibility.*`) — próba sortowania np. po `keyword` lub `statistics.position.current` zakończy się błędem `418`. > **Ostrzeżenie:** > Wymagane są: **`domain`**, **`fetch_mode`**, **`country_id`**, **`date_min`**, **`date_max`** oraz pojedynczy obiekt **`order`** (`{ prop, dir }` — nie tablica). Dozwolone są **tylko 4** wartości `order.prop` (właściwości `statistics.visibility.*`) — mniej niż w raporcie fraz, który ma ich 11. Wartość `fetch_mode: "domain"` **nie istnieje** — użyj `topLevelDomain`. Uwaga na typy: część pól liczbowych przychodzi jako **stringi** (`keywords_count`, `statistics.wins.current`, `statistics.losses.current`). ## Odpowiedź W przypadku powodzenia otrzymujesz `data` (tablicę wierszy z URL-ami) oraz `pagination`. Dla zapytania (`zalando.pl`, 2026-06-20 → 2026-06-29) raport zwrócił `count` = 2 983 utraconych URL-i. **Skrócona** ```json filename="przykładowa-odpowiedź (skrócona)" { "success": true, "data": [ { "url": "zalando.pl/ochnik/", "keywords_count": "5", "statistics": { "visibility": { "current": 22678.5, "previous": 79197.19, "diff": -56518.69 } /* … */ } } ], "pagination": { "page_count": 1492, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 2983, "limit": 2 } } ``` **Pełna** ```json filename="przykładowa-odpowiedź (200)" { "success": true, "data": [ { "url": "zalando.pl/ochnik/", "keywords_count": "5", "statistics": { "top3": { "current": 0, "previous": 2, "diff": -2, "percent": -1 }, "top10": { "current": 3, "previous": 2, "diff": 1, "percent": 0.5 }, "top50": { "current": 2, "previous": 1, "diff": 1, "percent": 1 }, "visibility": { "current": 22678.5, "previous": 79197.19, "diff": -56518.69, "percent": -0.7136 }, "position": { "current": 12, "previous": 7, "diff": 5 }, "summary_position": { "current": 63, "previous": 38, "diff": 25, "percent": 0.6579 }, "wins": { "current": "0" }, "losses": { "current": "5" } } }, { "url": "zalando.pl/akcesoria-torby-kobiety/pinko/", "keywords_count": "1", "statistics": { "top3": { "current": 1, "previous": 1, "diff": 0, "percent": 0 }, "top10": { "current": 0, "previous": 0, "diff": 0, "percent": 0 }, "top50": { "current": 0, "previous": 0, "diff": 0, "percent": 0 }, "visibility": { "current": 5321.25, "previous": 8672.4, "diff": -3351.15, "percent": -0.3864 }, "position": { "current": 3, "previous": 2, "diff": 1 }, "summary_position": { "current": 3, "previous": 2, "diff": 1, "percent": 0.5 }, "wins": { "current": "0" }, "losses": { "current": "1" } } } ], "pagination": { "page_count": 1492, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 2983, "limit": 2 } } ``` ### Struktura odpowiedzi ```ts type HistoryUrlsGetLostResponse = { /** `true` przy powodzeniu; przy błędzie `false` i koperta z `error` */ success: boolean; /** Zwrócone wiersze z URL-ami */ data: UrlRow[]; /** Metadane paginacji */ pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; limit: number; }; } type UrlRow = { /** Adres URL (bez protokołu) */ url: string; /** Liczba fraz rankujących na ten URL. **Uwaga: string, nie number.** */ keywords_count: string; statistics: { /** Liczba fraz URL-a w TOP 3 (`{ current, previous, diff, percent }`) */ top3: RangeStat; /** Liczba fraz URL-a w TOP 10 */ top10: RangeStat; /** Liczba fraz URL-a w TOP 50 */ top50: RangeStat; /** Widoczność — szacowany ruch */ visibility: RangeStat; /** Średnia pozycja (bez pola percent) */ position: { current: number; previous: number; diff: number }; /** Suma pozycji */ summary_position: RangeStat; /** Liczba fraz, które zyskały w tym URL-u. **Uwaga: string, nie number.** */ wins: { current: string }; /** Liczba fraz, które straciły w tym URL-u. **Uwaga: string, nie number.** */ losses: { current: string }; }; } type RangeStat = { current: number; previous: number; diff: number; percent: number; } export default HistoryUrlsGetLostResponse ``` ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data, database, timeout, unknown */ type: string; message: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > **`418`** zwracany jest również dla błędów walidacji — nie tylko przy ograniczeniu liczby żądań (rate limiting). Nieznane `country_id` → `418` z komunikatem "Unknown country\_id". Niedozwolony `order.prop` → `418` `invalid_data` z komunikatem "This value is not allow. Please use correct colum name" (pisownia oryginalna). > > **Znany błąd — odwrócony komunikat.** Gdy `date_min` jest **późniejszy** niż `date_max`, reguła `DateRangeRules` zwraca komunikat `"date_max must be less or equal than date_min"` — treść jest odwrócona; należy go odczytywać jako naruszenie reguły zakresu dat, a nie jako dosłowną instrukcję. ## Powiązane akcje - `getData` — pełna lista URL-i w zakresie dat (taka sama struktura żądania) - `getWins` — URL-e, których widoczność wzrosła w zakresie dat - `getLosses` — URL-e, których widoczność spadła w zakresie dat - `getAcquired` — URL-e nowo pozyskane (zaczęły rankować w badanym okresie) - `getLost` — URL-e utracone (`MODE_LOSE`, ta strona) --- # Dashboard: statystyki domeny (`getDomainStatistics`) **`GET /api/visibility_analysis/reports/dashboard/getDomainStatistics`** Zwraca kluczowe metryki widoczności dla domeny (TOP3 / TOP10, widoczność całkowita, ranking domeny, wartość ekwiwalentu reklamowego, wiodąca kategoria oraz słowa kluczowe AI Overviews), każdą jako porównanie ostatniego okresu z poprzednim. --- ## Żądanie `GET` `/api/visibility_analysis/reports/dashboard/getDomainStatistics` Parametry są odczytywane z **query string** (`getQuery`). Nagłówki: `Authorization: Bearer `. ### Struktura żądania **Podstawowy** ```jsonc filename="żądanie-podstawowe.jsonc" // Query string parameters { "domain": "zalando.pl", "fetch_mode": "topLevelDomain" } // GET /api/visibility_analysis/reports/dashboard/getDomainStatistics?domain=zalando.pl&fetch_mode=topLevelDomain ``` **Rozszerzony** ```jsonc filename="żądanie-rozszerzone.jsonc" // Query string parameters { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "country_id": 1 } // GET /api/visibility_analysis/reports/dashboard/getDomainStatistics?domain=zalando.pl&fetch_mode=topLevelDomain&country_id=1 ``` **cURL** ```bash curl --location --request GET 'https://api.senuto.com/api/visibility_analysis/reports/dashboard/getDomainStatistics?domain=zalando.pl&fetch_mode=topLevelDomain' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' ``` ### Parametry ```ts type DashboardGetDomainStatisticsRequest = { /** * **Wymagane**. Domena lub subdomena do analizy, walidowana po stronie API. * Bez schematu/protokołu — np. `zalando.pl`. */ domain: string; /** * **Wymagane**. Tryb agregacji danych o widoczności. * Uwaga: `domain` NIE jest prawidłową wartością — dla całej domeny użyj `topLevelDomain`. * - `topLevelDomain` — cała domena (typowy przypadek) * - `subdomain` — pojedyncza subdomena * - `catalog` — ścieżka/katalog * - `url` — dokładny adres URL */ fetch_mode: 'topLevelDomain' | 'subdomain' | 'catalog' | 'url'; /** * Identyfikator kraju (baza Google). Opcjonalne — gdy jest pominięte, `0` lub nieprawidłowe, * backend stosuje domyślny kraj (PL). * @default 1 */ country_id?: number; } export default DashboardGetDomainStatisticsRequest ``` > **Ostrzeżenie:** > Dozwolone wartości `fetch_mode` to dokładnie `['topLevelDomain', 'subdomain', 'catalog', 'url']` (`DataFetchMode::AVAILABLE_MODES`). Przekazanie `domain` to częsty błąd — nie odpowiada żadnemu trybowi i nie przechodzi walidacji. > **Ostrzeżenie:** > Ta akcja odczytuje dane z **query string** (kontroler używa `getQuery`), więc parametry przesyłaj w adresie URL — wysłanie ich wyłącznie w treści JSON zwraca `418`. Zarówno **`domain`**, jak i **`fetch_mode`** są **wymagane**. Wartość `fetch_mode` **`domain` nie istnieje** — dla całej domeny użyj **`topLevelDomain`**. `country_id` jest opcjonalne i domyślnie przyjmuje **PL (`1`)**, gdy jest pominięte lub nieprawidłowe. ## Odpowiedź W przypadku powodzenia otrzymujesz `data.statistics` — pojedynczy obiekt zawierający 9 metryk. Każda metryka to porównanie `recent_value` z `older_value` (bieżący okres vs poprzedni) wraz z bezwzględną różnicą `diff` oraz ułamkowym `percent` (np. `-0.0011` = `-0.11%`). Nie ma paginacji — to obiekt statystyk, a nie lista. `category` jest zagnieżdżona jako `{ name, statistics }`. **Skrócona** ```json filename="przykładowa-odpowiedź (skrócona)" { "success": true, "data": { "statistics": { "visibility": { "recent_value": 5423828, "older_value": 5433671, "diff": -9843, "percent": -0.0018 }, "top3": { "recent_value": 46546, "older_value": 46595, "diff": -49, "percent": -0.0011 } /* … top10, domain_rank, ads_equivalent, category, aio_keywords, aio_visible_keywords */ } } } ``` **Pełna** ```json filename="przykładowa-odpowiedź (200)" { "success": true, "data": { "statistics": { "top3": { "recent_value": 46546, "older_value": 46595, "diff": -49, "percent": -0.0011 }, "top10": { "recent_value": 114215, "older_value": 114263, "diff": -48, "percent": -0.0004 }, "visibility": { "recent_value": 5423828, "older_value": 5433671, "diff": -9843, "percent": -0.0018 }, "domain_rank": { "recent_value": 103, "older_value": 103, "diff": 0, "percent": 0 }, "ads_equivalent": { "recent_value": 13282982.37, "older_value": 15171821.56, "diff": -1888839.19, "percent": -0.1245 }, "category": { "name": "Styl i moda", "statistics": { "recent_value": 9, "older_value": 9, "diff": 0, "percent": 0 } }, "aio_keywords": { "recent_value": 12721, "older_value": 12721, "diff": 0, "percent": 0 }, "aio_visible_keywords": { "recent_value": 0, "older_value": 0, "diff": 0, "percent": 0 } } } } ``` ### Struktura odpowiedzi ```ts type DashboardStatisticsResponse = { /** `true` przy powodzeniu; przy błędzie `false` i koperta z `error` */ success: boolean; data: { statistics: { /** Słowa kluczowe w TOP3 */ top3: Metric; /** Słowa kluczowe w TOP10 */ top10: Metric; /** Całkowita szacowana widoczność (ruch) */ visibility: Metric; /** Pozycja w rankingu domen */ domain_rank: Metric; /** Wartość ekwiwalentu reklamowego ruchu organicznego, w PLN */ ads_equivalent: Metric; /** Wiodąca kategoria, z własną metryką */ category: { name: string; statistics: Metric }; /** Słowa kluczowe wywołujące AI Overviews */ aio_keywords: Metric; /** Słowa kluczowe, dla których domena jest widoczna w AI Overviews */ aio_visible_keywords: Metric; }; }; } /** Porównanie ostatniego okresu z poprzednim. `percent` to ułamek, np. -0.0011 = -0.11%. */ type Metric = { recent_value: number; older_value: number; diff: number; percent: number; } export default DashboardStatisticsResponse ``` ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data, unreachable_data, timeout, database, unknown */ type: string; message: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > **`418`** jest zwracane także dla błędów walidacji — nie tylko przy ograniczaniu liczby żądań. Najczęstsze przyczyny to przesłanie parametrów w treści JSON zamiast w query string (przez co `getQuery` jest puste) oraz pominięcie `fetch_mode`. Brakujące `fetch_mode` → > `{"success":false,"data":{"error":{"type":"invalid_data","params":{"fetch_mode":{"_required":"This field is required"}}}}}`. ## Powiązane akcje - `getDomainStatistics` — kluczowe statystyki domeny (ta strona) - `getData` (Positions) — pozycje na poziomie słów kluczowych, stojące za tymi zagregowanymi danymi (ten sam kształt `domain` + `fetch_mode`) - `getDomainStatistics` dla innego `fetch_mode` — przekaż `subdomain`, `catalog` lub `url`, aby ograniczyć te same metryki do subdomeny, ścieżki lub dokładnego adresu URL --- # Dashboard: dane domeny (`getDomainData`) **`GET /api/visibility_analysis/reports/dashboard/getDomainData`** Zwraca dashboardową „kartę domeny" dla danej domeny: najważniejsze kategorie, w których domena jest widoczna (każda z porównaniami `rank`, `visibility` i `top10`), technologie wykryte na stronie oraz informację, czy domena jest oznaczona jako ulubiona. --- ## Żądanie `GET` `/api/visibility_analysis/reports/dashboard/getDomainData` Parametry odczytywane są z **query stringa** (`getQuery`). Nagłówki: `Authorization: Bearer `. **Nie** wysyłaj treści JSON — jest ignorowana, a żądanie nie przechodzi walidacji i zwraca `418`. ### Struktura żądania **Podstawowy** ```jsonc filename="żądanie-podstawowe.jsonc" // Query string parameters { "domain": "zalando.pl", "fetch_mode": "topLevelDomain" } // GET /api/visibility_analysis/reports/dashboard/getDomainData?domain=zalando.pl&fetch_mode=topLevelDomain ``` **Rozszerzony** ```jsonc filename="żądanie-rozszerzone.jsonc" // Query string parameters { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "country_id": 1 } // GET /api/visibility_analysis/reports/dashboard/getDomainData?domain=zalando.pl&fetch_mode=topLevelDomain&country_id=1 ``` **cURL** ```bash curl --location --request GET 'https://api.senuto.com/api/visibility_analysis/reports/dashboard/getDomainData?domain=zalando.pl&fetch_mode=topLevelDomain' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' ``` ### Parametry ```ts type DashboardGetDomainDataRequest = { /** * **Wymagane**. Domena lub subdomena do analizy, walidowana po stronie API. * Bez schematu/protokołu — np. `zalando.pl`. Przekazywane jako parametr **query stringa**. */ domain: string; /** * **Wymagane**. Tryb agregacji danych o widoczności. * Uwaga: `domain` NIE jest prawidłową wartością — użyj `topLevelDomain` dla całej domeny. * - `topLevelDomain` — cała domena (najczęstszy przypadek; „domena" mapuje się tutaj) * - `subdomain` — pojedyncza subdomena * - `catalog` — ścieżka/katalog * - `url` — dokładny adres URL */ fetch_mode: 'topLevelDomain' | 'subdomain' | 'catalog' | 'url'; /** * Identyfikator kraju (baza Google). Opcjonalne — gdy brak, `0` lub nieprawidłowe, * backend używa domyślnego kraju (PL). * @default 1 */ country_id?: number; } export default DashboardGetDomainDataRequest ``` > **Ostrzeżenie:** > Dozwolone wartości `fetch_mode` to dokładnie `['topLevelDomain', 'subdomain', 'catalog', 'url']` (`DataFetchMode::AVAILABLE_MODES`). Przekazanie `domain` to częsty błąd — nie mapuje się na nic i nie przechodzi walidacji. Pamiętaj, że te parametry trafiają do **query stringa**, a nie do treści żądania. > **Ostrzeżenie:** > **To jest endpoint `GET`.** Parametry odczytywane są z **query stringa** (kontroler używa `getQuery`) — wysyłaj je w URL-u, a nie w treści JSON. Wysłanie treści JSON zwraca **`418`** (`invalid_data` — wymagane `domain`/`fetch_mode`), ponieważ `getQuery` jest puste. Zarówno **`domain`**, jak i **`fetch_mode`** są **wymagane**. Wartość `fetch_mode` **`domain` nie istnieje** — użyj **`topLevelDomain`** dla całej domeny. `country_id` jest opcjonalne i domyślnie przyjmuje **PL (`1`)**, gdy nie zostanie podane. ## Odpowiedź W przypadku powodzenia otrzymujesz pojedynczy **obiekt** `data` (a nie listę — **nie ma `pagination`**). Zawiera on analizowaną `domain`, datę `updated`, tablicę `categories`, w których domena jest widoczna (każda z porównaniem `rank` / `visibility` / `top10`), tablicę wykrytych `technologies` oraz flagę `is_favourite`. Każda metryka to porównanie `recent_value` z `older_value` wraz z bezwzględną różnicą `diff` i ułamkowym `percent` (np. `0.0364` = `+3.64%`). **Skrócona** ```json filename="przykładowa-odpowiedź (skrócona)" { "success": true, "data": { "domain": "zalando.pl", "updated": "2026-06-30", "categories": [ { "id": 290, "name": "Styl i moda", "statistics": { "rank": { "recent_value": 3, "older_value": 3, "diff": 0, "percent": 0 } /* … visibility, top10 */ } } /* … more categories */ ], "technologies": [ { "name": "ActiveCampaign", "icon": "activecampaign.png" } /* … */ ], "is_favourite": false } } ``` **Pełna** ```json filename="przykładowa-odpowiedź (200)" { "success": true, "data": { "domain": "zalando.pl", "updated": "2026-06-30", "categories": [ { "id": 290, "name": "Styl i moda", "statistics": { "rank": { "recent_value": 3, "older_value": 3, "diff": 0, "percent": 0 }, "visibility": { "recent_value": 2096740, "older_value": 2020426.41, "diff": 76314, "percent": 0.0364 }, "top10": { "recent_value": 53273, "older_value": 53542, "diff": -269, "percent": -0.005 } } }, { "id": 295, "name": "Ubrania", "statistics": { "rank": { "recent_value": 3, "older_value": 3, "diff": 0, "percent": 0 }, "visibility": { "recent_value": 1550110, "older_value": 1472866.32, "diff": 77243, "percent": 0.0498 }, "top10": { "recent_value": 30106, "older_value": 30265, "diff": -159, "percent": -0.0053 } } } ], "technologies": [ { "name": "ActiveCampaign", "icon": "activecampaign.png" }, { "name": "Google Cloud", "icon": "google_cloud.svg" }, { "name": "Nginx", "icon": "Nginx.svg" } ], "is_favourite": false } } ``` ### Struktura odpowiedzi ```ts type DashboardDomainDataResponse = { /** `true` przy powodzeniu; przy błędzie `false` i koperta z `error` */ success: boolean; /** Pojedynczy obiekt — tutaj NIE ma paginacji. */ data: { /** Analizowana domena, zwrócona zwrotnie */ domain: string; /** Data ostatniego odświeżenia danych o widoczności (YYYY-MM-DD) */ updated: string; /** Najważniejsze kategorie, w których domena jest widoczna */ categories: { /** Identyfikator kategorii Senuto */ id: number; /** Nazwa kategorii */ name: string; statistics: { /** Pozycja domeny w obrębie kategorii */ rank: Metric; /** Szacowana widoczność (ruch) w kategorii */ visibility: Metric; /** Słowa kluczowe rankujące w TOP10 w kategorii */ top10: Metric; }; }[]; /** Technologie wykryte na stronie */ technologies: { name: string; /** Nazwa pliku ikony serwowanej przez Senuto */ icon: string; }[]; /** Czy domena jest oznaczona gwiazdką przez bieżące konto */ is_favourite: boolean; }; } /** Porównanie bieżącego okresu z poprzednim. `percent` to ułamek, np. 0.0364 = +3.64%. */ type Metric = { recent_value: number; older_value: number; diff: number; percent: number; } export default DashboardDomainDataResponse ``` ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data, unreachable_data, timeout, database, unknown */ type: string; message: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > **`418`** zwracane jest również dla błędów walidacji — nie tylko dla limitowania zapytań. Najczęstsze przyczyny to wysłanie parametrów w **treści JSON** zamiast w query stringu (przez co `getQuery` jest puste → wymagane `domain`/`fetch_mode`) oraz pominięcie `fetch_mode`. Brak `fetch_mode` → > `{"success":false,"data":{"error":{"type":"invalid_data","params":{"fetch_mode":{"_required":"This field is required"}}}}}`. Nieprawidłowy lub brakujący token zwraca **`Unauthorized`**. ## Powiązane akcje - `getDomainData` — karta domeny: kategorie, technologie, flaga ulubionej (ta strona) - `getDomainStatistics` — kluczowe metryki domeny (TOP3 / TOP10, widoczność, pozycja, AIO) dla tej samej pary `domain` + `fetch_mode` - `getData` (Positions) — pozycje na poziomie słów kluczowych stojące za tymi agregatami (ten sam kształt `domain` + `fetch_mode`) --- # Dashboard: technologie (`getTechnologies`) **`GET /api/visibility_analysis/reports/dashboard/getTechnologies`** Zwraca listę technologii wykrytych na analizowanej domenie (np. serwer WWW, CDN, narzędzia marketingowe) w formie prostej tablicy: nazwa technologii oraz nazwa pliku jej ikony. To lekki endpoint dashboardu Analizy widoczności — bez paginacji i bez dodatkowych statystyk. | Nazwa | Plik ikony | | --- | --- | | ActiveCampaign | activecampaign.png | | Google Cloud | google_cloud.svg | | Google Cloud CDN | google_cloud_cdn.svg | | Nginx | Nginx.svg | _zalando.pl — technologie wykryte na domenie (endpoint bez paginacji). Wszystkie adresowalne pola wiersza._ --- ## Żądanie `GET` `/api/visibility_analysis/reports/dashboard/getTechnologies` Nagłówki: `Authorization: Bearer `. Parametry przekazywane w query stringu. ### Struktura żądania **Podstawowy** ```text filename="query-string.txt" /api/visibility_analysis/reports/dashboard/getTechnologies ?domain=zalando.pl &fetch_mode=topLevelDomain &country_id=1 ``` **cURL** ```bash curl --location --request GET 'https://api.senuto.com/api/visibility_analysis/reports/dashboard/getTechnologies?domain=zalando.pl&fetch_mode=topLevelDomain&country_id=1' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' ``` ### Parametry ```ts type DashboardGetTechnologiesRequest = { /** * **Wymagane**. Domena, subdomena, katalog lub URL do analizy — interpretowane zgodnie z `fetch_mode`. */ domain: string; /** * **Wymagane**. Sposób interpretacji `domain`. * - `topLevelDomain` — cała domena * - `subdomain` — pojedyncza subdomena * - `catalog` — ścieżka/katalog * - `url` — dokładny URL */ fetch_mode: 'topLevelDomain' | 'subdomain' | 'catalog' | 'url'; /** * Id kraju (bazy danych). Polska = `1`. W tym endpoincie **opcjonalne** * (walidator `GetTechnologiesValidator`); podana nieznana wartość → `418` z komunikatem `Unknown country_id`. */ country_id?: number; } export default DashboardGetTechnologiesRequest ``` > **Ostrzeżenie:** > Dwa parametry są **wymagane**: **`domain`** oraz **`fetch_mode`** (walidator `GetTechnologiesValidator`); `country_id` jest tutaj **opcjonalne** — inaczej niż w większości endpointów tego modułu. To endpoint **GET** — parametry przekazujesz w query stringu. Jeśli podasz `country_id`, nieznana wartość zwraca `418` z komunikatem `Unknown country_id`. ## Odpowiedź W przypadku powodzenia otrzymujesz `data` — tablicę wykrytych technologii. Każdy element zawiera `name` (nazwę technologii) oraz `icon` (nazwę pliku ikony, np. `activecampaign.png` lub `Nginx.svg` — bez pełnego adresu URL). Endpoint nie zwraca paginacji. **Skrócona** ```json filename="przykładowa-odpowiedź (skrócona)" { "success": true, "data": [ { "name": "Nginx", "icon": "Nginx.svg" } ] } ``` **Pełna** ```json filename="przykładowa-odpowiedź (200)" { "success": true, "data": [ { "name": "ActiveCampaign", "icon": "activecampaign.png" }, { "name": "Google Cloud", "icon": "google_cloud.svg" }, { "name": "Google Cloud CDN", "icon": "google_cloud_cdn.svg" }, { "name": "Nginx", "icon": "Nginx.svg" } ] } ``` ### Struktura odpowiedzi ```ts type DashboardTechnologiesResponse = { /** `true` przy powodzeniu; przy błędzie `false` i koperta z `error` */ success: boolean; /** Lista technologii wykrytych na domenie */ data: Technology[]; } type Technology = { /** Nazwa technologii, np. "Nginx", "Google Cloud CDN" */ name: string; /** Nazwa pliku ikony, np. "activecampaign.png" lub "Nginx.svg" (bez pełnego URL) */ icon: string; } export default DashboardTechnologiesResponse ``` ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data, database, timeout, unknown */ type: string; message: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > **`418`** zwracany jest również dla błędów walidacji — nie tylko przy ograniczeniu liczby żądań (rate limiting). Brak `domain` lub `fetch_mode` → odpowiedź `invalid_data`; podanie nieznanego `country_id` → komunikat `Unknown country_id`. ## Powiązane akcje - `getTechnologies` — technologie wykryte na domenie (ta strona) - `getDomainStatistics` — zbiorcze statystyki widoczności domeny na dashboardzie - `getDomainData` — podstawowe dane analizowanej domeny --- # Analiza konkurentów (`getData`) **`POST /api/visibility_analysis/tools/competitors_analysis/getData`** Narzędzie **Analiza konkurentów** porównuje frazy kluczowe domeny głównej z frazami konkurentów. W zależności od trybu (`mode`) zwraca frazy wspólne, frazy konkurentów, których brakuje domenie głównej, albo frazy domeny głównej. Każdy wiersz zawiera statystyki frazy (średnia liczba wyszukiwań, CPC, trendy, trudność) oraz pozycję i URL dla każdej z porównywanych domen. Wynik jest stronicowany. | Fraza | Wyszukiwania/mies. | CPC | Trudność | Trend (12 mies.) | Parametry | | --- | --- | --- | --- | --- | --- | | nike | 673000 | 1.31 | 77 | [673000,673000,673000,673000,673000,673000,1000000,673000,550000,550000,823000,550000] | [] | | adidas | 368000 | 0.68 | 77 | [450000,368000,368000,450000,368000,301000,368000,301000,301000,368000,550000,450000] | [] | _zalando.pl vs eobuwie.com.pl (mode: common_keywords, limit: 2). Wszystkie adresowalne pola stałe wiersza (pominięto klucze dynamiczne per domena — ``, `_pos`, `_url` — bo zawierają kropkę w nazwie i nie da się ich zaadresować ścieżką; są w JSON-ie i sekcji „Struktura odpowiedzi”)._ --- ## Żądanie `POST` `/api/visibility_analysis/tools/competitors_analysis/getData` Nagłówki: `Authorization: Bearer `, `Content-Type: application/json`. ### Struktura żądania **Podstawowy** ```jsonc filename="żądanie-podstawowe.jsonc" { "main_domain": { "domain": "zalando.pl", "gte": 1, "lte": 50 }, "competitors_domains": [ { "domain": "eobuwie.com.pl", "gte": 1, "lte": 50 } ], "mode": "common_keywords" } ``` **Rozszerzony** ```jsonc filename="żądanie-rozszerzone.jsonc" { "main_domain": { "domain": "zalando.pl", "gte": 1, "lte": 50 }, "competitors_domains": [ { "domain": "eobuwie.com.pl", "gte": 1, "lte": 50 } ], "mode": "common_keywords", "country_id": 1, "page": 1, "limit": 2 } ``` **cURL** ```bash curl --location --request POST 'https://api.senuto.com/api/visibility_analysis/tools/competitors_analysis/getData' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' \ --data-raw '{ "main_domain": { "domain": "zalando.pl", "gte": 1, "lte": 50 }, "competitors_domains": [{ "domain": "eobuwie.com.pl", "gte": 1, "lte": 50 }], "mode": "common_keywords", "limit": 2 }' ``` ### Parametry ```ts type GetCompetitorsAnalysisRequest = { /** * **Wymagane** (walidator `DataValidator`). Domena główna analizy jako **obiekt** — * nie zwykły string. Pola `gte` i `lte` (zakres pozycji w TOP wyników) są wymagane * wewnątrz obiektu. */ main_domain: DomainWithPositionRange; /** * **Wymagane**. Niepusta **tablica** obiektów o tej samej strukturze co `main_domain` — * konkurenci, z którymi porównywana jest domena główna. Każdy obiekt musi zawierać * `domain`, `gte` i `lte`. */ competitors_domains: DomainWithPositionRange[]; /** * **Wymagane**. Tryb porównania (klasa `KeywordsFetchMode`): * - `common_keywords` — frazy wspólne domeny głównej i konkurentów, * - `competitors_keywords` — frazy konkurentów, których brakuje domenie głównej, * - `main_domain_keywords` — frazy domeny głównej. */ mode: "common_keywords" | "competitors_keywords" | "main_domain_keywords"; /** * ID kraju (bazy słów kluczowych). * @default 1 (Polska) */ country_id?: number; /** * Numer strony paginacji. * @default 1 */ page?: number; /** * Rozmiar strony paginacji. Odbija się w `pagination.limit`. * @default 10 */ limit?: number; /** * Filtrowanie wyników. Rejestr dostępnych pól filtrowania jest **niezweryfikowany** — * używaj ostrożnie i testuj na małych zapytaniach. */ filtering?: Record; /** * Sortowanie wyników. Podobnie jak `filtering` — rejestr pól niezweryfikowany. */ order?: Record; } type DomainWithPositionRange = { /** **Wymagane**. Nazwa domeny, np. `zalando.pl`. */ domain: string; /** **Wymagane w obiekcie**. Dolna granica zakresu pozycji (np. `1`). */ gte: number; /** **Wymagane w obiekcie**. Górna granica zakresu pozycji (np. `50`). */ lte: number; } export default GetCompetitorsAnalysisRequest ``` > **Ostrzeżenie:** > Pamiętaj, że `main_domain` to **obiekt** (nie string), a `competitors_domains` to **tablica obiektów** — każdy z kompletem `domain` + `gte` + `lte`. Brak któregokolwiek z pól wymaganych kończy się odpowiedzią `418` z `invalid_data` i kluczem `_required`. > **Ostrzeżenie:** > Endpoint obsługuje metodę **`POST`** z ciałem JSON (`Authorization: Bearer `, `Content-Type: application/json`). Narzędzia z grupy `tools` mają **dzienny limit użyć** — po jego przekroczeniu API zwraca `418`. **Pułapka w strukturze odpowiedzi:** poza polami stałymi (`keyword`, `cpc`, `searches`, `trends`, `difficulty`, `params`) każdy wiersz zawiera klucze **dynamiczne per domena**, zdublowane w dwóch postaciach — obiekt `"": { pos, url }` **oraz** spłaszczone pola `"_pos"` i `"_url"` z tymi samymi wartościami. ## Odpowiedź Po pomyślnym żądaniu otrzymujesz `data` (tablicę wierszy fraz kluczowych) oraz `pagination`. Wiersz składa się z pól stałych — `keyword`, `cpc`, `searches`, `trends` (12 miesięcy), `difficulty`, `params` — oraz z kluczy **dynamicznych per domena** dla domeny głównej i każdego konkurenta. Dane pozycji każdej domeny są zdublowane: raz jako obiekt `"": { pos, url }`, a raz jako spłaszczone pola `"_pos"` i `"_url"` z tymi samymi wartościami. **Skrócona** ```json filename="przykładowa-odpowiedź (skrócona)" { "success": true, "data": [ { "keyword": "nike", "cpc": 1.31, "searches": 673000, "difficulty": 77, "zalando.pl": { "pos": 3, "url": "zalando.pl/nike/" }, "eobuwie.com.pl": { "pos": 2, "url": "eobuwie.com.pl/c/eobuwie/marka:nike" } } ], "pagination": { "page_count": 37217, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 74433, "limit": 2 } } ``` **Pełna** ```json filename="przykładowa-odpowiedź (200)" { "success": true, "data": [ { "keyword": "nike", "cpc": 1.31, "searches": 673000, "trends": [673000, 673000, 673000, 673000, 673000, 673000, 1000000, 673000, 550000, 550000, 823000, 550000], "difficulty": 77, "params": [], "zalando.pl": { "pos": 3, "url": "zalando.pl/nike/" }, "zalando.pl_pos": 3, "zalando.pl_url": "zalando.pl/nike/", "eobuwie.com.pl": { "pos": 2, "url": "eobuwie.com.pl/c/eobuwie/marka:nike" }, "eobuwie.com.pl_pos": 2, "eobuwie.com.pl_url": "eobuwie.com.pl/c/eobuwie/marka:nike" }, { "keyword": "adidas", "cpc": 0.68, "searches": 368000, "trends": [450000, 368000, 368000, 450000, 368000, 301000, 368000, 301000, 301000, 368000, 550000, 450000], "difficulty": 77, "params": [], "zalando.pl": { "pos": 10, "url": "zalando.pl/adidas-originals/" }, "zalando.pl_pos": 10, "zalando.pl_url": "zalando.pl/adidas-originals/", "eobuwie.com.pl": { "pos": 2, "url": "eobuwie.com.pl/c/eobuwie/marka:adidas" }, "eobuwie.com.pl_pos": 2, "eobuwie.com.pl_url": "eobuwie.com.pl/c/eobuwie/marka:adidas" } ], "pagination": { "page_count": 37217, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 74433, "limit": 2 } } ``` ### Struktura odpowiedzi ```ts type GetCompetitorsAnalysisResponse = { /** `true` przy powodzeniu; przy błędzie `false` i koperta z `error` */ success: boolean; /** Wiersze fraz kluczowych z pozycjami porównywanych domen */ data: CompetitorsAnalysisRow[]; /** Metadane paginacji */ pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; /** Łączna liczba fraz spełniających kryteria (w przykładzie: 74433) */ count: number; /** Wartość przekazanego `limit` */ limit: number; }; } type CompetitorsAnalysisRow = { /** Fraza kluczowa */ keyword: string; /** Średni koszt kliknięcia (CPC) */ cpc: number; /** Średnia miesięczna liczba wyszukiwań */ searches: number; /** Liczba wyszukiwań w ostatnich 12 miesiącach (tablica 12 liczb) */ trends: number[]; /** Trudność frazy (0-100) */ difficulty: number; /** Parametry frazy */ params: unknown[]; /** * **Klucze dynamiczne per domena** — dla domeny głównej i każdego konkurenta wiersz * zawiera trzy wpisy o kluczu pochodnym od nazwy domeny: * - `""`: obiekt `{ pos: number, url: string }`, * - `"_pos"`: ta sama pozycja jako liczba, * - `"_url"`: ten sam URL jako string. * Wartości w postaci obiektowej i spłaszczonej są identyczne (duplikacja). */ [domainKey: string]: { pos: number; url: string } | number | string | unknown[]; } export default GetCompetitorsAnalysisResponse ``` ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data, unknown */ type: string; message: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > **`418`** jest zwracane przy błędach walidacji: brak pól wymaganych (`main_domain`, `competitors_domains`, `mode`) skutkuje `invalid_data` z regułą `_required` dla brakującego pola. Pola `gte` i `lte` są wymagane **wewnątrz** obiektów domen — pominięcie ich w `main_domain` lub w elemencie `competitors_domains` również kończy się `418`. Kod `418` pojawia się także po **przekroczeniu dziennego limitu użyć** narzędzi `tools`. ## Powiązane akcje - `getData` — porównanie fraz domeny głównej z konkurentami (ta strona; jedyna akcja narzędzia) --- # Konkurenci (raport) (`getData`) **`POST /api/visibility_analysis/reports/competitors/getData`** Zwraca listę konkurentów badanej domeny wraz z zestawem porównawczych statystyk widoczności: liczbą fraz w TOP3 / TOP10 / TOP50, widocznością, ekwiwalentem Ads oraz rangą domeny (`domain_rank`) — każda metryka z wartością bieżącą, poprzednią oraz zmianą (`diff`, `percent`). Dla każdego konkurenta podawana jest też liczba fraz wspólnych z badaną domeną (`common_keywords`). | Domena | Domena główna | Wspólne frazy | TOP3 · bieżąca | TOP3 · poprz. | TOP3 · zmiana | TOP3 · % | TOP3 · historia | TOP10 · bieżąca | TOP10 · poprz. | TOP10 · zmiana | TOP10 · % | TOP10 · historia | TOP50 · bieżąca | TOP50 · poprz. | TOP50 · zmiana | TOP50 · % | TOP50 · historia | Widoczność · bieżąca | Widoczność · poprz. | Widoczność · zmiana | Widoczność · % | Widoczność · historia | Ekwiwalent Ads · bieżąca | Ekwiwalent Ads · poprz. | Ekwiwalent Ads · zmiana | Ekwiwalent Ads · % | Ekwiwalent Ads · historia | Ranga domeny · bieżąca | Ranga domeny · poprz. | Ranga domeny · zmiana | Ranga domeny · % | Ranga domeny · historia | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | ccc.eu | false | 849 | 10276 | 10297 | -21 | -0.002 | | 22397 | 22415 | -18 | -0.0008 | | 72842 | 72989 | -147 | -0.002 | | 1794218.55 | 1804132.4 | -9913.85 | -0.0055 | | 1225878.21 | 1225878.21 | 0 | 0 | | 262 | 260 | 2 | 0.0077 | | | www2.hm.com | false | 623 | 8920 | 8933 | -13 | -0.0015 | | 20055 | 20105 | -50 | -0.0025 | | 84146 | 84219 | -73 | -0.0009 | | 3281018.52 | 3272619.07 | 8399.45 | 0.0026 | | 4465558.99 | 4465558.99 | 0 | 0 | | 0 | 0 | 0 | 0 | | _zalando.pl (limit: 2) — najwięksi konkurenci wg liczby wspólnych fraz. Wszystkie adresowalne pola wiersza (brak pól o zmiennych kluczach; `history` jest w tym raporcie zawsze `null`)._ > **Ostrzeżenie:** > To **inny raport** niż przestarzały `domain_competitors/getTopCompetitors` — ten raport **nie jest przestarzały** i to jego należy używać do porównywania konkurentów. Wymagane są trzy parametry: **`domain`**, **`fetch_mode`** oraz **`country_id`** (walidator `CompetitorsValidator`). Nieznane `country_id` zwraca `418` z komunikatem `"Unknown country_id"`. --- ## Żądanie `POST` `/api/visibility_analysis/reports/competitors/getData` Nagłówki: `Authorization: Bearer `, `Content-Type: application/json`. ### Struktura żądania **Podstawowy** ```jsonc filename="żądanie-podstawowe.jsonc" { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "country_id": 1 } ``` **Rozszerzony** ```jsonc filename="żądanie-rozszerzone.jsonc" { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "country_id": 1, "page": 1, "limit": 2 } ``` **cURL** ```bash curl --location --request POST 'https://api.senuto.com/api/visibility_analysis/reports/competitors/getData' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' \ --data-raw '{ "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "country_id": 1 }' ``` ### Parametry ```ts type CompetitorsGetDataRequest = { /** * **Wymagane**. Domena, subdomena, katalog lub URL do analizy — interpretowane zgodnie z `fetch_mode`. */ domain: string; /** * **Wymagane**. Sposób interpretacji `domain`. * - `topLevelDomain` — cała domena (najczęstszy przypadek) * - `subdomain` — pojedyncza subdomena * - `catalog` — ścieżka/katalog * - `url` — dokładny URL */ fetch_mode: 'topLevelDomain' | 'subdomain' | 'catalog' | 'url'; /** * **Wymagane**. Id kraju (bazy danych). Polska = `1`. * Nieznana wartość → `418` z komunikatem `"Unknown country_id"`. */ country_id: number; /** * Numer strony. * @default 1 */ page?: number; /** * Liczba wierszy na stronę. * @default 10 */ limit?: number; } export default CompetitorsGetDataRequest ``` ## Odpowiedź W przypadku powodzenia otrzymujesz `data` (tablicę wierszy konkurentów) oraz `pagination`. Każdy wiersz zawiera domenę konkurenta, flagę `is_main_domain` (czy to badana domena), liczbę wspólnych fraz `common_keywords` oraz obiekt `statistics` z sześcioma metrykami — każda w formacie `{ current, previous, diff, percent, history }`. Wartość `domain_rank` równa `0` oznacza brak rangi dla danej domeny. **Skrócona** ```json filename="przykładowa-odpowiedź (skrócona)" { "success": true, "data": [ { "domain": "ccc.eu", "is_main_domain": false, "common_keywords": 849, "statistics": { "top10": { "current": 22397 }, "visibility": { "current": 1794218.55 } /* … */ } } ], "pagination": { "page_count": 27, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 53, "limit": 2 } } ``` **Pełna** ```json filename="przykładowa-odpowiedź (200)" { "success": true, "data": [ { "domain": "ccc.eu", "is_main_domain": false, "common_keywords": 849, "statistics": { "top3": { "current": 10276, "previous": 10297, "diff": -21, "percent": -0.002, "history": null }, "top10": { "current": 22397, "previous": 22415, "diff": -18, "percent": -0.0008, "history": null }, "top50": { "current": 72842, "previous": 72989, "diff": -147, "percent": -0.002, "history": null }, "visibility": { "current": 1794218.55, "previous": 1804132.4, "diff": -9913.85, "percent": -0.0055, "history": null }, "ads_equivalent": { "current": 1225878.21, "previous": 1225878.21, "diff": 0, "percent": 0, "history": null }, "domain_rank": { "current": 262, "previous": 260, "diff": 2, "percent": 0.0077, "history": null } } }, { "domain": "www2.hm.com", "is_main_domain": false, "common_keywords": 623, "statistics": { "top3": { "current": 8920, "previous": 8933, "diff": -13, "percent": -0.0015, "history": null }, "top10": { "current": 20055, "previous": 20105, "diff": -50, "percent": -0.0025, "history": null }, "top50": { "current": 84146, "previous": 84219, "diff": -73, "percent": -0.0009, "history": null }, "visibility": { "current": 3281018.52, "previous": 3272619.07, "diff": 8399.45, "percent": 0.0026, "history": null }, "ads_equivalent": { "current": 4465558.99, "previous": 4465558.99, "diff": 0, "percent": 0, "history": null }, "domain_rank": { "current": 0, "previous": 0, "diff": 0, "percent": 0, "history": null } } } ], "pagination": { "page_count": 27, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 53, "limit": 2 } } ``` ### Struktura odpowiedzi ```ts type CompetitorsGetDataResponse = { /** `true` przy powodzeniu; przy błędzie `false` i koperta z `error` */ success: boolean; /** Zwrócone wiersze konkurentów */ data: CompetitorRow[]; /** Metadane paginacji */ pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; limit: number; }; } type CompetitorRow = { /** Domena konkurenta */ domain: string; /** Czy wiersz dotyczy badanej (głównej) domeny */ is_main_domain: boolean; /** Liczba fraz wspólnych z badaną domeną */ common_keywords: number; statistics: { /** Liczba fraz w TOP3 */ top3: MetricValue; /** Liczba fraz w TOP10 */ top10: MetricValue; /** Liczba fraz w TOP50 */ top50: MetricValue; /** Widoczność (szacowany miesięczny ruch organiczny) */ visibility: MetricValue; /** Ekwiwalent Ads — szacowany koszt równoważnego ruchu płatnego */ ads_equivalent: MetricValue; /** Ranga domeny; `0` oznacza brak rangi */ domain_rank: MetricValue; }; } type MetricValue = { /** Wartość bieżąca */ current: number; /** Wartość z poprzedniego pomiaru */ previous: number; /** Różnica current - previous */ diff: number; /** Zmiana względna (ułamek, np. -0.0055 = -0,55%) */ percent: number; /** W tym raporcie zawsze null */ history: null; } export default CompetitorsGetDataResponse ``` ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data, database, timeout, unknown */ type: string; message: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > **`418`** zwracany jest również dla błędów walidacji (`CompetitorsValidator`) — nie tylko przy ograniczeniu liczby żądań. Brak wymaganego pola (`domain`, `fetch_mode`, `country_id`) zwraca `invalid_data` z mapą `params`, a nieznane `country_id` zwraca komunikat `"Unknown country_id"`. ## Powiązane akcje - `getData` — porównawcze dane konkurentów (ta strona) Pokrewny, ale **przestarzały** raport: `domain_competitors/getTopCompetitors` (kontroler oznaczony `@deprecated`) — zwraca listę największych konkurentów w starszym formacie. W nowych integracjach używaj opisywanego tu `reports/competitors/getData`. --- # AI Overviews: frazy (`getKeywords`) **`POST /api/visibility_analysis/reports/ai_overviews/getKeywords`** > **Ostrzeżenie:** > **Endpoint przestarzały.** Działa, ale może nie być aktywnie utrzymywany i w przyszłości zostać wycofany lub zastąpiony — co może też tłumaczyć, dlaczego zwraca pustą listę mimo obecności danych AIO. Planuj integrację z ostrożnością. Zwraca frazy kluczowe, dla których domena pojawia się w sekcji **AI Overviews** Google (generatywne podsumowania wyświetlane nad wynikami organicznymi), wraz ze statystykami pozycji, widoczności, ruchu oraz cech SERP dla każdej frazy. Kształt żądania i odpowiedzi jest spójny z pozostałymi raportami kontrolera `visibility_analysis/reports`. --- ## Żądanie `POST` `/api/visibility_analysis/reports/ai_overviews/getKeywords` Nagłówki: `Authorization: Bearer `, `Content-Type: application/json`. ### Struktura żądania **Podstawowy** ```jsonc filename="żądanie-podstawowe.jsonc" { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "limit": 2 } ``` **Rozszerzony** ```jsonc filename="żądanie-rozszerzone.jsonc" { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "limit": 10, "page": 1, "with_history": true, "order": { "prop": "visibility", "dir": "desc" }, "filtering": [] } ``` **cURL** ```bash curl --location --request POST 'https://api.senuto.com/api/visibility_analysis/reports/ai_overviews/getKeywords' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' \ --data-raw '{ "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "limit": 2 }' ``` ### Parametry ```ts type AiOverviewsGetKeywordsRequest = { /** * **Wymagane**. Domena, subdomena, katalog lub URL do analizy — interpretowane zgodnie z `fetch_mode`. */ domain: string; /** * **Wymagane**. Sposób interpretacji `domain`. * - `topLevelDomain` — cała domena (najczęstszy przypadek; "domena") * - `subdomain` — pojedyncza subdomena * - `catalog` — ścieżka/katalog * - `url` — dokładny URL */ fetch_mode: 'topLevelDomain' | 'subdomain' | 'catalog' | 'url'; /** * Liczba wierszy na stronę. Nieujemna liczba całkowita. * @default 10 */ limit?: number; /** * Numer strony. Nieujemna liczba całkowita. * @default 1 */ page?: number; /** * Dołącz do odpowiedzi mapę historii pozycji (`history`) dla każdej frazy. * @default true */ with_history?: boolean; /** * Sortowanie wyników — **pojedynczy obiekt**, nie tablica. * Dozwolone `prop` m.in.: `keyword`, `organic_pos`, `visibility`, * `searches`, `best_aio_pos`, `aio_positions_count`, `aio_domains_count`. * Zły kształt lub nieznany klucz NIE zwraca błędu — API po cichu wraca * do sortu domyślnego (`best_organic_pos` rosnąco, `searches` malejąco). */ order?: { prop: string; dir: 'asc' | 'desc' }; /** * Dyrektywy filtrowania. Pusta tablica = brak filtrowania. */ filtering?: unknown[]; } export default AiOverviewsGetKeywordsRequest ``` > **Ostrzeżenie:** > Nazwy parametrów: jest to **`filtering`** (nie `filters`) oraz **`order`** (nie `sort_by` / `sort_order`). Uwaga na kształt `order`: to **obiekt `{ "prop": …, "dir": … }`** — forma tablicowa `[{ field, direction }]` jest przez API **ignorowana po cichu** (zwraca 200 z sortem domyślnym). Kierunku sortowania nie udało się potwierdzić na żywym API, bo raport zwraca pustą listę (patrz ostrzeżenie o statusie `@deprecated` powyżej) — kształt na podstawie źródła backendu. > **Ostrzeżenie:** > Zarówno **`domain`**, jak i **`fetch_mode`** są **wymagane**; pominięcie `fetch_mode` zwraca `418` z `invalid_data`. Metoda to **`POST`** z treścią JSON — przesłanie parametrów inną drogą skutkuje `405`/`418`. > **Ostrzeżenie:** > Przykładowa domena nie zwróciła danych dla tego raportu (`data` jest puste, `count` = `0`) — poniżej udokumentowano **strukturę odpowiedzi** na podstawie analizy kontrolera, bez zmyślania wartości. Dla domeny obecnej w AI Overviews `data` zawiera wiersze fraz o kształcie analogicznym do raportu pozycji. ## Odpowiedź Po pomyślnym żądaniu otrzymujesz `data` (tablicę wierszy fraz, dla których domena pojawia się w AI Overviews) oraz `pagination`. Gdy domena nie występuje w AI Overviews, `data` jest puste, a `count` wynosi `0`. **Skrócona** ```json filename="przykładowa-odpowiedź (skrócona)" { "success": true, "data": [], "pagination": { "count": 0, "page_count": 0, "current_page": 1, "limit": 2 } } ``` **Pełna** ```json filename="przykładowa-odpowiedź (200)" { "success": true, "data": [], "pagination": { "page_count": 0, "current_page": 1, "has_next_page": false, "has_prev_page": false, "count": 0, "limit": 2 } } ``` ### Struktura odpowiedzi ```ts type AiOverviewsResponse = { /** `true` przy powodzeniu; przy błędzie `false` i koperta z `error` */ success: boolean; /** Zwrócone wiersze fraz; puste, gdy domena nie pojawia się w AI Overviews */ data: KeywordRow[]; /** Metadane paginacji */ pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; limit: number; }; } type KeywordRow = { keyword_id: number; kid: string; domain: string; keyword: string; words_count: number; statistics: { position: { current: number; previous: number; diff: number; changes: { wins: number; losses: number; no_changes: number }; history: Record | [] }; visibility: { current: number; previous: number; diff: number; percent: number; history: null }; url: { current: string; previous: string; is_change: 0 | 1 }; cpc: { current: number }; searches: { current: number }; trends: { history: number[]; peak: number | null }; difficulty: { current: number }; snippets: { current: string[] }; }; } export default AiOverviewsResponse ``` ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data, invalid_filtering, unreachable_data, timeout, database, unknown */ type: string; message: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > **`418`** jest zwracane również w przypadku błędów walidacji — nie tylko przy ograniczaniu liczby żądań. Brak `fetch_mode` → > `{"success":false,"data":{"error":{"type":"invalid_data","params":{"fetch_mode":{"_required":"This field is required"}}}}}`. ## Powiązane akcje - `getKeywords` — frazy z AI Overviews (ta strona) - `getData` — bieżące pozycje organiczne (raport `positions`, taki sam kształt żądania) - `getWins` / `getLosses` — frazy, które zyskały / straciły pozycje - `getKeywordHistory` — pełna historia pozycji dla pojedynczej frazy (`keyword_id` + `kid` + `domain` + `fetch_mode`) --- # AI Overviews: statystyki (`getStatistics`) **`GET /api/visibility_analysis/reports/ai_overviews/getStatistics`** > **Ostrzeżenie:** > **Endpoint przestarzały.** Działa, ale może nie być aktywnie utrzymywany i w przyszłości zostać wycofany lub zastąpiony. Planuj integrację z ostrożnością. Zwraca zbiorcze statystyki obecności domeny w **AI Overviews** (odpowiedziach generowanych przez AI w wynikach Google) dla zadanej domeny. Pojedynczy obiekt `data` z licznikami fraz wyzwalających AIO, liczbą fraz z udziałem domeny, średnią pozycją oraz bilansem zysków i strat widoczności. Raport służy do oceny, na ile domena jest reprezentowana w wynikach AI względem własnej widoczności organicznej. --- ## Żądanie `GET` `/api/visibility_analysis/reports/ai_overviews/getStatistics` Nagłówki: `Authorization: Bearer `. Parametry przekazuje się w **query stringu** (np. `?domain=zalando.pl&fetch_mode=topLevelDomain`); pola zagnieżdżone zapisuje się w notacji `klucz[pod]=…`. ### Struktura żądania **Podstawowy** ```jsonc filename="żądanie-podstawowe.jsonc" { "domain": "zalando.pl", "fetch_mode": "topLevelDomain" } ``` **Rozszerzony** ```jsonc filename="żądanie-rozszerzone.jsonc" { "domain": "zalando.pl", "fetch_mode": "topLevelDomain" } ``` **cURL** ```bash curl --location --request GET 'https://api.senuto.com/api/visibility_analysis/reports/ai_overviews/getStatistics?domain=zalando.pl&fetch_mode=topLevelDomain' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' ``` ### Parametry ```ts type AiOverviewsGetStatisticsRequest = { /** * **Wymagane**. Domena, subdomena, katalog lub URL do analizy — interpretowane zgodnie z `fetch_mode`. Przekazywane w query stringu. */ domain: string; /** * **Wymagane**. Sposób interpretacji `domain`. Przekazywany w query stringu. * - `topLevelDomain` — cała domena (najczęstszy przypadek; "domena") * - `subdomain` — pojedyncza subdomena * - `catalog` — ścieżka/katalog * - `url` — dokładny URL */ fetch_mode: 'topLevelDomain' | 'subdomain' | 'catalog' | 'url'; } export default AiOverviewsGetStatisticsRequest ``` > **Ostrzeżenie:** > Endpoint jest obsługiwany metodą **`GET`** — parametry przekazuje się w **query stringu**, a nie w ciele żądania. Zarówno **`domain`**, jak i **`fetch_mode`** są **wymagane**; pominięcie `fetch_mode` zwraca `418` z `invalid_data`. Wysłanie parametrów w ciele żądania (`body`) zamiast w query stringu skutkuje błędem (`418` / `405`). ## Odpowiedź Po pomyślnym żądaniu otrzymujesz pojedynczy obiekt `data` ze statystykami AI Overviews. > **Informacja:** > Przykładowa domena nie zwróciła danych dla tego raportu — wszystkie liczniki w odpowiedzi `200` mają wartość `0`. Poniżej struktura odpowiedzi wraz z nazwami i typami pól. **Skrócona** ```json filename="przykładowa-odpowiedź (skrócona)" { "success": true, "data": { "aio_keywords_count": 0, "aio_keywords_with_domain_count": 0, "aio_avg_pos": 0 /* … */ } } ``` **Pełna** ```json filename="przykładowa-odpowiedź (200)" { "success": true, "data": { "aio_keywords_count": 0, "aio_keywords_with_domain_count": 0, "aio_avg_pos": 0, "aio_losses_count": 0, "aio_wins_count": 0, "aio_organic_keywords_count": 0, "aio_losses_vis_sum": 0, "organic_keywords_count": 0, "aio_vis_loss_percentage": 0 } } ``` ### Struktura odpowiedzi ```ts type AiOverviewsStatisticsResponse = { /** `true` przy powodzeniu; przy błędzie `false` i koperta z `error` */ success: boolean; data: { /** Liczba fraz wyzwalających AI Overview w analizowanym zakresie */ aio_keywords_count: number; /** Liczba fraz z AI Overview, w których pojawia się analizowana domena */ aio_keywords_with_domain_count: number; /** Średnia pozycja domeny w obrębie AI Overview */ aio_avg_pos: number; /** Liczba fraz, w których domena straciła obecność w AI Overview */ aio_losses_count: number; /** Liczba fraz, w których domena zyskała obecność w AI Overview */ aio_wins_count: number; /** Liczba fraz organicznych powiązanych z AI Overview */ aio_organic_keywords_count: number; /** Suma utraconej widoczności (visibility) z tytułu strat w AI Overview */ aio_losses_vis_sum: number; /** Całkowita liczba fraz organicznych domeny */ organic_keywords_count: number; /** Procentowy udział utraconej widoczności AI Overview względem widoczności organicznej */ aio_vis_loss_percentage: number; }; } export default AiOverviewsStatisticsResponse ``` ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data, unreachable_data, timeout, database, unknown */ type: string; message: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > **`418`** jest zwracane również w przypadku błędów walidacji — nie tylko przy ograniczaniu liczby żądań. Brak `fetch_mode` → > `{"success":false,"data":{"error":{"type":"invalid_data","params":{"fetch_mode":{"_required":"This field is required"}}}}}`. Przekazanie parametrów w ciele żądania zamiast w query stringu (ten endpoint to `GET`) skutkuje błędem `418` / `405`. ## Powiązane akcje - `getStatistics` — zbiorcze statystyki AI Overviews dla domeny (ta strona) - `getData` z kontrolera `positions` — bieżące pozycje organiczne fraz domeny (taki sam kształt parametrów `domain` + `fetch_mode`) - Pozostałe raporty w przestrzeni nazw `visibility_analysis/reports/…` korzystają z tej samej pary parametrów `domain` + `fetch_mode` --- # AI Overviews: rozkład (`getDistribution`) **`GET /api/visibility_analysis/reports/ai_overviews/getDistribution`** > **Ostrzeżenie:** > **Endpoint przestarzały.** Działa, ale może nie być aktywnie utrzymywany i w przyszłości zostać wycofany lub zastąpiony. Planuj integrację z ostrożnością. Zwraca rozkład obecności domeny w blokach **AI Overviews (AIO)** Google dla wskazanej domeny. Raport pokazuje, jak słowa kluczowe wywołujące AI Overviews rozkładają się względem widoczności domeny w tych blokach — pozwala ocenić, w jakiej części zapytań wyzwalających AIO domena faktycznie pojawia się jako cytowane źródło. --- ## Żądanie `GET` `/api/visibility_analysis/reports/ai_overviews/getDistribution` Parametry są odczytywane z **query string** (`getQuery`). Nagłówki: `Authorization: Bearer `. ### Struktura żądania **Podstawowy** ```jsonc filename="żądanie-podstawowe.jsonc" // Query string parameters { "domain": "zalando.pl", "fetch_mode": "topLevelDomain" } // GET /api/visibility_analysis/reports/ai_overviews/getDistribution?domain=zalando.pl&fetch_mode=topLevelDomain ``` **Rozszerzony** ```jsonc filename="żądanie-rozszerzone.jsonc" // Query string parameters { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "country_id": 1 } // GET /api/visibility_analysis/reports/ai_overviews/getDistribution?domain=zalando.pl&fetch_mode=topLevelDomain&country_id=1 ``` **cURL** ```bash curl --location --request GET 'https://api.senuto.com/api/visibility_analysis/reports/ai_overviews/getDistribution?domain=zalando.pl&fetch_mode=topLevelDomain' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' ``` ### Parametry ```ts type AiOverviewsGetDistributionRequest = { /** * **Wymagane**. Domena, subdomena, katalog lub URL do analizy — interpretowane zgodnie z `fetch_mode`. * Bez schematu/protokołu — np. `zalando.pl`. */ domain: string; /** * **Wymagane**. Sposób interpretacji `domain`. * Uwaga: `domain` NIE jest prawidłową wartością — dla całej domeny użyj `topLevelDomain`. * - `topLevelDomain` — cała domena (najczęstszy przypadek) * - `subdomain` — pojedyncza subdomena * - `catalog` — ścieżka/katalog * - `url` — dokładny adres URL */ fetch_mode: 'topLevelDomain' | 'subdomain' | 'catalog' | 'url'; /** * Identyfikator kraju (baza Google). Opcjonalne — gdy pominięte, `0` lub nieprawidłowe, * backend stosuje domyślny kraj (PL). * @default 1 */ country_id?: number; } export default AiOverviewsGetDistributionRequest ``` > **Ostrzeżenie:** > Dozwolone wartości `fetch_mode` to dokładnie `['topLevelDomain', 'subdomain', 'catalog', 'url']` (`DataFetchMode::AVAILABLE_MODES`). Przekazanie `domain` to częsty błąd — nie odpowiada żadnemu trybowi i nie przechodzi walidacji. > **Ostrzeżenie:** > Ta akcja jest wywoływana metodą **`GET`** i odczytuje dane z **query string**, więc parametry przesyłaj w adresie URL. Wysłanie żądania metodą `POST` zwraca `405`, a przesłanie parametrów wyłącznie w treści JSON kończy się `418`. Zarówno **`domain`**, jak i **`fetch_mode`** są **wymagane**. Wartość `fetch_mode` **`domain` nie istnieje** — dla całej domeny użyj **`topLevelDomain`**. Parametr `country_id` jest opcjonalny i domyślnie przyjmuje **PL (`1`)**, gdy jest pominięty lub nieprawidłowy. > **Ostrzeżenie:** > Domena bez danych AI Overviews zwraca `200` z **pustą tablicą** `data` — to nie błąd, tylko brak wyników dla tej domeny. ## Odpowiedź W przypadku powodzenia otrzymujesz `success: true` oraz `data` — tablicę obiektów rozkładu obecności w AI Overviews. Gdy dla danej domeny brak danych AIO, `data` jest pustą tablicą (`[]`). Tak właśnie odpowiedziała domena testowa `zalando.pl` — `200` z pustą tablicą. **Skrócona** ```json filename="przykładowa-odpowiedź (skrócona)" { "success": true, "data": [] } ``` **Pełna** ```json filename="przykładowa-odpowiedź (200)" { "success": true, "data": [] } ``` > **Ostrzeżenie:** > Dla domen, które nie mają danych AI Overviews, `data` jest **pustą tablicą** — to nie błąd, tylko brak wyników dla tej domeny. Poniżej opisana jest koperta odpowiedzi. ### Struktura odpowiedzi ```ts type AiOverviewsDistributionResponse = { /** Flaga przetworzenia żądania. Potwierdzone (`true`) w odpowiedzi `200`. */ success: boolean; /** * Tablica obiektów rozkładu obecności w AI Overviews. * Pusta tablica (`[]`), gdy domena nie ma danych AIO. */ data: unknown[]; } export default AiOverviewsDistributionResponse ``` ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data, unreachable_data, timeout, database, unknown */ type: string; message: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > Wysłanie żądania niewłaściwą metodą **`POST`** zwraca **`405`** (Method Not Allowed) — ta akcja akceptuje wyłącznie `GET`. Z kolei **`418`** jest zwracane przy błędach walidacji (nie tylko przy ograniczaniu liczby żądań), m.in. gdy parametry trafią do treści JSON zamiast do query string (przez co `getQuery` jest puste) lub gdy pominięto `fetch_mode`. Brak `fetch_mode` → > `{"success":false,"data":{"error":{"type":"invalid_data","params":{"fetch_mode":{"_required":"This field is required"}}}}}`. ## Powiązane akcje - `getDistribution` — rozkład obecności domeny w AI Overviews (ta strona) - `getStatistics` — zbiorcze statystyki AI Overviews dla domeny - `getKeywords` — słowa kluczowe wywołujące AI Overviews (taki sam kształt żądania `domain` + `fetch_mode`) --- # AI Overviews: konkurenci (`getCompetitors`) **`POST /api/visibility_analysis/reports/ai_overviews/getCompetitors`** > **Ostrzeżenie:** > **Endpoint przestarzały.** Działa, ale może nie być aktywnie utrzymywany i w przyszłości zostać wycofany lub zastąpiony. Planuj integrację z ostrożnością. Aktualne odpowiedniki raportów AI Overviews per projekt znajdziesz w module **Monitoring** (`rank_tracker/reports/ai_overviews`). Zwraca listę **konkurentów domeny w blokach AI Overviews (AIO)** Google — czyli inne domeny, które pojawiają się jako cytowane źródła w AI Overviews dla zapytań istotnych dla analizowanej domeny. Raport pozwala ocenić, kto konkuruje z Twoją domeną o obecność w odpowiedziach generowanych przez AI. --- ## Żądanie `POST` `/api/visibility_analysis/reports/ai_overviews/getCompetitors` Parametry przesyłasz w **treści żądania jako JSON**. Nagłówki: `Authorization: Bearer ` oraz `Content-Type: application/json`. ### Struktura żądania **Podstawowy** ```jsonc filename="żądanie-podstawowe.jsonc" { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "country_id": 1 } ``` **Rozszerzony** ```jsonc filename="żądanie-rozszerzone.jsonc" { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "country_id": 1, "page": 1, "limit": 2 } ``` **cURL** ```bash curl --location --request POST 'https://api.senuto.com/api/visibility_analysis/reports/ai_overviews/getCompetitors' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' \ --header 'Content-Type: application/json' \ --data '{"domain":"zalando.pl","fetch_mode":"topLevelDomain","country_id":1,"limit":2}' ``` ### Parametry ```ts type AiOverviewsGetCompetitorsRequest = { /** * **Wymagane**. Domena, subdomena, katalog lub URL do analizy — interpretowane zgodnie z `fetch_mode`. * Bez schematu/protokołu — np. `zalando.pl`. */ domain: string; /** * **Wymagane**. Sposób interpretacji `domain`. * Uwaga: `domain` NIE jest prawidłową wartością — dla całej domeny użyj `topLevelDomain`. * - `topLevelDomain` — cała domena (najczęstszy przypadek) * - `subdomain` — pojedyncza subdomena * - `catalog` — ścieżka/katalog * - `url` — dokładny adres URL */ fetch_mode: 'topLevelDomain' | 'subdomain' | 'catalog' | 'url'; /** * Identyfikator kraju (baza Google). Opcjonalne — walidator tej akcji nie wymusza pola; * gdy pominięte, backend stosuje domyślny kraj (PL). * @default 1 */ country_id?: number; /** * Numer strony wyników. Opcjonalne. * @default 1 */ page?: number; /** * Maksymalna liczba wyników na stronę. Opcjonalne. */ limit?: number; } export default AiOverviewsGetCompetitorsRequest ``` > **Ostrzeżenie:** > Dozwolone wartości `fetch_mode` to dokładnie `topLevelDomain`, `subdomain`, `catalog` i `url` — przekazanie `domain` nie przechodzi walidacji. > **Ostrzeżenie:** > Domena bez danych AI Overviews zwraca `200` z **pustą tablicą** `data` — to nie błąd, tylko brak wyników dla tej domeny. ## Odpowiedź W przypadku powodzenia otrzymujesz `success: true`, `data` — tablicę obiektów konkurentów w AI Overviews — oraz obiekt `pagination` z informacjami o stronicowaniu. Gdy dla danej domeny brak danych AIO, `data` jest pustą tablicą (`[]`), a liczniki paginacji wskazują zero wyników. Tak właśnie odpowiedziała domena testowa `zalando.pl` — `200` z pustą tablicą. **Skrócona** ```json filename="przykładowa-odpowiedź (skrócona)" { "success": true, "data": [] } ``` **Pełna** ```json filename="przykładowa-odpowiedź (200)" { "success": true, "data": [], "pagination": { "page_count": 0, "current_page": 1, "has_next_page": false, "has_prev_page": false, "count": 0, "limit": 2 } } ``` > **Ostrzeżenie:** > Dla domen, które nie mają danych AI Overviews, `data` jest **pustą tablicą** — to nie błąd, tylko brak wyników dla tej domeny. Poniżej opisana jest koperta odpowiedzi. ### Struktura odpowiedzi ```ts type AiOverviewsCompetitorsResponse = { /** Flaga przetworzenia żądania. Potwierdzone (`true`) w odpowiedzi `200`. */ success: boolean; /** * Tablica obiektów konkurentów w AI Overviews. * Pusta tablica (`[]`), gdy domena nie ma danych AIO. */ data: unknown[]; /** Informacje o stronicowaniu wyników. Potwierdzone w odpowiedzi `200`. */ pagination: { /** Łączna liczba stron wyników. */ page_count: number; /** Numer bieżącej strony. */ current_page: number; /** Czy istnieje następna strona. */ has_next_page: boolean; /** Czy istnieje poprzednia strona. */ has_prev_page: boolean; /** Łączna liczba wyników. */ count: number; /** Limit wyników na stronę (odzwierciedla przekazany `limit`). */ limit: number; }; } export default AiOverviewsCompetitorsResponse ``` ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data, unreachable_data, timeout, database, unknown */ type: string; message: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > Błędy walidacji (np. brak wymaganego `domain` lub `fetch_mode`, albo nieprawidłowa wartość `fetch_mode`) zwracane są ze statusem **`418`** i kopertą `invalid_data` z mapą `params` wskazującą pole i naruszoną regułę. ## Powiązane akcje Wszystkie poniższe akcje są przestarzałe: - `getStatistics` — zbiorcze statystyki AI Overviews dla domeny - `getKeywords` — słowa kluczowe wywołujące AI Overviews - `getDistribution` — rozkład obecności domeny w AI Overviews - `getCompetitors` — konkurenci domeny w AI Overviews (ta strona) - `getKeywordResults` — wyniki AI Overviews dla pojedynczej frazy - `getKeywordsIntents` — agregacja fraz AIO według intencji - `getOpportunities` — frazy-szanse: domena rankuje organicznie, ale nie jest w AIO Aktualne odpowiedniki raportów AI Overviews **per projekt** znajdziesz w module **Monitoring** (`rank_tracker/reports/ai_overviews`). --- # AI Overviews: wyniki frazy (`getKeywordResults`) **`POST /api/visibility_analysis/reports/ai_overviews/getKeywordResults`** > **Ostrzeżenie:** > **Endpoint przestarzały.** Działa, ale może nie być aktywnie utrzymywany i w przyszłości zostać wycofany lub zastąpiony. Planuj integrację z ostrożnością. Aktualne odpowiedniki raportów AI Overviews per projekt znajdziesz w module **Monitoring** (`rank_tracker/reports/ai_overviews`). Zwraca **wyniki AI Overviews (AIO) dla pojedynczej frazy** — szczegóły obecności źródeł w bloku AI Overviews dla konkretnego słowa kluczowego wskazanego identyfikatorem `keyword_id`. Raport pozwala sprawdzić, jak wygląda blok AIO dla wybranego zapytania w kontekście analizowanej domeny. --- ## Żądanie `POST` `/api/visibility_analysis/reports/ai_overviews/getKeywordResults` Parametry przesyłasz w **treści żądania jako JSON**. Nagłówki: `Authorization: Bearer ` oraz `Content-Type: application/json`. ### Struktura żądania **Podstawowy** ```jsonc filename="żądanie-podstawowe.jsonc" { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "country_id": 1, "keyword_id": null } ``` **Rozszerzony** ```jsonc filename="żądanie-rozszerzone.jsonc" { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "country_id": 1, "keyword_id": null, "limit": 2 } ``` **cURL** ```bash curl --location --request POST 'https://api.senuto.com/api/visibility_analysis/reports/ai_overviews/getKeywordResults' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' \ --header 'Content-Type: application/json' \ --data '{"domain":"zalando.pl","fetch_mode":"topLevelDomain","country_id":1,"keyword_id":null,"limit":2}' ``` ### Parametry ```ts type AiOverviewsGetKeywordResultsRequest = { /** * **Wymagane**. Domena, subdomena, katalog lub URL do analizy — interpretowane zgodnie z `fetch_mode`. * Bez schematu/protokołu — np. `zalando.pl`. */ domain: string; /** * **Wymagane**. Sposób interpretacji `domain`. * Uwaga: `domain` NIE jest prawidłową wartością — dla całej domeny użyj `topLevelDomain`. * - `topLevelDomain` — cała domena (najczęstszy przypadek) * - `subdomain` — pojedyncza subdomena * - `catalog` — ścieżka/katalog * - `url` — dokładny adres URL */ fetch_mode: 'topLevelDomain' | 'subdomain' | 'catalog' | 'url'; /** * Identyfikator kraju (baza Google). Opcjonalne — walidator tej akcji nie wymusza pola; * gdy pominięte, backend stosuje domyślny kraj (PL). * @default 1 */ country_id?: number; /** * **Wymagane** (walidator: requirePresence). Identyfikator słowa kluczowego, dla którego * mają zostać zwrócone wyniki AI Overviews. * Identyfikatory fraz uzyskasz np. z akcji `getKeywords` tego kontrolera. */ keyword_id: number; /** * Filtrowanie wyników AI Overviews frazy. Dozwolone klucze m.in.: `domain`, `pos`, `url`, `title`, * `organic_pos`, `organic_url`, `faq_pos`, `faq_url`, `is_translated`, `is_fragment`, * `fragment_text`, `is_analyzed_domain_and_url_match`. * ⚠️ **Uwaga:** backend AIO działa na ClickHouse i — inaczej niż raporty Bazy słów — na * nieznany `key` **nie** zwraca `418` (klucz bywa po cichu pomijany). Efektu filtra nie * wsparcie potwierdzone w implementacji endpointu. */ filtering?: Array<{ filters: Array<{ key: string; match?: 'gt' | 'gte' | 'lt' | 'lte' | 'eq'; value: string | number | Array; complement?: boolean }>; conjunction?: 'and' | 'or'; }>; /** * Numer strony wyników. Opcjonalne. * @default 1 */ page?: number; /** * Maksymalna liczba wyników na stronę. Opcjonalne. */ limit?: number; } export default AiOverviewsGetKeywordResultsRequest ``` > **Ostrzeżenie:** > Dozwolone wartości `fetch_mode` to dokładnie `['topLevelDomain', 'subdomain', 'catalog', 'url']` — przekazanie `domain` to częsty błąd i nie przechodzi walidacji. > **Ostrzeżenie:** > Domena bez danych AI Overviews zwraca `200` z **pustą tablicą** `data` — to nie błąd, tylko brak wyników dla tej domeny. ## Odpowiedź W przypadku powodzenia otrzymujesz `success: true`, `data` — tablicę wyników AI Overviews dla wskazanej frazy — oraz obiekt `pagination` z informacjami o stronicowaniu. Gdy dla danej domeny (lub frazy) brak danych AIO, `data` jest pustą tablicą (`[]`), a liczniki paginacji wskazują zero wyników. Tak właśnie odpowiedziała domena testowa `zalando.pl` — `200` z pustą tablicą. **Skrócona** ```json filename="przykładowa-odpowiedź (skrócona)" { "success": true, "data": [] } ``` **Pełna** ```json filename="przykładowa-odpowiedź (200)" { "success": true, "data": [], "pagination": { "page_count": 0, "current_page": 1, "has_next_page": false, "has_prev_page": false, "count": 0, "limit": 2 } } ``` > **Ostrzeżenie:** > Dla domen, które nie mają danych AI Overviews, `data` jest **pustą tablicą** — to nie błąd, tylko brak wyników dla tej domeny. Poniżej opisana jest koperta odpowiedzi. ### Struktura odpowiedzi ```ts type AiOverviewsKeywordResultsResponse = { /** Flaga przetworzenia żądania. Potwierdzone (`true`) w odpowiedzi `200`. */ success: boolean; /** * Tablica wyników AI Overviews dla wskazanej frazy. * Pusta tablica (`[]`), gdy domena nie ma danych AIO. */ data: unknown[]; /** Informacje o stronicowaniu wyników. Potwierdzone w odpowiedzi `200`. */ pagination: { /** Łączna liczba stron wyników. */ page_count: number; /** Numer bieżącej strony. */ current_page: number; /** Czy istnieje następna strona. */ has_next_page: boolean; /** Czy istnieje poprzednia strona. */ has_prev_page: boolean; /** Łączna liczba wyników. */ count: number; /** Limit wyników na stronę (odzwierciedla przekazany `limit`). */ limit: number; }; } export default AiOverviewsKeywordResultsResponse ``` ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data, unreachable_data, timeout, database, unknown */ type: string; message: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > Błędy walidacji (np. brak wymaganego `domain` lub `fetch_mode`, albo nieprawidłowa wartość `fetch_mode`) zwracane są ze statusem **`418`** i kopertą `invalid_data` z mapą `params` wskazującą pole i naruszoną regułę. ## Powiązane akcje Wszystkie poniższe akcje są przestarzałe: - `getStatistics` — zbiorcze statystyki AI Overviews dla domeny - `getKeywords` — słowa kluczowe wywołujące AI Overviews - `getDistribution` — rozkład obecności domeny w AI Overviews - `getCompetitors` — konkurenci domeny w AI Overviews - `getKeywordResults` — wyniki AI Overviews dla pojedynczej frazy (ta strona) - `getKeywordsIntents` — agregacja fraz AIO według intencji - `getOpportunities` — frazy-szanse: domena rankuje organicznie, ale nie jest w AIO Aktualne odpowiedniki raportów AI Overviews **per projekt** znajdziesz w module **Monitoring** (`rank_tracker/reports/ai_overviews`). --- # AI Overviews: intencje (`getKeywordsIntents`) **`GET /api/visibility_analysis/reports/ai_overviews/getKeywordsIntents`** > **Ostrzeżenie:** > **Endpoint przestarzały.** Działa, ale może nie być aktywnie utrzymywany i w przyszłości zostać wycofany lub zastąpiony. Planuj integrację z ostrożnością. Aktualne odpowiedniki raportów AI Overviews per projekt znajdziesz w module **Monitoring** (`rank_tracker/reports/ai_overviews`). Zwraca **agregację fraz wywołujących AI Overviews (AIO) według intencji wyszukiwania**. Wymiar agregacji wybierasz parametrem `aggregation_type` — dostępnych jest pięć ujęć: intencja podstawowa (`primary_intent_value`), intencja główna (`main_intent_value`), typ akcji (`action_type_value`), etap ścieżki zakupowej (`journey_stage_value`) i charakter treści (`content_timeliness_value`). Raport pozwala zrozumieć, jakie intencje użytkowników dominują wśród zapytań wyzwalających bloki AIO dla analizowanej domeny. --- ## Żądanie `GET` `/api/visibility_analysis/reports/ai_overviews/getKeywordsIntents` Parametry są odczytywane z **query string**. Nagłówki: `Authorization: Bearer `. ### Struktura żądania **Podstawowy** ```jsonc filename="żądanie-podstawowe.jsonc" // Query string parameters { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "country_id": 200, "aggregation_type": "primary_intent_value" } // GET /api/visibility_analysis/reports/ai_overviews/getKeywordsIntents?domain=zalando.pl&fetch_mode=topLevelDomain&country_id=200&aggregation_type=primary_intent_value ``` **Rozszerzony** ```jsonc filename="żądanie-rozszerzone.jsonc" // Query string parameters — agregacja według intencji głównej { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "country_id": 200, "aggregation_type": "main_intent_value" } // GET /api/visibility_analysis/reports/ai_overviews/getKeywordsIntents?domain=zalando.pl&fetch_mode=topLevelDomain&country_id=200&aggregation_type=main_intent_value ``` **cURL** ```bash curl --location --request GET 'https://api.senuto.com/api/visibility_analysis/reports/ai_overviews/getKeywordsIntents?domain=zalando.pl&fetch_mode=topLevelDomain&country_id=200&aggregation_type=primary_intent_value' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' ``` ### Parametry ```ts type AiOverviewsGetKeywordsIntentsRequest = { /** * **Wymagane**. Domena, subdomena, katalog lub URL do analizy — interpretowane zgodnie z `fetch_mode`. * Bez schematu/protokołu — np. `zalando.pl`. */ domain: string; /** * **Wymagane**. Sposób interpretacji `domain`. * Uwaga: `domain` NIE jest prawidłową wartością — dla całej domeny użyj `topLevelDomain`. * - `topLevelDomain` — cała domena (najczęstszy przypadek) * - `subdomain` — pojedyncza subdomena * - `catalog` — ścieżka/katalog * - `url` — dokładny adres URL */ fetch_mode: 'topLevelDomain' | 'subdomain' | 'catalog' | 'url'; /** * Identyfikator kraju (baza Google). Opcjonalne — walidator tej akcji nie wymusza pola; * gdy pominięte, backend stosuje domyślny kraj (PL). * @default 1 */ country_id?: number; /** * **Wymagane**. Wymiar agregacji intencji (walidator `inList`, pięć wartości): * - `primary_intent_value` — intencja podstawowa (Know / Do / Website / …) * - `main_intent_value` — intencja główna (INFORMATIONAL / TRANSACTIONAL / …) * - `action_type_value` — typ akcji (BUY / COMPARE / RESEARCH / TROUBLESHOOT) * - `journey_stage_value` — etap ścieżki zakupowej (TOFU / MOFU / BOFU) * - `content_timeliness_value` — charakter treści (Evergreen / Seasonal) * Inna wartość → `418` z komunikatem "Invalid aggregation type. Allowed values: ...". */ aggregation_type: 'primary_intent_value' | 'main_intent_value' | 'action_type_value' | 'journey_stage_value' | 'content_timeliness_value'; } export default AiOverviewsGetKeywordsIntentsRequest ``` > **Ostrzeżenie:** > Dozwolone wartości `fetch_mode` to dokładnie `['topLevelDomain', 'subdomain', 'catalog', 'url']` — przekazanie `domain` to częsty błąd i nie przechodzi walidacji. > **Ostrzeżenie:** > Ta akcja jest wywoływana metodą **`GET`** i odczytuje dane z **query string** — parametry przesyłaj w adresie URL, nie w treści JSON. Poza standardowym zestawem `domain` + `fetch_mode` + `country_id` wymagany jest parametr **`aggregation_type`**, walidowany regułą `inList` — dozwolonych jest **pięć** wartości (patrz wyżej). Nieprawidłowa wartość kończy się statusem **`418`** z komunikatem `"Invalid aggregation type. Allowed values: ..."`. Ta akcja **nie ma paginacji**. > **Błąd:** > **Dane intencji są wyłącznie w bazie PL 2.0 — wywołuj z `country_id: 200`.** Bez tego (czyli na > domyślnej bazie `country_id: 1`) raport zwraca `200` z pustą tablicą **dla każdej domeny**, co > wygląda jak brak danych AIO, a jest tylko złą bazą. Zwalidowane 2026-08-02: `medonet.pl` > z `country_id: 1` → `[]`, to samo żądanie z `country_id: 200` → 68 tys. fraz w rozbiciu na intencje. ## Odpowiedź W przypadku powodzenia otrzymujesz `success: true` oraz `data` — tablicę wyników agregacji fraz AIO według wybranego wymiaru intencji, posortowaną malejąco po liczbie fraz. Ta akcja **nie zwraca obiektu `pagination`**. Pusta tablica oznacza brak danych AIO dla tej domeny w tej bazie — najczęściej to po prostu `country_id` inne niż `200` (patrz ostrzeżenie wyżej). ```json filename="przykładowa-odpowiedź (zwalidowana 2026-08-02: zalando.pl, country_id=200)" { "success": true, "data": [ { "name": "Know", "count": "1854", "percentage": 61.07 }, { "name": "Do", "count": "800", "percentage": 26.35 }, { "name": "Website", "count": "329", "percentage": 10.84 }, { "name": "Know Simple", "count": "13", "percentage": 0.43 }, { "name": "Visit-in-Person", "count": "11", "percentage": 0.36 } ] } ``` ### Struktura odpowiedzi ```ts type AiOverviewsKeywordsIntentsResponse = { /** Flaga przetworzenia żądania. Potwierdzone (`true`) w odpowiedzi `200`. */ success: boolean; /** Agregacja fraz AIO według wymiaru z `aggregation_type`, malejąco po `count`. */ data: { /** Wartość wymiaru — zestaw zależy od `aggregation_type` (tabela niżej). */ name: string; /** Liczba fraz w tej kategorii. Uwaga: przychodzi jako STRING. */ count: string; /** Udział procentowy w całości (liczba, np. 61.07). */ percentage: number; }[]; } export default AiOverviewsKeywordsIntentsResponse ``` ### Jakie wartości zwraca każdy wymiar Zaobserwowane na produkcji 2026-08-02 (`country_id: 200`, domeny `zalando.pl`, `medonet.pl`, `senuto.com`). Nazwy pochodzą wprost z danych — API nie udostępnia słownika, więc lista jest tym, co realnie wystąpiło, a nie zamkniętym enumem: | `aggregation_type` | Wartości `name` | | -------------------------- | --------------------------------------------------------------------- | | `primary_intent_value` | `Know`, `Know Simple`, `Do`, `Website`, `Visit-in-Person`, `Unknown` | | `main_intent_value` | `INFORMATIONAL`, `TRANSACTIONAL`, `NAVIGATIONAL`, `LOCAL` | | `action_type_value` | `RESEARCH`, `BUY`, `COMPARE`, `TROUBLESHOOT`, `Unknown`, `""` (puste) | | `journey_stage_value` | `TOFU`, `MOFU`, `BOFU`, `Unknown` | | `content_timeliness_value` | `Evergreen`, `Seasonal` | > **Informacja:** > **Gdzie jeszcze w API znajdziesz intencje.** Ten raport podaje je **zbiorczo dla domeny** i tylko dla fraz > wywołujących AI Overviews. Drugie miejsce to **Content Planner** — `szczegóły grupy planów` > i `szczegóły planu` zwracają `main_intent` oraz rozbicie > `intents[]` dla fraz w grupie. W **Bazie słów kluczowych nie ma endpointu z intencją pojedynczej frazy** — > jeśli tego szukasz, dziś API tego nie udostępnia. > > ### Trzy słowniki intencji — jak się mapują > > Uwaga: **te same pojęcia mają różne nazwy** w Content Plannerze, w tym raporcie i w interfejsie aplikacji. > Zestawienie (aplikacja sprawdzona 2026-08-12 w filtrze „Intencje" Content Plannera): > > | Znaczenie | Content Planner — API | Ten raport (`main_intent_value`) | Aplikacja Senuto | > | -------------------------- | --------------------- | -------------------------------- | ----------------- | > | szukanie informacji | `research` | `INFORMATIONAL` | **Research** | > | zamiar zakupu / działania | `transactional` | `TRANSACTIONAL` | **Transactional** | > | konkretna marka lub serwis | `navigational` | `NAVIGATIONAL` | **Navigational** | > | intencja lokalna | `local` | `LOCAL` | **Local** | > > Content Planner zwraca małe litery, ten raport — wersaliki. Filtr w aplikacji zna dokładnie te cztery > wartości. Wymiar `primary_intent_value` (Know / Do / Website / Visit-in-Person) jest **osobną, bardziej > szczegółową klasyfikacją** w duchu taksonomii Google i nie ma odpowiednika w powyższej czwórce. > > `primary_intent_value` to klasyfikacja w duchu taksonomii Google (Know / Do / Website / Visit-in-Person), > a `main_intent_value` — klasyczny podział na intencje informacyjną, transakcyjną, nawigacyjną i lokalną. > Wartości `Unknown` oraz pusty string występują realnie w danych — obsłuż je w kliencie. > Nazewnictwo w aplikacji Senuto może być przetłumaczone; API zwraca zawsze formy powyżej. ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data, unreachable_data, timeout, database, unknown */ type: string; message: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > Nieprawidłowa wartość `aggregation_type` (spoza listy `primary_intent_value`, `main_intent_value`, `action_type_value`) zwraca **`418`** z komunikatem `"Invalid aggregation type. Allowed values: ..."`. Pozostałe błędy walidacji (brak `domain`, `fetch_mode` czy `country_id`, nieprawidłowy `fetch_mode`) również zwracają `418` z kopertą `invalid_data`. Pamiętaj, że parametry muszą trafić do **query string** — ta akcja jest wywoływana metodą `GET`. ## Powiązane akcje Wszystkie poniższe akcje są przestarzałe: - `getStatistics` — zbiorcze statystyki AI Overviews dla domeny - `getKeywords` — słowa kluczowe wywołujące AI Overviews - `getDistribution` — rozkład obecności domeny w AI Overviews - `getCompetitors` — konkurenci domeny w AI Overviews - `getKeywordResults` — wyniki AI Overviews dla pojedynczej frazy - `getKeywordsIntents` — agregacja fraz AIO według intencji (ta strona) - `getOpportunities` — frazy-szanse: domena rankuje organicznie, ale nie jest w AIO Aktualne odpowiedniki raportów AI Overviews **per projekt** znajdziesz w module **Monitoring** (`rank_tracker/reports/ai_overviews`). --- # AI Overviews: szanse (`getOpportunities`) **`POST /api/visibility_analysis/reports/ai_overviews/getOpportunities`** > **Ostrzeżenie:** > **Endpoint przestarzały.** Działa, ale może nie być aktywnie utrzymywany i w przyszłości zostać wycofany lub zastąpiony. Planuj integrację z ostrożnością. Aktualne odpowiedniki raportów AI Overviews per projekt znajdziesz w module **Monitoring** (`rank_tracker/reports/ai_overviews`). Zwraca listę **fraz-szans w AI Overviews (AIO)** — zgodnie z adnotacją w kodzie źródłowym są to słowa kluczowe, dla których **istnieje blok AI Overview** i analizowana domena **rankuje organicznie**, ale **nie pojawia się w samym AIO** jako cytowane źródło. To naturalni kandydaci do optymalizacji treści: domena ma już autorytet organiczny dla tych zapytań, a mimo to nie jest obecna w odpowiedziach generowanych przez AI. --- ## Żądanie `POST` `/api/visibility_analysis/reports/ai_overviews/getOpportunities` Parametry przesyłasz w **treści żądania jako JSON**. Nagłówki: `Authorization: Bearer ` oraz `Content-Type: application/json`. ### Struktura żądania **Podstawowy** ```jsonc filename="żądanie-podstawowe.jsonc" { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "country_id": 1 } ``` **Rozszerzony** ```jsonc filename="żądanie-rozszerzone.jsonc" { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "country_id": 1, "page": 1, "limit": 2 } ``` **cURL** ```bash curl --location --request POST 'https://api.senuto.com/api/visibility_analysis/reports/ai_overviews/getOpportunities' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' \ --header 'Content-Type: application/json' \ --data '{"domain":"zalando.pl","fetch_mode":"topLevelDomain","country_id":1,"limit":2}' ``` ### Parametry ```ts type AiOverviewsGetOpportunitiesRequest = { /** * **Wymagane**. Domena, subdomena, katalog lub URL do analizy — interpretowane zgodnie z `fetch_mode`. * Bez schematu/protokołu — np. `zalando.pl`. */ domain: string; /** * **Wymagane**. Sposób interpretacji `domain`. * Uwaga: `domain` NIE jest prawidłową wartością — dla całej domeny użyj `topLevelDomain`. * - `topLevelDomain` — cała domena (najczęstszy przypadek) * - `subdomain` — pojedyncza subdomena * - `catalog` — ścieżka/katalog * - `url` — dokładny adres URL */ fetch_mode: 'topLevelDomain' | 'subdomain' | 'catalog' | 'url'; /** * Identyfikator kraju (baza Google). Opcjonalne — walidator tej akcji nie wymusza pola; * gdy pominięte, backend stosuje domyślny kraj (PL). * @default 1 */ country_id?: number; /** * Filtrowanie listy szans (AI Overviews). Dozwolone klucze m.in.: `keywords`, `searches`, * `organic_pos`, `organic_url`, `organic_vis`, `aio_positions_count`, `aio_domains_count`, * `aio_length`, `aio_text` oraz `intentions.primary_intent` / `intentions.main_intent` / * `intentions.action_type` / `intentions.journey_stage` / `intentions.content_timeliness`. * ⚠️ **Uwaga:** backend AIO działa na ClickHouse i — inaczej niż raporty Bazy słów — na * nieznany `key` **nie** zwraca `418` (klucz bywa po cichu pomijany). Efektu filtra nie * wsparcie potwierdzone w implementacji endpointu. */ filtering?: Array<{ filters: Array<{ key: string; match?: 'gt' | 'gte' | 'lt' | 'lte' | 'eq'; value: string | number | Array; complement?: boolean }>; conjunction?: 'and' | 'or'; }>; /** * Numer strony wyników. Opcjonalne. * @default 1 */ page?: number; /** * Maksymalna liczba wyników na stronę. Opcjonalne. */ limit?: number; } export default AiOverviewsGetOpportunitiesRequest ``` > **Ostrzeżenie:** > Dozwolone wartości `fetch_mode` to dokładnie `['topLevelDomain', 'subdomain', 'catalog', 'url']` — przekazanie `domain` to częsty błąd i nie przechodzi walidacji. > **Ostrzeżenie:** > Domena bez danych AI Overviews zwraca `200` z **pustą tablicą** `data` — to nie błąd, tylko brak wyników dla tej domeny. ## Odpowiedź W przypadku powodzenia otrzymujesz `success: true`, `data` — tablicę fraz-szans AI Overviews — oraz obiekt `pagination` z informacjami o stronicowaniu. Gdy dla danej domeny brak danych AIO, `data` jest pustą tablicą (`[]`), a liczniki paginacji wskazują zero wyników. Tak właśnie odpowiedziała domena testowa `zalando.pl` — `200` z pustą tablicą. **Skrócona** ```json filename="przykładowa-odpowiedź (skrócona)" { "success": true, "data": [] } ``` **Pełna** ```json filename="przykładowa-odpowiedź (200)" { "success": true, "data": [], "pagination": { "page_count": 0, "current_page": 1, "has_next_page": false, "has_prev_page": false, "count": 0, "limit": 2 } } ``` > **Ostrzeżenie:** > Dla domen, które nie mają danych AI Overviews, `data` jest **pustą tablicą** — to nie błąd, tylko brak wyników dla tej domeny. Poniżej opisana jest koperta odpowiedzi. ### Struktura odpowiedzi ```ts type AiOverviewsOpportunitiesResponse = { /** Flaga przetworzenia żądania. Potwierdzone (`true`) w odpowiedzi `200`. */ success: boolean; /** * Tablica fraz-szans: dla frazy istnieje AI Overview i domena rankuje organicznie, * ale nie jest obecna w AIO. Pusta tablica (`[]`), gdy domena nie ma danych AIO — */ data: unknown[]; /** Informacje o stronicowaniu wyników. Potwierdzone w odpowiedzi `200`. */ pagination: { /** Łączna liczba stron wyników. */ page_count: number; /** Numer bieżącej strony. */ current_page: number; /** Czy istnieje następna strona. */ has_next_page: boolean; /** Czy istnieje poprzednia strona. */ has_prev_page: boolean; /** Łączna liczba wyników. */ count: number; /** Limit wyników na stronę (odzwierciedla przekazany `limit`). */ limit: number; }; } export default AiOverviewsOpportunitiesResponse ``` ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data, unreachable_data, timeout, database, unknown */ type: string; message: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > Błędy walidacji (np. brak wymaganego `domain` lub `fetch_mode`, albo nieprawidłowa wartość `fetch_mode`) zwracane są ze statusem **`418`** i kopertą `invalid_data` z mapą `params` wskazującą pole i naruszoną regułę. ## Powiązane akcje Wszystkie poniższe akcje są przestarzałe: - `getStatistics` — zbiorcze statystyki AI Overviews dla domeny - `getKeywords` — słowa kluczowe wywołujące AI Overviews - `getDistribution` — rozkład obecności domeny w AI Overviews - `getCompetitors` — konkurenci domeny w AI Overviews - `getKeywordResults` — wyniki AI Overviews dla pojedynczej frazy - `getKeywordsIntents` — agregacja fraz AIO według intencji - `getOpportunities` — frazy-szanse: domena rankuje organicznie, ale nie jest w AIO (ta strona) Aktualne odpowiedniki raportów AI Overviews **per projekt** znajdziesz w module **Monitoring** (`rank_tracker/reports/ai_overviews`). --- # Dane rankingu (`getRankingData`) **`POST /api/visibility_analysis/tools/domains_ranking/getRankingData`** Zwraca globalny ranking domen według widoczności w wynikach wyszukiwania — obejmuje **całą bazę kraju** (dla Polski ponad 13,6 mln domen, `count: 13608048` przy `match_mode: "main_domain"`). Ranking można zawęzić do wybranych kategorii tematycznych (`categories_ranking`) albo sfokusować na konkretnej domenie (`domain`). Wynik jest stronicowany. | Domena | Kategoria | Udział | Ranking · bieżący | Ranking · poprz. | Ranking · zmiana | Ranking · % | Widoczność · bieżąca | Widoczność · poprz. | Widoczność · zmiana | Widoczność · % | Widoczność · current (alias) | Widoczność · previous (alias) | TOP10 · bieżąca | TOP10 · poprz. | TOP10 · zmiana | TOP10 · % | TOP10 · current (alias) | TOP10 · previous (alias) | Ranga domeny · bieżąca | Ranga domeny · poprz. | Ranga domeny · zmiana | Ranga domeny · % | Technologie | Nowe technologie | Grupy technologii | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | facebook.com | Main Ranking | 1 | 1 | 1 | 0 | 0 | 354010944 | 355756117.4 | -1745173.4 | -0.0049 | 354010944 | 355756117.4 | 4633561 | 4628947 | 4614 | 0.001 | 4633561 | 4628947 | 1 | 1 | 0 | 0 | ["ActiveCampaign","HSTS","HTTP/3"] | [] | ["Email","Marketing automation","Miscellaneous","Security"] | | youtube.com | Main Ranking | 1 | 2 | 2 | 0 | 0 | 342211072 | 342656302.28 | -445230.28 | -0.0013 | 342211072 | 342656302.28 | 4783588 | 4776688 | 6900 | 0.0014 | 4783588 | 4776688 | 2 | 2 | 0 | 0 | ["ActiveCampaign","HSTS","HTTP/3"] | [] | ["Email","Marketing automation","Miscellaneous","Security"] | _match_mode: main_domain, limit: 2 — czoło globalnego rankingu domen dla Polski. Wszystkie adresowalne pola wiersza (brak pól o zmiennych kluczach). Uwaga: w `visibility` i `top10` pola `current`/`previous` to duplikaty (aliasy) `recent_value`/`older_value`._ > **Błąd:** > **Zwalidowany błąd API:** wywołanie **bez** `match_mode` i **bez** `categories_ranking` (np. samo `{"limit": 2}`) kończy się **`HTTP 400` z surową stroną HTML błędu** (nieobsłużony wyjątek), a nie JSON-ową kopertą `{"success": false, ...}`. W praktyce zawsze podawaj `match_mode: "main_domain"` (ranking główny) **albo** `categories_ranking: []` (ranking w obrębie kategorii). --- ## Żądanie `POST` `/api/visibility_analysis/tools/domains_ranking/getRankingData` Nagłówki: `Authorization: Bearer `, `Content-Type: application/json`. ### Struktura żądania **Podstawowy** ```jsonc filename="żądanie-podstawowe.jsonc" { "match_mode": "main_domain", "limit": 2 } ``` **Rozszerzony** ```jsonc filename="żądanie-rozszerzone.jsonc" { // ranking w obrębie kategorii zamiast rankingu głównego "categories_ranking": [1], "limit": 2, "page": 1 } ``` **cURL** ```bash curl --location --request POST 'https://api.senuto.com/api/visibility_analysis/tools/domains_ranking/getRankingData' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' \ --data-raw '{ "match_mode": "main_domain", "limit": 2 }' ``` ### Parametry ```ts type GetRankingDataRequest = { /** * Tryb dopasowania domen w rankingu głównym (klasa `MatchMode`): * `"main_domain"` — domeny główne, `"domain"` — domeny, `"subdomains"` — subdomeny. * **Uwaga:** żądanie bez `match_mode` i bez `categories_ranking` kończy się `HTTP 400` * z surową stroną HTML błędu (nieobsłużony wyjątek) — podaj jedno z tych pól. */ match_mode?: "main_domain" | "domain" | "subdomains"; /** * Tablica id kategorii — zamiast rankingu głównego zwracany jest ranking domen * **w obrębie wskazanych kategorii** (pole `category` w wierszach przyjmuje nazwę kategorii, * np. `"Sztuka i rozrywka"` dla `[1]`). Nazwę odpowiadającą podanemu id zwraca pole `category` * w wierszach odpowiedzi. */ categories_ranking?: number[]; /** * Opcjonalna domena — fokus rankingu na wskazanej domenie. */ domain?: string; /** * Sortowanie wyników. Kształt parametru nie został jeszcze zweryfikowany na żywo * (kontroler przekazuje go wprost do komponentu) — zostanie doprecyzowany. */ order?: unknown; /** * ID kraju bazy Senuto. * @default 1 (Polska) */ country_id?: number; /** * Rozmiar strony paginacji. Odbija się w `pagination.limit`. * @default 10 */ limit?: number; /** * Numer strony paginacji. Paginacja działa na całej bazie * (`count: 13608048` przy `match_mode: "main_domain"`). * @default 1 */ page?: number; } export default GetRankingDataRequest ``` > **Ostrzeżenie:** > Endpoint obsługuje metodę **`POST`** z parametrami w **ciele żądania** (JSON). Endpoint **nie waliduje nazw pól** — parametry są czytane wprost z ciała żądania, więc literówki w nazwach pól nie zwrócą błędu walidacji, tylko zostaną po cichu zignorowane. `country_id` jest opcjonalne (domyślnie Polska, `country_id = 1`). ## Odpowiedź Po pomyślnym żądaniu otrzymujesz `data` (tablicę wierszy rankingu) oraz `pagination`. Każdy wiersz opisuje jedną domenę: pole `category` przyjmuje wartość `"Main Ranking"` dla rankingu głównego albo nazwę kategorii (np. `"Sztuka i rozrywka"` przy `categories_ranking: [1]` — ranking otwiera wtedy `youtube.com`). Statystyki obejmują pozycję w rankingu (`rank`, `domain_rank`), widoczność (`visibility`) i liczbę fraz w TOP10 (`top10`), każdorazowo z wartością bieżącą, poprzednią, różnicą i zmianą procentową. Wiersz zawiera też listy technologii wykrytych na domenie. **Skrócona** ```json filename="przykładowa-odpowiedź (skrócona)" { "success": true, "data": [ { "domain": "facebook.com", "category": "Main Ranking", "share": 1, "statistics": { "rank": { "recent_value": 1, "older_value": 1, "diff": 0, "percent": 0 }, "visibility": { "recent_value": 354010944, "older_value": 355756117.4, "diff": -1745173.4, "percent": -0.0049, "current": 354010944, "previous": 355756117.4 }, "top10": { "recent_value": 4633561, "older_value": 4628947, "diff": 4614, "percent": 0.001, "current": 4633561, "previous": 4628947 }, "domain_rank": { "current": 1, "previous": 1, "diff": 0, "percent": 0 } }, "technologies": ["ActiveCampaign", "HSTS", "HTTP/3"], "technologies_new": [], "technologies_groups": ["Email", "Marketing automation", "Miscellaneous", "Security"] } ], "pagination": { "page_count": 6804024, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 13608048, "limit": 2 } } ``` **Pełna** ```json filename="przykładowa-odpowiedź (200)" { "success": true, "data": [ { "domain": "facebook.com", "category": "Main Ranking", "share": 1, "statistics": { "rank": { "recent_value": 1, "older_value": 1, "diff": 0, "percent": 0 }, "visibility": { "recent_value": 354010944, "older_value": 355756117.4, "diff": -1745173.4, "percent": -0.0049, "current": 354010944, "previous": 355756117.4 }, "top10": { "recent_value": 4633561, "older_value": 4628947, "diff": 4614, "percent": 0.001, "current": 4633561, "previous": 4628947 }, "domain_rank": { "current": 1, "previous": 1, "diff": 0, "percent": 0 } }, "technologies": ["ActiveCampaign", "HSTS", "HTTP/3"], "technologies_new": [], "technologies_groups": ["Email", "Marketing automation", "Miscellaneous", "Security"] }, { "domain": "youtube.com", "category": "Main Ranking", "share": 1, "statistics": { "rank": { "recent_value": 2, "older_value": 2, "diff": 0, "percent": 0 }, "visibility": { "recent_value": 342211072, "older_value": 342656302.28, "diff": -445230.28, "percent": -0.0013, "current": 342211072, "previous": 342656302.28 }, "top10": { "recent_value": 4783588, "older_value": 4776688, "diff": 6900, "percent": 0.0014, "current": 4783588, "previous": 4776688 }, "domain_rank": { "current": 2, "previous": 2, "diff": 0, "percent": 0 } }, "technologies": ["ActiveCampaign", "HSTS", "HTTP/3"], "technologies_new": [], "technologies_groups": ["Email", "Marketing automation", "Miscellaneous", "Security"] } ], "pagination": { "page_count": 6804024, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 13608048, "limit": 2 } } ``` ### Struktura odpowiedzi ```ts type GetRankingDataResponse = { /** `true` przy powodzeniu; przy błędzie `false` i koperta z `error` */ success: boolean; /** Wiersze rankingu domen */ data: RankingRow[]; /** Metadane paginacji */ pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; /** Łączna liczba domen w rankingu (13 608 048 przy `match_mode: "main_domain"`) */ count: number; limit: number; }; } type RankingRow = { /** Nazwa domeny */ domain: string; /** `"Main Ranking"` dla rankingu głównego albo nazwa kategorii (np. `"Sztuka i rozrywka"`) przy `categories_ranking` */ category: string; share: number; statistics: { /** Pozycja w rankingu (bieżąca vs poprzednia) */ rank: StatDiff; /** Widoczność domeny. **Uwaga:** `current`/`previous` to duplikaty (aliasy) `recent_value`/`older_value` */ visibility: StatDiff & { current: number; previous: number }; /** Liczba fraz w TOP10. **Uwaga:** `current`/`previous` to duplikaty (aliasy) `recent_value`/`older_value` */ top10: StatDiff & { current: number; previous: number }; /** Pozycja rankingowa domeny */ domain_rank: { current: number; previous: number; diff: number; percent: number }; }; /** Technologie wykryte na domenie */ technologies: string[]; /** Nowo wykryte technologie */ technologies_new: string[]; /** Grupy technologii */ technologies_groups: string[]; } type StatDiff = { /** Wartość bieżąca */ recent_value: number; /** Wartość poprzednia */ older_value: number; /** Różnica bieżąca − poprzednia */ diff: number; /** Zmiana procentowa (ułamek, np. `-0.0049`) */ percent: number; } export default GetRankingDataResponse ``` ## Błędy > **Błąd:** > Żądanie bez `match_mode` i bez `categories_ranking` (np. samo `{"limit": 2}`) zwraca **`HTTP 400` z surową stroną HTML** (nieobsłużony wyjątek serwera) — odpowiedź **nie jest** JSON-em, więc parser JSON po stronie klienta rzuci własny błąd. Zabezpiecz integrację: sprawdzaj `Content-Type` odpowiedzi i zawsze przekazuj `match_mode` albo `categories_ranking`. Kontroler nie ma walidatora pól, więc nie zwraca typowej JSON-owej koperty `invalid_data` — nieznane lub błędnie nazwane pola są ignorowane, a brak pól wymaganych funkcjonalnie kończy się opisanym wyżej `400` z HTML-em. ## Powiązane akcje - [Analiza widoczności — przegląd modułu](/modules/visibility_analysis) — pozostałe raporty widoczności domeny. --- # Sekcje domeny (`getSections`) **`POST /api/visibility_analysis/reports/sections/getSections`** Zwraca widoczność domeny zagregowaną po sekcjach URL (katalogach). Każdy wiersz odpowiada jednej sekcji (`url_section`) i zawiera liczbę fraz oraz statystyki `visibility`, `top3`, `top10` i `top50` w formacie `{current, previous, diff, percent, history}`. Użyj tej akcji, aby zobaczyć, które części serwisu (np. `zalando.pl/obuwie/`) generują widoczność w wynikach wyszukiwania. | Sekcja URL | Frazy | Udział wid. % | Widoczność | Widoczność poprz. | Widoczność Δ | Widoczność % | TOP3 | TOP3 poprz. | TOP3 Δ | TOP3 % | TOP10 | TOP10 poprz. | TOP10 Δ | TOP10 % | TOP50 | TOP50 poprz. | TOP50 Δ | TOP50 % | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | zalando.pl/ | 26672 | 0 | 1143250.66 | 1136367.71 | 6882.95 | 0.0061 | 7886 | 7848 | 38 | 0.0048 | 26672 | 26441 | 231 | 0.0087 | 73530 | 72995 | 535 | 0.0073 | | zalando.pl/obuwie/ | 7012 | 0 | 471266.74 | 444738.36 | 26528.38 | 0.0596 | 2727 | 2732 | -5 | -0.0018 | 7012 | 7023 | -11 | -0.0016 | 14666 | 14641 | 25 | 0.0017 | _zalando.pl — sekcje URL o najwyższej widoczności. Wszystkie pola wiersza (poza mapami historii `statistics.*.history` o kluczach-datach — są w JSON; w tej odpowiedzi mają wartość null)._ --- ## Żądanie `POST` `/api/visibility_analysis/reports/sections/getSections` Nagłówki: `Authorization: Bearer `, `Content-Type: application/json`. ### Struktura żądania **Podstawowy** ```jsonc filename="żądanie-podstawowe.jsonc" { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "country_id": 1 } ``` **Rozszerzony** ```jsonc filename="żądanie-rozszerzone.jsonc" { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "country_id": 1, "limit": 2, "page": 1 } ``` **cURL** ```bash curl --location --request POST 'https://api.senuto.com/api/visibility_analysis/reports/sections/getSections' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' \ --data-raw '{ "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "country_id": 1, "limit": 2 }' ``` ### Parametry ```ts type SectionsGetSectionsRequest = { /** * **Wymagane**. Domena, subdomena, katalog lub URL do analizy — interpretowane zgodnie z `fetch_mode`. */ domain: string; /** * **Wymagane**. Sposób interpretacji `domain`. * - `topLevelDomain` — cała domena * - `subdomain` — pojedyncza subdomena * - `catalog` — ścieżka/katalog * - `url` — dokładny URL * * Wartość `"domain"` nie istnieje — użyj `topLevelDomain`. */ fetch_mode: 'topLevelDomain' | 'subdomain' | 'catalog' | 'url'; /** * **Wymagane**. Id kraju (bazy danych). Polska = `1`. Nieznana wartość zwraca `418` z komunikatem `Unknown country_id`. */ country_id: number; /** * Liczba wierszy na stronę. * @default 10 */ limit?: number; /** * Numer strony. * @default 1 */ page?: number; } export default SectionsGetSectionsRequest ``` > **Ostrzeżenie:** > Ten endpoint **nie obsługuje** parametrów `order` ani `filtering`. Jeśli je wyślesz, API zwróci `200`, ale zostaną **zignorowane** — wyniki wracają w kolejności domyślnej. Zweryfikowane na prod (`dir: 'asc'` i `'desc'` dają identyczną kolejność) — endpoint ich nie odczytuje. W przeciwieństwie do raportu `positions`, sekcje nie mają sortowania ani filtrowania po stronie API — jedyne parametry sterujące to `limit` i `page`. > **Ostrzeżenie:** > Trzy parametry są **wymagane** (walidator `SectionsValidator`): **`domain`**, **`fetch_mode`** oraz **`country_id`**. Dozwolone wartości `fetch_mode` to `topLevelDomain`, `subdomain`, `catalog` i `url` — wartość `"domain"` **nie istnieje**. Nieznane `country_id` zwraca `418` z komunikatem `"Unknown country_id"`. ## Odpowiedź W przypadku powodzenia otrzymujesz `data` (tablicę sekcji URL) oraz `pagination`. Dla `zalando.pl` API zwróciło łącznie 4528 sekcji. **Skrócona** ```json filename="przykładowa-odpowiedź (skrócona)" { "success": true, "data": [ { "url_section": "zalando.pl/", "keywords_count": 26672, "statistics": { "visibility": { "current": 1143250.66 } /* … */ } }, { "url_section": "zalando.pl/obuwie/", "keywords_count": 7012, "statistics": { "visibility": { "current": 471266.74 } /* … */ } } ], "pagination": { "page_count": 2264, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 4528, "limit": 2 } } ``` **Pełna** ```json filename="przykładowa-odpowiedź (200)" { "success": true, "data": [ { "url_section": "zalando.pl/", "keywords_count": 26672, "visibility_percent": 0, "statistics": { "visibility": { "current": 1143250.66, "previous": 1136367.71, "diff": 6882.95, "percent": 0.0061, "history": null }, "top3": { "current": 7886, "previous": 7848, "diff": 38, "percent": 0.0048, "history": null }, "top10": { "current": 26672, "previous": 26441, "diff": 231, "percent": 0.0087, "history": null }, "top50": { "current": 73530, "previous": 72995, "diff": 535, "percent": 0.0073, "history": null } } }, { "url_section": "zalando.pl/obuwie/", "keywords_count": 7012, "visibility_percent": 0, "statistics": { "visibility": { "current": 471266.74, "previous": 444738.36, "diff": 26528.38, "percent": 0.0596, "history": null }, "top3": { "current": 2727, "previous": 2732, "diff": -5, "percent": -0.0018, "history": null }, "top10": { "current": 7012, "previous": 7023, "diff": -11, "percent": -0.0016, "history": null }, "top50": { "current": 14666, "previous": 14641, "diff": 25, "percent": 0.0017, "history": null } } } ], "pagination": { "page_count": 2264, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 4528, "limit": 2 } } ``` ### Struktura odpowiedzi ```ts type SectionsGetSectionsResponse = { /** `true` przy powodzeniu; przy błędzie `false` i koperta z `error` */ success: boolean; /** Zwrócone wiersze z sekcjami URL */ data: SectionRow[]; /** Metadane paginacji */ pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; limit: number; }; } type SectionRow = { /** Sekcja URL (katalog) domeny, np. "zalando.pl/obuwie/" */ url_section: string; /** Liczba fraz w TOP10 przypisanych do sekcji */ keywords_count: number; /** Procentowy udział sekcji w widoczności */ visibility_percent: number; statistics: { visibility: StatEntry; top3: StatEntry; top10: StatEntry; top50: StatEntry; }; } type StatEntry = { current: number; previous: number; diff: number; percent: number; history: null; } export default SectionsGetSectionsResponse ``` ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data, database, timeout, unknown */ type: string; message: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > **`418`** zwracany jest również dla błędów walidacji — nie tylko przy ograniczeniu liczby żądań (rate limiting). Brak wymaganego pola zwraca `invalid_data` z mapą `params`, a nieznane `country_id` zwraca komunikat `"Unknown country_id"`. ## Powiązane akcje - `getSections` — widoczność zagregowana po sekcjach URL (katalogach) domeny (ta strona) - [`getSubdomains`](/modules/visibility_analysis/va-sections-getSubdomains) — widoczność per subdomena (dodatkowo `visibility_coverage`) - [`getUrls`](/modules/visibility_analysis/va-sections-getUrls) — widoczność per pojedynczy adres URL --- # Sekcje i URL-e: subdomeny (`getSubdomains`) **`POST /api/visibility_analysis/reports/sections/getSubdomains`** Zwraca widoczność domeny w rozbiciu na subdomeny. Każdy wiersz odpowiada jednej subdomenie (pole `domain` zawiera jej nazwę) i obok statystyk `visibility`, `top3`, `top10` i `top50` zawiera **dodatkowo** `visibility_coverage` — pokrycie widoczności całej domeny przez daną subdomenę. Użyj tej akcji, aby sprawdzić, jak widoczność rozkłada się między subdomeny serwisu. | Domena | Frazy | Udział wid. % | Widoczność | Widoczność poprz. | Widoczność Δ | Widoczność % | TOP3 | TOP3 poprz. | TOP3 Δ | TOP3 % | TOP10 | TOP10 poprz. | TOP10 Δ | TOP10 % | TOP50 | TOP50 poprz. | TOP50 Δ | TOP50 % | Pokrycie wid. | Pokrycie wid. poprz. | Pokrycie wid. Δ | Pokrycie wid. % | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | zalando.pl | 114658 | 100 | 5434165.18 | 5380498.4 | 53666.78 | 0.01 | 46869 | 46517 | 352 | 0.0076 | 114658 | 114147 | 511 | 0.0045 | 288521 | 288895 | -374 | -0.0013 | 100 | 100 | 0 | 0 | _zalando.pl — domena nie ma innych subdomen, stąd jeden wiersz z pełnym pokryciem widoczności. Wszystkie pola wiersza (poza mapami historii `statistics.*.history` o kluczach-datach — są w JSON; w tej odpowiedzi mają wartość null)._ --- ## Żądanie `POST` `/api/visibility_analysis/reports/sections/getSubdomains` Nagłówki: `Authorization: Bearer `, `Content-Type: application/json`. ### Struktura żądania **Podstawowy** ```jsonc filename="żądanie-podstawowe.jsonc" { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "country_id": 1 } ``` **Rozszerzony** ```jsonc filename="żądanie-rozszerzone.jsonc" { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "country_id": 1, "limit": 2, "page": 1 } ``` **cURL** ```bash curl --location --request POST 'https://api.senuto.com/api/visibility_analysis/reports/sections/getSubdomains' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' \ --data-raw '{ "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "country_id": 1, "limit": 2 }' ``` ### Parametry ```ts type SectionsGetSubdomainsRequest = { /** * **Wymagane**. Domena, subdomena, katalog lub URL do analizy — interpretowane zgodnie z `fetch_mode`. */ domain: string; /** * **Wymagane**. Sposób interpretacji `domain`. * - `topLevelDomain` — cała domena * - `subdomain` — pojedyncza subdomena * - `catalog` — ścieżka/katalog * - `url` — dokładny URL * * Wartość `"domain"` nie istnieje — użyj `topLevelDomain`. */ fetch_mode: 'topLevelDomain' | 'subdomain' | 'catalog' | 'url'; /** * **Wymagane**. Id kraju (bazy danych). Polska = `1`. Nieznana wartość zwraca `418` z komunikatem `Unknown country_id`. */ country_id: number; /** * Liczba wierszy na stronę. * @default 10 */ limit?: number; /** * Numer strony. * @default 1 */ page?: number; } export default SectionsGetSubdomainsRequest ``` > **Ostrzeżenie:** > Ten endpoint **nie obsługuje** parametrów `order` ani `filtering`. Jeśli je wyślesz, API zwróci `200`, ale zostaną **zignorowane** — wyniki wracają w kolejności domyślnej. Zweryfikowane na prod — endpoint ich nie odczytuje. W przeciwieństwie do raportu `positions`, sekcje nie mają sortowania ani filtrowania po stronie API — jedyne parametry sterujące to `limit` i `page`. > **Ostrzeżenie:** > Trzy parametry są **wymagane** (walidator `SectionsValidator`): **`domain`**, **`fetch_mode`** oraz **`country_id`**. Dozwolone wartości `fetch_mode` to `topLevelDomain`, `subdomain`, `catalog` i `url` — wartość `"domain"` **nie istnieje**. Nieznane `country_id` zwraca `418` z komunikatem `"Unknown country_id"`. ## Odpowiedź W przypadku powodzenia otrzymujesz `data` (tablicę subdomen) oraz `pagination`. Dla `zalando.pl` API zwróciło jeden wiersz — domena nie ma innych subdomen, stąd `visibility_percent` i `visibility_coverage` wynoszą `100`. **Skrócona** ```json filename="przykładowa-odpowiedź (skrócona)" { "success": true, "data": [ { "domain": "zalando.pl", "keywords_count": 114658, "visibility_percent": 100, "statistics": { "visibility": { "current": 5434165.18 }, "visibility_coverage": { "current": 100 } /* … */ } } ], "pagination": { "page_count": 1, "current_page": 1, "has_next_page": false, "has_prev_page": false, "count": 1, "limit": 2 } } ``` **Pełna** ```json filename="przykładowa-odpowiedź (200)" { "success": true, "data": [ { "domain": "zalando.pl", "keywords_count": 114658, "visibility_percent": 100, "statistics": { "visibility": { "current": 5434165.18, "previous": 5380498.4, "diff": 53666.78, "percent": 0.01, "history": null }, "top3": { "current": 46869, "previous": 46517, "diff": 352, "percent": 0.0076, "history": null }, "top10": { "current": 114658, "previous": 114147, "diff": 511, "percent": 0.0045, "history": null }, "top50": { "current": 288521, "previous": 288895, "diff": -374, "percent": -0.0013, "history": null }, "visibility_coverage": { "current": 100, "previous": 100, "diff": 0, "percent": 0, "history": null } } } ], "pagination": { "page_count": 1, "current_page": 1, "has_next_page": false, "has_prev_page": false, "count": 1, "limit": 2 } } ``` ### Struktura odpowiedzi ```ts type SectionsGetSubdomainsResponse = { /** `true` przy powodzeniu; przy błędzie `false` i koperta z `error` */ success: boolean; /** Zwrócone wiersze z subdomenami */ data: SubdomainRow[]; /** Metadane paginacji */ pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; limit: number; }; } type SubdomainRow = { /** Nazwa subdomeny, np. "zalando.pl" */ domain: string; /** Liczba fraz w TOP10 przypisanych do subdomeny */ keywords_count: number; /** Procentowy udział subdomeny w widoczności domeny */ visibility_percent: number; statistics: { visibility: StatEntry; top3: StatEntry; top10: StatEntry; top50: StatEntry; /** Pokrycie widoczności domeny przez subdomenę (tylko w tej akcji) */ visibility_coverage: StatEntry; }; } type StatEntry = { current: number; previous: number; diff: number; percent: number; history: null; } export default SectionsGetSubdomainsResponse ``` ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data, database, timeout, unknown */ type: string; message: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > **`418`** zwracany jest również dla błędów walidacji — nie tylko przy ograniczeniu liczby żądań (rate limiting). Brak wymaganego pola zwraca `invalid_data` z mapą `params`, a nieznane `country_id` zwraca komunikat `"Unknown country_id"`. ## Powiązane akcje - [`getSections`](/modules/visibility_analysis/va-sections-getSections) — widoczność zagregowana po sekcjach URL (katalogach) domeny - `getSubdomains` — widoczność per subdomena (ta strona) - [`getUrls`](/modules/visibility_analysis/va-sections-getUrls) — widoczność per pojedynczy adres URL --- # Adresy URL (`getUrls`) **`POST /api/visibility_analysis/reports/sections/getUrls`** Zwraca widoczność domeny w rozbiciu na pojedyncze adresy URL. Każdy wiersz odpowiada jednemu adresowi (pole `url`) i zawiera liczbę fraz oraz statystyki `visibility`, `top3`, `top10` i `top50` w formacie `{current, previous, diff, percent, history}`. Użyj tej akcji, aby znaleźć konkretne podstrony generujące widoczność w wynikach wyszukiwania. | URL | Frazy | Udział wid. % | Widoczność | Widoczność poprz. | Widoczność Δ | Widoczność % | TOP3 | TOP3 poprz. | TOP3 Δ | TOP3 % | TOP10 | TOP10 poprz. | TOP10 Δ | TOP10 % | TOP50 | TOP50 poprz. | TOP50 Δ | TOP50 % | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | zalando.pl/ | 226 | 0 | 963824.36 | 963804.6 | 19.76 | 0 | 119 | 120 | -1 | -0.0083 | 226 | 224 | 2 | 0.0089 | 3035 | 2933 | 102 | 0.0348 | | zalando.pl/bershka/ | 70 | 0 | 311746.17 | 311727.33 | 18.83 | 0.0001 | 28 | 27 | 1 | 0.037 | 70 | 72 | -2 | -0.0278 | 150 | 150 | 0 | 0 | _zalando.pl — adresy URL o najwyższej widoczności. Wszystkie pola wiersza (poza mapami historii `statistics.*.history` o kluczach-datach — są w JSON; w tej odpowiedzi mają wartość null)._ --- ## Żądanie `POST` `/api/visibility_analysis/reports/sections/getUrls` Nagłówki: `Authorization: Bearer `, `Content-Type: application/json`. ### Struktura żądania **Podstawowy** ```jsonc filename="żądanie-podstawowe.jsonc" { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "country_id": 1 } ``` **Rozszerzony** ```jsonc filename="żądanie-rozszerzone.jsonc" { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "country_id": 1, "limit": 2, "page": 1 } ``` **cURL** ```bash curl --location --request POST 'https://api.senuto.com/api/visibility_analysis/reports/sections/getUrls' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' \ --data-raw '{ "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "country_id": 1, "limit": 2 }' ``` ### Parametry ```ts type SectionsGetUrlsRequest = { /** * **Wymagane**. Domena, subdomena, katalog lub URL do analizy — interpretowane zgodnie z `fetch_mode`. */ domain: string; /** * **Wymagane**. Sposób interpretacji `domain`. * - `topLevelDomain` — cała domena * - `subdomain` — pojedyncza subdomena * - `catalog` — ścieżka/katalog * - `url` — dokładny URL * * Wartość `"domain"` nie istnieje — użyj `topLevelDomain`. */ fetch_mode: 'topLevelDomain' | 'subdomain' | 'catalog' | 'url'; /** * **Wymagane**. Id kraju (bazy danych). Polska = `1`. Nieznana wartość zwraca `418` z komunikatem `Unknown country_id`. */ country_id: number; /** * Liczba wierszy na stronę. * @default 10 */ limit?: number; /** * Numer strony. * @default 1 */ page?: number; } export default SectionsGetUrlsRequest ``` > **Ostrzeżenie:** > Ten endpoint **nie obsługuje** parametrów `order` ani `filtering`. Jeśli je wyślesz, API zwróci `200`, ale zostaną **zignorowane** — wyniki wracają w kolejności domyślnej. Zweryfikowane na prod — endpoint ich nie odczytuje. W przeciwieństwie do raportu `positions`, sekcje nie mają sortowania ani filtrowania po stronie API — jedyne parametry sterujące to `limit` i `page`. > **Ostrzeżenie:** > Trzy parametry są **wymagane** (walidator `SectionsValidator`): **`domain`**, **`fetch_mode`** oraz **`country_id`**. Dozwolone wartości `fetch_mode` to `topLevelDomain`, `subdomain`, `catalog` i `url` — wartość `"domain"` **nie istnieje**. Nieznane `country_id` zwraca `418` z komunikatem `"Unknown country_id"`. ## Odpowiedź W przypadku powodzenia otrzymujesz `data` (tablicę adresów URL) oraz `pagination`. Dla `zalando.pl` API zwróciło łącznie 72 773 adresy. **Skrócona** ```json filename="przykładowa-odpowiedź (skrócona)" { "success": true, "data": [ { "url": "zalando.pl/", "keywords_count": 226, "statistics": { "visibility": { "current": 963824.36 } /* … */ } }, { "url": "zalando.pl/bershka/", "keywords_count": 70, "statistics": { "visibility": { "current": 311746.17 } /* … */ } } ], "pagination": { "page_count": 36387, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 72773, "limit": 2 } } ``` **Pełna** ```json filename="przykładowa-odpowiedź (200)" { "success": true, "data": [ { "url": "zalando.pl/", "keywords_count": 226, "visibility_percent": 0, "statistics": { "visibility": { "current": 963824.36, "previous": 963804.6, "diff": 19.76, "percent": 0, "history": null }, "top3": { "current": 119, "previous": 120, "diff": -1, "percent": -0.0083, "history": null }, "top10": { "current": 226, "previous": 224, "diff": 2, "percent": 0.0089, "history": null }, "top50": { "current": 3035, "previous": 2933, "diff": 102, "percent": 0.0348, "history": null } } }, { "url": "zalando.pl/bershka/", "keywords_count": 70, "visibility_percent": 0, "statistics": { "visibility": { "current": 311746.17, "previous": 311727.33, "diff": 18.83, "percent": 0.0001, "history": null }, "top3": { "current": 28, "previous": 27, "diff": 1, "percent": 0.037, "history": null }, "top10": { "current": 70, "previous": 72, "diff": -2, "percent": -0.0278, "history": null }, "top50": { "current": 150, "previous": 150, "diff": 0, "percent": 0, "history": null } } } ], "pagination": { "page_count": 36387, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 72773, "limit": 2 } } ``` ### Struktura odpowiedzi ```ts type SectionsGetUrlsResponse = { /** `true` przy powodzeniu; przy błędzie `false` i koperta z `error` */ success: boolean; /** Zwrócone wiersze z adresami URL */ data: UrlRow[]; /** Metadane paginacji */ pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; limit: number; }; } type UrlRow = { /** Pojedynczy adres URL, np. "zalando.pl/bershka/" */ url: string; /** Liczba fraz w TOP10 przypisanych do adresu */ keywords_count: number; /** Procentowy udział adresu w widoczności */ visibility_percent: number; statistics: { visibility: StatEntry; top3: StatEntry; top10: StatEntry; top50: StatEntry; }; } type StatEntry = { current: number; previous: number; diff: number; percent: number; history: null; } export default SectionsGetUrlsResponse ``` ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data, database, timeout, unknown */ type: string; message: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > **`418`** zwracany jest również dla błędów walidacji — nie tylko przy ograniczeniu liczby żądań (rate limiting). Brak wymaganego pola zwraca `invalid_data` z mapą `params`, a nieznane `country_id` zwraca komunikat `"Unknown country_id"`. ## Powiązane akcje - [`getSections`](/modules/visibility_analysis/va-sections-getSections) — widoczność zagregowana po sekcjach URL (katalogach) domeny - [`getSubdomains`](/modules/visibility_analysis/va-sections-getSubdomains) — widoczność per subdomena (dodatkowo `visibility_coverage`) - `getUrls` — widoczność per pojedynczy adres URL (ta strona) --- # Kanibalizacja: frazy (`getKeywords`) **`POST /api/visibility_analysis/reports/cannibalization/getKeywords`** Zwraca frazy dotknięte **kanibalizacją** — czyli takie, na które rankuje więcej niż jeden adres domeny lub dla których adres rankujący zmienia się między pomiarami. Każdy wiersz zawiera frazę wraz ze statystykami (pozycja, widoczność, CPC, liczba wyszukiwań, trudność, trendy, snippety SERP) oraz parę adresów `url.current` / `url.previous` — jeśli adresy się różnią, mamy do czynienia z kanibalizacją. | Fraza | ID frazy | KID | Domena | Liczba słów | Marka | Pozycja | Pozycja poprz. | Zmiana pozycji | Wzrosty | Spadki | Bez zmian | Widoczność | Widoczność poprz. | Δ widoczności | Widoczność % | CPC | Wyszukiwania/mies. | Trudność | Trend (12 mies.) | Szczyt trendu | URL bieżący | URL poprzedni | Snippety SERP | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | korektor maybelline | 4663 | 000fd51efa78d65196f81fd56938c4ad | zalando.pl | 2 | | 27 | 10 | 17 | 0 | 0 | 0 | 0 | 44.66 | -44.66 | -1 | 1.41 | 2900 | 47 | [3600,2900,2400,2400,2400,2400,2900,2900,3600,3600,3600,5400] | [true] | zalando.pl/maybelline-new-york-instant-concealer-korektor-mj331e00a-s16.html | zalando.pl/maybelline-new-york-instant-anti-age-eraser-color-corrector-concealer-korektor-lila-mj331e06e-i11.html | ["image_thumbs","people_also_ask"] | | trzewiki adidas | 5283 | 00120febf81775e90ad212e342df6450 | zalando.pl | 2 | | 17 | 3 | 14 | 0 | 0 | 0 | 0 | 5.38 | -5.38 | -1 | 0 | 50 | 48 | [20,10,10,20,70,170,110,40,70,50,20,10] | [true,true,true] | zalando.pl/obuwie-meskie-trzewiki/adidas/ | zalando.pl/obuwie-meskie-trzewiki-sznurowane/adidas/ | ["image_thumbs"] | _zalando.pl (limit: 2) — frazy z kanibalizacją (różne url.current i url.previous). Wszystkie adresowalne pola wiersza (pominięto mapę historii pozycji `statistics.position.history` — ma zmienne klucze-daty; jest w JSON-ie i sekcji „Struktura odpowiedzi”)._ --- ## Żądanie `POST` `/api/visibility_analysis/reports/cannibalization/getKeywords` Nagłówki: `Authorization: Bearer `, `Content-Type: application/json`. ### Struktura żądania **Podstawowy** ```jsonc filename="żądanie-podstawowe.jsonc" { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "country_id": 1 } ``` **Rozszerzony** ```jsonc filename="żądanie-rozszerzone.jsonc" { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "country_id": 1, "limit": 2, "page": 1 } ``` **cURL** ```bash curl --location --request POST 'https://api.senuto.com/api/visibility_analysis/reports/cannibalization/getKeywords' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' \ --data-raw '{ "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "country_id": 1, "limit": 2 }' ``` ### Parametry ```ts type CannibalizationGetKeywordsRequest = { /** * **Wymagane**. Domena, subdomena, katalog lub URL do analizy — interpretowane zgodnie z `fetch_mode`. */ domain: string; /** * **Wymagane**. Sposób interpretacji `domain`. * - `topLevelDomain` — cała domena (najczęstszy przypadek) * - `subdomain` — pojedyncza subdomena * - `catalog` — ścieżka/katalog * - `url` — dokładny URL */ fetch_mode: 'topLevelDomain' | 'subdomain' | 'catalog' | 'url'; /** * **Wymagane**. Id kraju (bazy danych). Polska = `1`. Nieznana wartość → `418` z komunikatem `Unknown country_id`. */ country_id: number; /** * Liczba wierszy na stronę. * @default 10 */ limit?: number; /** * Numer strony. * @default 1 */ page?: number; /** * Okres odniesienia do porównania (baza wykrywania zmian kanibalizacji). * Dozwolone: `week_ago_monday` (domyślny), `last_monday`, `yesterday`. */ days_compare_mode?: 'week_ago_monday' | 'last_monday' | 'yesterday'; /** * Grupy filtrów. Dozwolone pola m.in.: `keyword`, `statistics.searches.current`, * `statistics.cpc.current`, `statistics.difficulty.current`, * `statistics.url.current` (nowy URL), `statistics.url.previous` (stary URL). * Pusta tablica = brak filtrowania. */ filtering?: unknown[]; /** * Sortowanie — **pojedynczy obiekt** `{ prop, dir }`. Dozwolone `prop`: * `keyword`, `statistics.position.current|previous|diff`, * `statistics.visibility.current|previous|diff`, `statistics.searches.current`, * `statistics.cpc.current`, `statistics.difficulty.current`, * `statistics.url.is_change`, `words_count`. Nieznany klucz → sort domyślny * (`keyword_id` rosnąco). */ order?: { prop: string; dir: 'asc' | 'desc' }; } export default CannibalizationGetKeywordsRequest ``` > **Ostrzeżenie:** > Trzy parametry są **wymagane**: **`domain`**, **`fetch_mode`** oraz **`country_id`**. Nieznane `country_id` zwraca `418` z komunikatem `Unknown country_id`. Parametry `page` i `limit` są opcjonalne (domyślnie `1` / `10`). Endpoint **obsługuje** `order` (obiekt `{ prop, dir }`) oraz `filtering` — **zweryfikowane na prod** (`order` po `statistics.searches.current` realnie zmienia kolejność asc/desc). To odróżnia ten raport od raportów sekcji (`sections/*`), które `order`/`filtering` ignorują. ## Odpowiedź W przypadku powodzenia otrzymujesz `data` (tablicę wierszy z frazami dotkniętymi kanibalizacją) oraz `pagination`. Kluczowa jest para `statistics.url.current` / `statistics.url.previous` — **różne adresy oznaczają kanibalizację**. **Skrócona** ```json filename="przykładowa-odpowiedź (skrócona)" { "success": true, "data": [ { "keyword": "korektor maybelline", "keyword_id": 4663, "statistics": { "position": { "current": 27, "previous": 10 }, "url": { "current": "zalando.pl/maybelline-new-york-instant-concealer-korektor-mj331e00a-s16.html", "previous": "zalando.pl/maybelline-new-york-instant-anti-age-eraser-color-corrector-concealer-korektor-lila-mj331e06e-i11.html" } /* … */ } } ], "pagination": { "page_count": 1429, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 2857, "limit": 2 } } ``` **Pełna** ```json filename="przykładowa-odpowiedź (200)" { "success": true, "data": [ { "keyword": "korektor maybelline", "keyword_id": 4663, "kid": "000fd51efa78d65196f81fd56938c4ad", "domain": "zalando.pl", "words_count": 2, "brand": "", "statistics": { "position": { "current": 27, "previous": 10, "diff": 17, "changes": { "wins": 0, "losses": 0, "no_changes": 0 }, "history": { "2026-06-25": 27 } }, "visibility": { "current": 0, "previous": 44.66, "diff": -44.66, "percent": -1, "history": null }, "cpc": { "current": 1.41 }, "searches": { "current": 2900 }, "difficulty": { "current": 47 }, "trends": { "history": [3600, 2900, 2400, 2400, 2400, 2400, 2900, 2900, 3600, 3600, 3600, 5400], "peak": [true] }, "url": { "current": "zalando.pl/maybelline-new-york-instant-concealer-korektor-mj331e00a-s16.html", "previous": "zalando.pl/maybelline-new-york-instant-anti-age-eraser-color-corrector-concealer-korektor-lila-mj331e06e-i11.html" }, "snippets": { "current": ["image_thumbs", "people_also_ask"] } } }, { "keyword": "trzewiki adidas", "keyword_id": 5283, "kid": "00120febf81775e90ad212e342df6450", "domain": "zalando.pl", "words_count": 2, "brand": "", "statistics": { "position": { "current": 17, "previous": 3, "diff": 14, "changes": { "wins": 0, "losses": 0, "no_changes": 0 }, "history": { "2026-06-26": 17 } }, "visibility": { "current": 0, "previous": 5.38, "diff": -5.38, "percent": -1, "history": null }, "cpc": { "current": 0 }, "searches": { "current": 50 }, "difficulty": { "current": 48 }, "trends": { "history": [20, 10, 10, 20, 70, 170, 110, 40, 70, 50, 20, 10], "peak": [true, true, true] }, "url": { "current": "zalando.pl/obuwie-meskie-trzewiki/adidas/", "previous": "zalando.pl/obuwie-meskie-trzewiki-sznurowane/adidas/" }, "snippets": { "current": ["image_thumbs"] } } } ], "pagination": { "page_count": 1429, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 2857, "limit": 2 } } ``` ### Struktura odpowiedzi ```ts type CannibalizationKeywordsResponse = { /** `true` przy powodzeniu; przy błędzie `false` i koperta z `error` */ success: boolean; /** Zwrócone wiersze z frazami dotkniętymi kanibalizacją */ data: CannibalizationKeywordRow[]; /** Metadane paginacji */ pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; limit: number; }; } type CannibalizationKeywordRow = { /** Treść frazy kluczowej */ keyword: string; keyword_id: number; kid: string; domain: string; /** Liczba słów we frazie */ words_count: number; /** Marka przypisana do frazy (może być pustym łańcuchem) */ brand: string; statistics: { /** Pozycja bieżąca i poprzednia; `history` to mapa data → pozycja */ position: { current: number; previous: number; diff: number; changes: { wins: number; losses: number; no_changes: number }; history: Record }; visibility: { current: number; previous: number; diff: number; percent: number; history: null }; cpc: { current: number }; searches: { current: number }; difficulty: { current: number }; /** `history` — 12 miesięcy wyszukiwań; `peak` — tablica wartości logicznych */ trends: { history: number[]; peak: boolean[] }; /** Różne adresy `current` i `previous` oznaczają kanibalizację */ url: { current: string; previous: string }; snippets: { current: string[] }; }; } export default CannibalizationKeywordsResponse ``` ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data, database, timeout, unknown */ type: string; message: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > **`418`** zwracany jest także dla błędów walidacji. Nieznane `country_id` → komunikat `Unknown country_id`. Pominięcie któregokolwiek wymaganego pola (`domain`, `fetch_mode`, `country_id`) skutkuje błędem walidacji. ## Powiązane akcje - `getKeywords` — frazy dotknięte kanibalizacją wraz ze statystykami (ta strona) - [`getSections`](/modules/visibility_analysis/va-cannibalization-getSections) — liczba skanibalizowanych fraz w podziale na sekcje URL --- # Kanibalizacja: sekcje (`getSections`) **`POST /api/visibility_analysis/reports/cannibalization/getSections`** Zwraca zbiorczy widok raportu kanibalizacji w podziale na **sekcje URL**: dla każdej sekcji serwisu (np. `zalando.pl/obuwie-damskie/`) podawana jest liczba skanibalizowanych fraz. Użyj tej akcji, aby szybko zlokalizować obszary serwisu najbardziej dotknięte kanibalizacją, a następnie przejść do szczegółów przez `getKeywords`. | Sekcja URL | Skanibalizowane frazy | | --- | --- | | zalando.pl/ | 733 | | zalando.pl/obuwie-damskie/ | 138 | _zalando.pl (limit: 2) — sekcje URL z liczbą skanibalizowanych fraz. Wszystkie adresowalne pola wiersza (`keywords_sum` zwracane jest jako string)._ --- ## Żądanie `POST` `/api/visibility_analysis/reports/cannibalization/getSections` Nagłówki: `Authorization: Bearer `, `Content-Type: application/json`. ### Struktura żądania **Podstawowy** ```jsonc filename="żądanie-podstawowe.jsonc" { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "country_id": 1 } ``` **Rozszerzony** ```jsonc filename="żądanie-rozszerzone.jsonc" { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "country_id": 1, "limit": 2, "page": 1 } ``` **cURL** ```bash curl --location --request POST 'https://api.senuto.com/api/visibility_analysis/reports/cannibalization/getSections' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' \ --data-raw '{ "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "country_id": 1, "limit": 2 }' ``` ### Parametry ```ts type CannibalizationGetSectionsRequest = { /** * **Wymagane**. Domena, subdomena, katalog lub URL do analizy — interpretowane zgodnie z `fetch_mode`. */ domain: string; /** * **Wymagane**. Sposób interpretacji `domain`. * - `topLevelDomain` — cała domena (najczęstszy przypadek) * - `subdomain` — pojedyncza subdomena * - `catalog` — ścieżka/katalog * - `url` — dokładny URL */ fetch_mode: 'topLevelDomain' | 'subdomain' | 'catalog' | 'url'; /** * **Wymagane**. Id kraju (bazy danych). Polska = `1`. Nieznana wartość → `418` z komunikatem `Unknown country_id`. */ country_id: number; /** * Liczba wierszy na stronę. * @default 10 */ limit?: number; /** * Numer strony. * @default 1 */ page?: number; /** * Okres odniesienia do porównania (baza wykrywania zmian kanibalizacji). * **Walidowane** — dozwolone: `week_ago_monday` (domyślny), `last_monday`, `yesterday`; * inna wartość zwraca `418`. */ days_compare_mode?: 'week_ago_monday' | 'last_monday' | 'yesterday'; } export default CannibalizationGetSectionsRequest ``` > **Ostrzeżenie:** > W przeciwieństwie do `cannibalization/getKeywords`, ten endpoint **nie obsługuje** `order` ani `filtering` — kontroler ich nie odczytuje. Sterowanie wynikiem to `limit`/`page` oraz `days_compare_mode`. > **Ostrzeżenie:** > Trzy parametry są **wymagane**: **`domain`**, **`fetch_mode`** oraz **`country_id`**. Nieznane `country_id` zwraca `418` z komunikatem `Unknown country_id`. Parametry `page` i `limit` są opcjonalne (domyślnie `1` / `10`). Uwaga: pole **`keywords_sum` w odpowiedzi jest łańcuchem znaków** (np. `"733"`), nie liczbą — przed obliczeniami skonwertuj je na liczbę. ## Odpowiedź W przypadku powodzenia otrzymujesz `data` (tablicę sekcji URL z liczbą skanibalizowanych fraz) oraz `pagination`. Zwróć uwagę, że `keywords_sum` jest zwracane jako **łańcuch znaków**, a nie liczba. **Skrócona** ```json filename="przykładowa-odpowiedź (skrócona)" { "success": true, "data": [ { "section": "zalando.pl/", "keywords_sum": "733" } ], "pagination": { "count": 646, "limit": 2 /* … */ } } ``` **Pełna** ```json filename="przykładowa-odpowiedź (200)" { "success": true, "data": [ { "section": "zalando.pl/", "keywords_sum": "733" }, { "section": "zalando.pl/obuwie-damskie/", "keywords_sum": "138" } ], "pagination": { "page_count": 323, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 646, "limit": 2 } } ``` ### Struktura odpowiedzi ```ts type CannibalizationSectionsResponse = { /** `true` przy powodzeniu; przy błędzie `false` i koperta z `error` */ success: boolean; /** Zwrócone wiersze z sekcjami URL */ data: CannibalizationSectionRow[]; /** Metadane paginacji */ pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; limit: number; }; } type CannibalizationSectionRow = { /** Sekcja URL serwisu, np. "zalando.pl/obuwie-damskie/" */ section: string; /** * Liczba skanibalizowanych fraz w sekcji. * Uwaga: zwracana jako łańcuch znaków (np. "733"), nie liczba. */ keywords_sum: string; } export default CannibalizationSectionsResponse ``` ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data, database, timeout, unknown */ type: string; message: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > **`418`** zwracany jest także dla błędów walidacji. Nieznane `country_id` → komunikat `Unknown country_id`. Pominięcie któregokolwiek wymaganego pola (`domain`, `fetch_mode`, `country_id`) skutkuje błędem walidacji. ## Powiązane akcje - [`getKeywords`](/modules/visibility_analysis/va-cannibalization-getKeywords) — frazy dotknięte kanibalizacją wraz ze statystykami - `getSections` — liczba skanibalizowanych fraz w podziale na sekcje URL (ta strona) --- # Cechy fraz: wykres (`getCharacteristicsChart`) **`GET /api/visibility_analysis/reports/keywords/getCharacteristicsChart`** Zwraca dane do wykresu rozkładu fraz domeny według wybranej cechy (`characteristics`) — np. liczby słów we frazie, trendów, liczby wyszukiwań, trudności, parametrów frazy lub parametrów SERP. Wynik to tablica serii (po jednej na domenę): każda seria zawiera etykietę osi Y oraz mapę kubełek → liczba fraz. Opcjonalnie możesz dodać do 10 konkurentów, aby porównać rozkłady na tym samym wykresie. **Rozkład fraz wg długości (characteristics: words_count)** | Wartość | fraz | | --- | --- | | 1 | 7475 | | 2 | 77199 | | 3 | 119515 | | 4 | 62457 | | 5 | 17151 | | 6 | 3838 | | 7 | 981 | | 8 | 326 | | 9 | 121 | | 10 | 51 | _zalando.pl. Kubełek = liczba słów we frazie, wartość = liczba fraz o tej długości. Inne wartości `characteristics` dają inne kubełki._ --- ## Żądanie `GET` `/api/visibility_analysis/reports/keywords/getCharacteristicsChart` Nagłówki: `Authorization: Bearer `. Parametry przekazywane w query stringu. ### Struktura żądania **Podstawowy** ```text filename="query-string.txt" /api/visibility_analysis/reports/keywords/getCharacteristicsChart ?domain=zalando.pl &fetch_mode=topLevelDomain &country_id=1 &characteristics=words_count ``` **cURL** ```bash curl --location --request GET 'https://api.senuto.com/api/visibility_analysis/reports/keywords/getCharacteristicsChart?domain=zalando.pl&fetch_mode=topLevelDomain&country_id=1&characteristics=words_count' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' ``` ### Parametry ```ts type KeywordsGetCharacteristicsChartRequest = { /** * **Wymagane**. Domena, subdomena, katalog lub URL do analizy — interpretowane zgodnie z `fetch_mode`. */ domain: string; /** * **Wymagane**. Sposób interpretacji `domain`. * - `topLevelDomain` — cała domena * - `subdomain` — pojedyncza subdomena * - `catalog` — ścieżka/katalog * - `url` — dokładny URL */ fetch_mode: 'topLevelDomain' | 'subdomain' | 'catalog' | 'url'; /** * **Wymagane**. Id kraju (bazy danych). Polska = `1`. Nieznana wartość → `418` z komunikatem `Unknown country_id`. */ country_id: number; /** * **Wymagane**. Cecha fraz, według której budowany jest rozkład (walidator `inList`). */ characteristics: 'words_count' | 'trends' | 'searches' | 'difficulty' | 'keyword_params' | 'serp_params'; /** * Tablica maks. **10** domen konkurentów do porównania na tym samym wykresie. * W query stringu przekazywana jako JSON, np. `competitors=["allegro.pl","modivo.pl"]`. */ competitors?: string[]; } export default KeywordsGetCharacteristicsChartRequest ``` > **Ostrzeżenie:** > Cztery parametry są **wymagane**: **`domain`**, **`fetch_mode`**, **`country_id`** oraz **`characteristics`**. To endpoint **GET** — parametry przekazujesz w query stringu, a `competitors` (tablicę) serializujesz jako JSON. Nieznane `country_id` zwraca `418` z komunikatem `Unknown country_id`; `characteristics` spoza dozwolonej listy jest odrzucane przez walidator (`inList`). ## Odpowiedź W przypadku powodzenia otrzymujesz `data` — tablicę serii, po jednej na każdą domenę (analizowaną i ewentualnych konkurentów). Każda seria zawiera `domain`, etykietę osi Y `label_y` (zwracaną **po angielsku**, np. `"Number of keywords"`), flagę `is_main` (czy to domena główna z żądania) oraz obiekt `data` z mapą kubełek → liczba fraz pod kluczem równym wybranej wartości `characteristics`. Endpoint nie zwraca paginacji. **Skrócona** ```json filename="przykładowa-odpowiedź (skrócona)" { "success": true, "data": [ { "domain": "zalando.pl", "label_y": "Number of keywords", "is_main": true, "data": { "words_count": { "1": 7475, "2": 77199 /* … */ } } } ] } ``` **Pełna** ```json filename="przykładowa-odpowiedź (200)" { "success": true, "data": [ { "domain": "zalando.pl", "label_y": "Number of keywords", "is_main": true, "data": { "words_count": { "1": 7475, "2": 77199, "3": 119515, "4": 62457, "5": 17151, "6": 3838, "7": 981, "8": 326, "9": 121, "10": 51 } } } ] } ``` ### Struktura odpowiedzi ```ts type KeywordsCharacteristicsChartResponse = { /** `true` przy powodzeniu; przy błędzie `false` i koperta z `error` */ success: boolean; /** Serie wykresu — po jednej na domenę (główna + ewentualni konkurenci) */ data: CharacteristicsSeries[]; } type CharacteristicsSeries = { /** Domena, której dotyczy seria */ domain: string; /** Etykieta osi Y — zwracana po angielsku, np. "Number of keywords" */ label_y: string; /** Czy to domena główna z żądania (false dla konkurentów) */ is_main: boolean; /** * Dane serii pod kluczem równym wybranej wartości `characteristics` * (np. `words_count`) — mapa kubełek → liczba fraz. * Dla `words_count` kubełki to liczby słów "1".."10". */ data: Record>; } export default KeywordsCharacteristicsChartResponse ``` ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data, database, timeout, unknown */ type: string; message: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > **`418`** zwracany jest również dla błędów walidacji — nie tylko przy ograniczeniu liczby żądań (rate limiting). Brak wymaganego pola lub nieznane `country_id` → odpowiedź `invalid_data` (dla nieznanego kraju komunikat `Unknown country_id`). Wartość `characteristics` spoza listy `words_count | trends | searches | difficulty | keyword_params | serp_params` jest odrzucana przez walidator `inList`. ## Powiązane akcje - `getCharacteristicsChart` — rozkład fraz według cechy jako serie wykresu (ta strona) - `getCharacteristicsTable` — ten sam rozkład w formie tabelarycznej z paginacją i statystykami TOP3/TOP10/TOP50 oraz widocznością - W kontrolerze `domain_keywords` istnieją **przestarzałe (deprecated) aliasy** tych akcji — używaj ścieżek z kontrolera `keywords`. --- # Cechy fraz: tabela (`getCharacteristicsTable`) **`POST /api/visibility_analysis/reports/keywords/getCharacteristicsTable`** Zwraca rozkład fraz domeny według wybranej cechy (`characteristics`) w formie tabeli: każdy wiersz to jeden kubełek (np. liczba słów we frazie) wraz z liczbą fraz, liczbą pozycji w TOP3/TOP10/TOP50 oraz statystykami widoczności (suma, widoczność całej domeny, udział procentowy). To tabelaryczny odpowiednik akcji `getCharacteristicsChart`. Opcjonalnie możesz dodać do 10 konkurentów. Wyniki są paginowane po kubełkach — dla `words_count` łączny `count` wynosi 10. | Kubełek (liczba słów) | Liczba fraz | TOP3 | TOP10 | TOP50 | Suma widoczności | Widoczność domeny | Udział widoczności (%) | | --- | --- | --- | --- | --- | --- | --- | --- | | 1 | 7475 | 1092 | 2911 | 7475 | 2006144.9339999994 | 5380498.401999987 | 37.29 | | 2 | 77199 | 14111 | 31618 | 77199 | 2037763.912999993 | 5380498.401999987 | 37.87 | _zalando.pl (characteristics: words_count, limit: 2) — rozkład fraz wg liczby słów. Wszystkie adresowalne pola wiersza (`count` zwracane jest jako string)._ --- ## Żądanie `POST` `/api/visibility_analysis/reports/keywords/getCharacteristicsTable` Nagłówki: `Authorization: Bearer `, `Content-Type: application/json`. ### Struktura żądania **Podstawowy** ```jsonc filename="żądanie-podstawowe.jsonc" { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "country_id": 1, "characteristics": "words_count" } ``` **Rozszerzony** ```jsonc filename="żądanie-rozszerzone.jsonc" { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "country_id": 1, "characteristics": "words_count", "competitors": ["allegro.pl", "modivo.pl"], "page": 1, "limit": 2 } ``` **cURL** ```bash curl --location --request POST 'https://api.senuto.com/api/visibility_analysis/reports/keywords/getCharacteristicsTable' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' \ --data-raw '{ "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "country_id": 1, "characteristics": "words_count", "limit": 2 }' ``` ### Parametry ```ts type KeywordsGetCharacteristicsTableRequest = { /** * **Wymagane**. Domena, subdomena, katalog lub URL do analizy — interpretowane zgodnie z `fetch_mode`. */ domain: string; /** * **Wymagane**. Sposób interpretacji `domain`. * - `topLevelDomain` — cała domena * - `subdomain` — pojedyncza subdomena * - `catalog` — ścieżka/katalog * - `url` — dokładny URL */ fetch_mode: 'topLevelDomain' | 'subdomain' | 'catalog' | 'url'; /** * **Wymagane**. Id kraju (bazy danych). Polska = `1`. Nieznana wartość → `418` z komunikatem `Unknown country_id`. */ country_id: number; /** * **Wymagane**. Cecha fraz, według której budowany jest rozkład (walidator `inList`). */ characteristics: 'words_count' | 'trends' | 'searches' | 'difficulty' | 'keyword_params' | 'serp_params'; /** * Tablica maks. **10** domen konkurentów do porównania. */ competitors?: string[]; /** * Numer strony. Paginacja odbywa się po kubełkach (dla `words_count` łącznie 10 wierszy). * @default 1 */ page?: number; /** * Liczba wierszy (kubełków) na stronę. * @default 10 */ limit?: number; } export default KeywordsGetCharacteristicsTableRequest ``` > **Ostrzeżenie:** > Cztery parametry są **wymagane**: **`domain`**, **`fetch_mode`**, **`country_id`** oraz **`characteristics`**. To endpoint **POST** z ciałem JSON (`Content-Type: application/json`). Nieznane `country_id` zwraca `418` z komunikatem `Unknown country_id`. Uwaga: pole `count` w wierszach odpowiedzi jest zwracane jako **string**, a nie liczba. ## Odpowiedź W przypadku powodzenia otrzymujesz `data` (tablicę wierszy — po jednym na kubełek cechy) oraz `pagination`. Każdy wiersz zawiera wartość kubełka `key` (np. liczbę słów we frazie), liczbę fraz `count` (zwracaną jako **string**), liczbę fraz w TOP3/TOP10/TOP50 oraz statystyki widoczności: `visibility_sum` (suma widoczności fraz w kubełku), `visibility_domain` (łączna widoczność domeny) i `visibility_percent` (procentowy udział pozycji TOP w kubełku). **Skrócona** ```json filename="przykładowa-odpowiedź (skrócona)" { "success": true, "data": [ { "key": 1, "count": "7475", "top3": 1092, "top10": 2911, "top50": 7475, "visibility_percent": 37.29 /* … */ } ], "pagination": { "page_count": 5, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 10, "limit": 2 } } ``` **Pełna** ```json filename="przykładowa-odpowiedź (200)" { "success": true, "data": [ { "top3": 1092, "top10": 2911, "count": "7475", "visibility_sum": 2006144.9339999994, "key": 1, "top50": 7475, "visibility_domain": 5380498.401999987, "visibility_percent": 37.29 }, { "top3": 14111, "top10": 31618, "count": "77199", "visibility_sum": 2037763.912999993, "key": 2, "top50": 77199, "visibility_domain": 5380498.401999987, "visibility_percent": 37.87 } ], "pagination": { "page_count": 5, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 10, "limit": 2 } } ``` ### Struktura odpowiedzi ```ts type KeywordsCharacteristicsTableResponse = { /** `true` przy powodzeniu; przy błędzie `false` i koperta z `error` */ success: boolean; /** Wiersze tabeli — po jednym na kubełek cechy */ data: CharacteristicsRow[]; /** Metadane paginacji (paginacja po kubełkach) */ pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; limit: number; }; } type CharacteristicsRow = { /** Wartość kubełka — np. liczba słów we frazie dla `words_count` */ key: number; /** Liczba fraz w kubełku — uwaga: zwracana jako string */ count: string; /** Liczba fraz w TOP3 */ top3: number; /** Liczba fraz w TOP10 */ top10: number; /** Liczba fraz w TOP50 */ top50: number; /** Suma widoczności fraz w kubełku */ visibility_sum: number; /** Łączna widoczność domeny (taka sama we wszystkich wierszach) */ visibility_domain: number; /** Procentowy udział pozycji TOP w kubełku */ visibility_percent: number; } export default KeywordsCharacteristicsTableResponse ``` ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data, database, timeout, unknown */ type: string; message: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > **`418`** zwracany jest również dla błędów walidacji — nie tylko przy ograniczeniu liczby żądań (rate limiting). Brak wymaganego pola lub nieznane `country_id` → odpowiedź `invalid_data` (dla nieznanego kraju komunikat `Unknown country_id`). Wartość `characteristics` spoza listy `words_count | trends | searches | difficulty | keyword_params | serp_params` jest odrzucana przez walidator `inList`. ## Powiązane akcje - `getCharacteristicsTable` — rozkład fraz według cechy w formie tabeli z paginacją (ta strona) - `getCharacteristicsChart` — ten sam rozkład jako serie wykresu (bez paginacji, z etykietą osi Y) - W kontrolerze `domain_keywords` istnieją **przestarzałe (deprecated) aliasy** tych akcji — używaj ścieżek z kontrolera `keywords`. --- --- title: Monitoring sidebarTitle: Monitoring asIndexPage: true ----------------- # Monitoring (Rank Tracker) Moduł **Monitoringu** śledzi pozycje fraz w Twoich **projektach** Rank Tracker — w odróżnieniu od Analizy widoczności operuje na `project_id` (oraz opcjonalnie `group_id`, `competitor_id`, zakresie dat), a nie na `domain`/`fetch_mode`. Identyfikatory projektów pobierzesz z [listy moich projektów](/modules/rank_tracker/rt-projects-getMyActiveProjects) — to punkt startowy modułu; grupy w wybranym projekcie daje [`groups/list`](/modules/rank_tracker/rt-groups-list). Wspólne mechanizmy: [Filtrowanie (`filtering`)](/types/filter) · [Paginacja](/types/pagination) · [Błędy i status `418`](/types/errors). ## Pozycje - [Pozycje projektu (getData)](/modules/rank_tracker/rt-positions-getData) - [Średnie dla całego konta](/modules/rank_tracker/rt-positions-getAvgData) ## Słowa kluczowe - [Słowa kluczowe projektu](/modules/rank_tracker/rt-keywords-getProjectKeywords) · [słowa kluczowe grupy](/modules/rank_tracker/rt-keywords-getGroupKeywords) · [frazy z przypisaniami grup](/modules/rank_tracker/rt-keywords-getData) - [Statusy przetwarzania projektów](/modules/rank_tracker/rt-keywords-getProjectsStatus) - [Zrzut HTML SERP](/modules/rank_tracker/rt-keywords-getSerpHtml) ## Konkurenci - [Ranking konkurentów](/modules/rank_tracker/rt-competitors-getRanking) - [Macierz pozycji (fraza × domena)](/modules/rank_tracker/rt-competitors-getMatrix) - [Lista konkurentów projektu](/modules/rank_tracker/rt-competitors-list) ## Snippety SERP - [Statystyki snippetów](/modules/rank_tracker/rt-snippets-getStatistics) · [historia snippetów](/modules/rank_tracker/rt-snippets-getHistory) ## AI Overviews - [Statystyki](/modules/rank_tracker/rt-ai-overviews-getStatistics) · [słowa kluczowe](/modules/rank_tracker/rt-ai-overviews-getKeywords) · [rozkład pozycji](/modules/rank_tracker/rt-ai-overviews-getDistribution) · [konkurenci](/modules/rank_tracker/rt-ai-overviews-getCompetitors) · [szanse](/modules/rank_tracker/rt-ai-overviews-getOpportunities) · [szczegóły AIO](/modules/rank_tracker/rt-ai-overviews-getAioDetails) · [źródła](/modules/rank_tracker/rt-ai-overviews-getAioSources) ## Pozostałe - [Landing pages — statystyki URL-i](/modules/rank_tracker/rt-landing-pages-getUrlsStatistics) - [Lista grup projektu](/modules/rank_tracker/rt-groups-list) --- # Monitoring: lista moich projektów (`getMyActiveProjects`) **Punkt startowy całego modułu.** Prawie każdy raport Monitoringu wymaga `project_id` — tutaj pobierzesz identyfikatory wszystkich swoich aktywnych projektów. **`POST /api/rank_tracker/management/projects/getMyActiveProjects`** Przykładowe żądanie: ```json {} ``` Przykładowa odpowiedź: ```json { "success": true, "data": [ { "id": 161843, "domain": "enerwis.pl", "name": "enerwis.pl" }, { "id": 87944, "domain": "pies.pl", "name": "pies.pl" }, { "id": 87908, "domain": "selfguru.com", "name": "selfguru.com" } ] } ``` ## Żądanie `POST` `/api/rank_tracker/management/projects/getMyActiveProjects` ```jsonc filename="żądanie.jsonc" {} ``` ### Parametry Brak — endpoint nie przyjmuje żadnych parametrów. Zwraca **wszystkie** aktywne projekty konta, posortowane alfabetycznie po `name`, bez paginacji. > **Informacja:** > Widoczne są wyłącznie projekty, których właścicielem jest zalogowane konto, i tylko te o statusie > aktywnym. Projekty udostępnione Ci przez inne konto znajdziesz w > `projects/getSimpleActiveProjectsData` > (pole `is_owner`). ## Odpowiedź ```ts type RtActiveProject = { /** ID projektu — używane jako `project_id` w raportach Monitoringu */ id: number; /** Domena projektu */ domain: string; /** Nazwa nadana projektowi w aplikacji */ name: string; } export default RtActiveProject ``` ## Błędy Błędy tego endpointu przychodzą we [wspólnej kopercie ze statusem `418`](/types/errors). ## Powiązane akcje - [`positions/getData`](/modules/rank_tracker/rt-positions-getData) — pozycje fraz w projekcie (`project_id` stąd). - [`groups/list`](/modules/rank_tracker/rt-groups-list) — grupy fraz w wybranym projekcie. --- # Projekty z rozszerzonymi danymi (`getListWithExtendedData`) **`GET /api/rank_tracker/management/projects/getListWithExtendedData`** Zwraca **listę Twoich projektów Rank Trackera razem ze statystykami** — liczbą monitorowanych fraz, rozkładem pozycji (TOP3 / TOP10 / TOP50), średnią pozycją, widocznością organiczną oraz liczbą wzrostów i spadków. Jedno żądanie zastępuje pobranie listy projektów i osobnego raportu pozycji dla każdego z nich. Zakres obejmuje projekty własne **oraz udostępnione** Twojemu kontu. --- ## Żądanie `GET` `/api/rank_tracker/management/projects/getListWithExtendedData` Nagłówki: `Authorization: Bearer `. Parametry przekazuje się w **query stringu**. **Żądanie** ```jsonc filename="żądanie.jsonc" { "limit": 2, "page": 1 } ``` **cURL** ```bash curl --location --request GET 'https://api.senuto.com/api/rank_tracker/management/projects/getListWithExtendedData?limit=2&page=1' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' ``` ### Parametry ```ts type ProjectsGetListWithExtendedDataRequest = { /** Liczba projektów na stronę. */ limit?: number; /** Numer strony. */ page?: number; /** Sortowanie listy projektów. */ order?: { prop: string; dir: 'ASC' | 'DESC' }; /** Filtry listy projektów. */ filtering?: Array>; /** Zawężenie do projektów oznaczonych wskazanymi tagami. Wartość inna niż tablica jest ignorowana. */ tags?: number[]; } export default ProjectsGetListWithExtendedDataRequest ``` > **Ostrzeżenie:** > W odpowiedzi są **dwa klucze wykresu**: poprawny `visibility_chart` i **literówka `visiblity_chart`** (brak drugiego `i`). Oba istnieją i zawierają to samo. Czytaj `visibility_chart`, ale nie zdziw się drugim polem. > **Ostrzeżenie:** > To metoda **`GET`** — `order`, `filtering`, `tags`, `limit` i `page` przekazuje się w **query stringu**. `tags` przekazane jako wartość inna niż tablica jest po cichu ignorowane. > **Ostrzeżenie:** > Lista zawiera także projekty **udostępnione** Twojemu kontu, nie tylko własne — rozróżnisz je polem `project.is_owner`. ## Odpowiedź Przykład to **rzeczywista odpowiedź produkcyjna**, skrócona. ```json filename="przykładowa-odpowiedź" { "success": true, "data": [ { "project": { "id": 1234, "domain": "example.com", "name": "medonet", "country_id": "1", "region_id": "22440", "project_fit_id": "1", "status": "1", "is_crawled": "1", "is_owner": false, "last_update": "2026-08-24", "tags": [] }, "dates": { "last_date": { "timestamp": 1787529600, "formatted": "2026-08-24" }, "previous_data": { "timestamp": 1787443200, "formatted": "2026-08-23" } }, "statistics": { "keywords_count": { "older_value": "100", "recent_value": "100", "diff": 0, "percent": 0 }, "top3": { "older_value": 0, "recent_value": 0, "diff": 0, "percent": 0 }, "top10": { "older_value": 9, "recent_value": 8, "diff": -1, "percent": -0.1111 }, "top50": { "older_value": 64, "recent_value": 67, "diff": 3, "percent": 0.0469 }, "avg_pos": { "older_value": 34.89, "recent_value": 34.61, "diff": -0.28, "percent": -0.008 }, "organic_visibility": { "older_value": 12219, "recent_value": 13514, "diff": 1295, "percent": 0.106 }, "wins": { "older_value": 19, "recent_value": 28, "diff": 9, "percent": 0.4737 }, "lost": { "older_value": 31, "recent_value": 27, "diff": -4, "percent": -0.129 } }, "visibility_chart": [], "visiblity_chart": [] } ], "pagination": { "page_count": 2, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": "4", "limit": 2 } } ``` ### Struktura odpowiedzi ```ts type ProjectsGetListWithExtendedDataResponse = { success: boolean; data: Array<{ /** Dane projektu: identyfikator, domena, nazwa, kraj, region, dopasowanie, tagi, właściciel. */ project: Record; /** Data ostatniego pomiaru i pomiaru poprzedniego — każda jako `{ timestamp, formatted }`. */ dates: { last_date: { timestamp: number; formatted: string }; previous_data: { timestamp: number; formatted: string }; }; /** Statystyki projektu: liczba fraz, rozkład pozycji, widoczność, wzrosty i spadki. */ statistics: { keywords_count: number; top3: number; top10: number; top50: number; out50: number; /** Rozkłady w przedziałach pozycji. */ '1_3': number; '4_10': number; '11_20': number; '21_50': number; avg_pos: number; organic_visibility: number; organic_potential: number; ad_visibility: number; ad_potential: number; budget: number; wins: number; lost: number; no_change: number; }; /** Szereg do wykresu widoczności. */ visibility_chart: unknown[]; /** **Literówka w nazwie klucza** — duplikat `visibility_chart`, utrzymywany dla kompatybilności. */ visiblity_chart: unknown[]; }>; pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; limit: number; }; } export default ProjectsGetListWithExtendedDataResponse ``` ## Błędy Błędy tego endpointu przychodzą we [wspólnej kopercie ze statusem `418`](/types/errors). ## Powiązane akcje - [`getMyActiveProjects`](/modules/rank_tracker/rt-projects-getMyActiveProjects) — lekka lista projektów (`id`, `domain`, `name`). - [`getMyGroupsInProject`](/modules/rank_tracker/rt-projects-getMyGroupsInProject) — grupy fraz w projekcie. - [`positions/getData`](/modules/rank_tracker/rt-positions-getData) — pełny raport pozycji projektu. --- # Grupy fraz w projekcie (`getMyGroupsInProject`) **`GET /api/rank_tracker/management/projects/getMyGroupsInProject`** Zwraca **grupy fraz zdefiniowane w projekcie** wraz z liczbą fraz w każdej z nich. Identyfikatory grup podaje się dalej jako `group_id` w raportach Monitoringu — to najprostsza droga, by przejść od projektu do konkretnego wycinka fraz. --- ## Żądanie `GET` `/api/rank_tracker/management/projects/getMyGroupsInProject` Nagłówki: `Authorization: Bearer `. Parametry przekazuje się w **query stringu**. **Żądanie** ```jsonc filename="żądanie.jsonc" { "project_id": null } ``` **cURL** ```bash curl --location --request GET 'https://api.senuto.com/api/rank_tracker/management/projects/getMyGroupsInProject?project_id=87944' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' ``` ### Parametry ```ts type ProjectsGetMyGroupsInProjectRequest = { /** * **Wymagane w praktyce**. Identyfikator projektu; brak parametru jest interpretowany jako `0`, * co zwraca wyłącznie wiersz zbiorczy. Własne projekty pobierzesz z `getMyActiveProjects`. */ project_id: number; } export default ProjectsGetMyGroupsInProjectRequest ``` > **Ostrzeżenie:** > **Typ pola `count` jest niespójny.** Wiersz zbiorczy (`id: 0`) ma liczbę, a grupy rzeczywiste — string (`"1"`). Rzutuj na liczbę przed obliczeniami. > **Ostrzeżenie:** > Pierwszy wiersz odpowiedzi jest **syntetyczny**: `id: 0` z nazwą „Słowa kluczowe projektu” i sumą fraz ze wszystkich grup. Nie jest to istniejąca grupa — nie przekazuj `0` jako `group_id`, jeśli chcesz konkretną grupę. > **Ostrzeżenie:** > Endpoint zwraca wyłącznie grupy **niedynamiczne** należące do Twojego konta w tym projekcie. Brak `project_id` w query nie jest błędem — zwróci pustą listę z samym wierszem zbiorczym. ## Odpowiedź Przykład to **rzeczywista odpowiedź produkcyjna**, skrócona. ```json filename="przykładowa-odpowiedź" { "success": true, "data": [ { "id": 0, "name": "Słowa kluczowe projektu", "count": 342 }, { "id": 12345, "name": "Kategorie produktowe", "count": "128" } ] } ``` ### Struktura odpowiedzi ```ts type ProjectsGetMyGroupsInProjectResponse = { success: boolean; data: Array<{ /** Identyfikator grupy. Wartość `0` to wiersz zbiorczy „wszystkie frazy projektu”. */ id: number; name: string; /** * Liczba fraz w grupie. **Typ niespójny**: dla wiersza zbiorczego (`id: 0`) liczba, * dla grup rzeczywistych string. */ count: number | string; }>; } export default ProjectsGetMyGroupsInProjectResponse ``` ## Błędy Błędy tego endpointu przychodzą we [wspólnej kopercie ze statusem `418`](/types/errors). ## Powiązane akcje - [`getMyActiveProjects`](/modules/rank_tracker/rt-projects-getMyActiveProjects) — skąd wziąć `project_id`. - [`keywords/getGroupKeywords`](/modules/rank_tracker/rt-keywords-getGroupKeywords) — frazy w wskazanej grupie. - [`groups/list`](/modules/rank_tracker/rt-groups-list) — grupy z pełnymi danymi. --- # Tryby dopasowania projektu (`getProjectFits`) **`GET /api/rank_tracker/management/projects/getProjectFits`** Zwraca **słownik trybów dopasowania adresu**, czyli dopuszczalne wartości `project_fit_id` używane przy zakładaniu projektu. Tryb decyduje, jakie adresy projekt uznaje za swoje: całą domenę z subdomenami, samą domenę, wskazany katalog albo jeden konkretny adres. --- ## Żądanie `GET` `/api/rank_tracker/management/projects/getProjectFits` Nagłówki: `Authorization: Bearer `. Endpoint nie przyjmuje parametrów. **Żądanie** ```jsonc filename="żądanie.jsonc" {} ``` **cURL** ```bash curl --location --request GET 'https://api.senuto.com/api/rank_tracker/management/projects/getProjectFits' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' ``` ### Parametry Endpoint nie przyjmuje parametrów. Zwraca stały słownik czterech pozycji w ustalonej kolejności. > **Ostrzeżenie:** > Odpowiedź nie zawiera `pagination` — to zamknięty słownik czterech pozycji. > **Ostrzeżenie:** > Pole `name` to **wzorzec do prezentacji** (np. `*.Domena.pl/*`), a nie wartość do wysłania. W żądaniach używaj `id`. ## Odpowiedź Przykład to **rzeczywista odpowiedź produkcyjna**, skrócona. ```json filename="przykładowa-odpowiedź" { "success": true, "data": [ { "id": 1, "name": "*.Domena.pl/*" }, { "id": 2, "name": "Domena.pl/*" }, { "id": 3, "name": "Domena.pl/catalog/*" }, { "id": 4, "name": "Domena.pl/link1.html" } ] } ``` ### Struktura odpowiedzi ```ts type ProjectsGetProjectFitsResponse = { success: boolean; data: Array<{ /** Wartość do przekazania jako `project_fit_id`. */ id: number; /** Wzorzec dopasowania w formie czytelnej dla człowieka. */ name: string; }>; } export default ProjectsGetProjectFitsResponse ``` ## Błędy Błędy tego endpointu przychodzą we [wspólnej kopercie ze statusem `418`](/types/errors). ## Powiązane akcje - [`getMyActiveProjects`](/modules/rank_tracker/rt-projects-getMyActiveProjects) — lista projektów. - [`checkDomain`](/modules/rank_tracker/rt-projects-checkDomain) — sprawdzenie domeny przed założeniem projektu. --- # Sprawdzenie domeny (`checkDomain`) **`GET /api/rank_tracker/management/projects/checkDomain`** Sprawdza, czy **podana domena nadaje się do założenia projektu** na Twoim koncie. Zwraca jedną flagę `status`. Warto wywołać przed właściwym utworzeniem projektu, żeby nie zużywać slotu na adres, który zostanie odrzucony. --- ## Żądanie `GET` `/api/rank_tracker/management/projects/checkDomain` Nagłówki: `Authorization: Bearer `. Parametry przekazuje się w **query stringu**. **Żądanie** ```jsonc filename="żądanie.jsonc" { "domain": "senuto.com" } ``` **cURL** ```bash curl --location --request GET 'https://api.senuto.com/api/rank_tracker/management/projects/checkDomain?domain=senuto.com' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' ``` ### Parametry ```ts type ProjectsCheckDomainRequest = { /** **Wymagane**. Domena do sprawdzenia, bez schematu (np. `example.com`). */ domain: string; } export default ProjectsCheckDomainRequest ``` > **Ostrzeżenie:** > Odpowiedź to **sama flaga, bez powodu odrzucenia** — przy `status: false` API nie mówi, czy problem to format adresu, duplikat projektu, czy limit konta. > **Ostrzeżenie:** > `success: true` oznacza tylko, że żądanie zostało przetworzone. Wynik sprawdzenia czytaj z `data.status`, nie z `success`. ## Odpowiedź Przykład to **rzeczywista odpowiedź produkcyjna**, skrócona. ```json filename="przykładowa-odpowiedź" { "success": true, "data": { "status": true } } ``` ### Struktura odpowiedzi ```ts type ProjectsCheckDomainResponse = { success: boolean; data: { /** `true` — domena przechodzi walidację; `false` — nie nadaje się. */ status: boolean; }; } export default ProjectsCheckDomainResponse ``` ## Błędy Błędy tego endpointu przychodzą we [wspólnej kopercie ze statusem `418`](/types/errors). ## Powiązane akcje - [`getProjectFits`](/modules/rank_tracker/rt-projects-getProjectFits) — tryby dopasowania adresu. - [`getMyActiveProjects`](/modules/rank_tracker/rt-projects-getMyActiveProjects) — lista istniejących projektów. --- # Pozycje: dane (`getData`) **`POST /api/rank_tracker/reports/positions/getData`** Zwraca monitorowane słowa kluczowe projektu Rank Tracker dla wybranego zakresu dat, wraz z pozycjami dla każdego słowa kluczowego, historią pozycji, widocznością, CPC, liczbą wyszukiwań, snippetami SERP oraz podziałem na desktop/mobile. | Fraza | ID | KID | Status | Widoczność org. | Widoczność org. poprz. | Δ widoczności org. | Potencjał org. | Δ pozycji org. | CPC | Wyszukiwania/mies. | Rankujący URL | Pozycja bieżąca | Pozycja ostatnia | Zmiana pozycji | Pozycja wczoraj | Pierwsza pozycja | Data pierwszej pozycji | Aktualizacja (data) | Aktualizacja (timestamp) | Wzrosty | Spadki | Bez zmian | Snippety SERP | Pozycja desktop | Pozycja mobile | Zakres: pierwsza | Zakres: ostatnia | Zakres: różnica | Poz. pierwsza | Poz. bieżąca | Poz. poprzednia | Poz. różnica | Poz. wzrosty | Poz. spadki | Poz. bez zmian | Poz. początek zakresu | Poz. koniec zakresu | Widoczność bieżąca | Widoczność poprz. | Δ widoczności | CPC (statystyki) | Wyszukiwania (statystyki) | URL (statystyki) | Snippety (statystyki) | Mapy aktywne | Mapy | Bezpośr. odpowiedzi aktywne | Bezpośr. odpowiedzi | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | slowa kluczowe | 3025571 | 95b343081f25ecf51403b940b739bf78 | complete | 140 | 228 | -88 | 463 | 1 | 6.34 | 1300 | https://www.senuto.com/pl/blog/slowa-kluczowe/ | 3 | 2 | 1 | 2 | 27 | | 2026-06-30 | 1782797439 | 3 | 3 | 3 | ["ai_overview","people_also_ask","featured_snippets"] | 2 | | 3 | 2 | 1 | 27 | 3 | 2 | 1 | 4 | 3 | 2 | 3 | 2 | 139.75 | 227.76 | -88.01 | 6.34 | 1300 | https://www.senuto.com/pl/blog/slowa-kluczowe/ | ["ai_overview","people_also_ask"] | | | | | _projekt Rank Trackera, zakres 2026-06-20 – 2026-06-29, limit: 2. Uwaga na niespójne typy: searches, current_position i diff_position to stringi, a cpc jest liczbą. Wszystkie adresowalne pola wiersza (pominięto mapy o zmiennych kluczach-datach: positions, desktop.positions_history, desktop.history, mobile.positions_history, mobile.history — są w JSON i sekcji „Struktura odpowiedzi”)._ --- ## Żądanie `POST` `/api/rank_tracker/reports/positions/getData` Nagłówki: `Authorization: Bearer `, `Content-Type: application/json`. ### Struktura żądania **Podstawowy** ```jsonc filename="żądanie-podstawowe.jsonc" { "project_id": null, "date_min": "2026-06-20", "date_max": "2026-06-29", "limit": 2, "page": 1 } ``` **Rozszerzony** ```jsonc filename="żądanie-rozszerzone.jsonc" { "project_id": null, "date_min": "2026-06-20", "date_max": "2026-06-29", "group_id": 0, "page": 1, "limit": 2, "mode": "desktop", "order": { "prop": "statistics.positions_date_range.last", "value": "asc" }, "filtering": [] } ``` **cURL** ```bash curl --location --request POST 'https://api.senuto.com/api/rank_tracker/reports/positions/getData' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' \ --data-raw '{ "project_id": null, "date_min": "2026-06-20", "date_max": "2026-06-29", "limit": 2, "page": 1 }' ``` ### Parametry ```ts type RtPositionsGetDataRequest = { /** * **Wymagane**. ID projektu Rank Tracker. Liczba całkowita nieujemna. Musisz mieć dostęp * do projektu (jako właściciel, administrator lub poprzez udostępnienie ACL) — w przeciwnym razie `Unauthorized access`. * Listę swoich projektów pobierzesz: `POST /api/rank_tracker/management/projects/getMyActiveProjects`. */ project_id: number; /** * **Wymagane**. Początek zakresu dat, format `YYYY-mm-dd`. * Musi być `<= date_max` oraz `<= today`. */ date_min: string; /** * **Wymagane**. Koniec zakresu dat, format `YYYY-mm-dd`. * Musi być `>= date_min` oraz `<= today`. */ date_max: string; /** * ID grupy słów kluczowych w projekcie. Gdy ustawione, raport zwraca pozycje * dla tej grupy zamiast dla całego projektu. * @default 0 */ group_id?: number; /** * ID konkurenta. Gdy ustawione, raport zwraca pozycje dla tego konkurenta * (w obrębie projektu lub grupy). */ competitor_id?: number; /** * Numer strony. Liczba całkowita nieujemna. * @default 1 */ page?: number; /** * Liczba wierszy na stronę. Liczba całkowita nieujemna (maxLimit kontrolera = 10000). * @default 10 */ limit?: number; /** * Tryb pobierania pozycji (urządzenie). Po stronie serwera zamieniane na małe litery. * @default 'desktop' */ mode?: 'desktop' | 'mobile'; /** * Sortowanie. Obiekt z `prop` (np. `statistics.positions_date_range.last` / * `.first` — wtedy `value` jest automatycznie uzupełniane z `date_max` / `date_min`) oraz `value`. */ order?: { prop: string; value: string }; /** * Filtrowanie — tablica grup (filtry w grupie łączone AND). Zwalidowany klucz: * `keywords` (filtr tekstowy przez `items`: match `contain`/`startsWith`/`endsWith`/`exact`/`notContain`). * Szczegóły i pełny mechanizm: sekcja "Filtrowanie" poniżej oraz [`Filter`](/types/filter). */ filtering?: { filters: { key: 'keywords'; items: { value: string; match: 'contain' | 'startsWith' | 'endsWith' | 'exact' | 'notContain' }[] }[]; }[]; } export default RtPositionsGetDataRequest ``` > **Ostrzeżenie:** > Warianty **`wins`** / **`losses`** **nie** są parametrem w treści żądania — to segment URL (argument `typeWinsOrLosses` akcji): `…/getData` (pełny raport), `…/getData/wins` (słowa kluczowe, które zyskały od wczoraj), `…/getData/losses` (słowa kluczowe, które spadły). Odpowiednik eksportu: `/api/rank_tracker/reports/exports/positions/getData[/wins|/losses]`. > **Ostrzeżenie:** > Wymagane pola w treści żądania to **`project_id`**, **`date_min`** i **`date_max`** (daty w formacie `YYYY-mm-dd`, obie `<= today`, przy czym `date_min <= date_max`). W przeciwieństwie do Analizy widoczności, Rank Tracker **nie** używa `domain` / `fetch_mode` — operuje na **`project_id`** (plus opcjonalnie `group_id` / `competitor_id`). Ścieżka jest w **snake\_case** i kanoniczna pod `/api/rank_tracker/…`. Warianty **`wins`** / **`losses`** to **segmenty URL**, a nie pola w treści żądania: `…/getData/wins` i `…/getData/losses`. Błędy walidacji zwracają **`418`** (`invalid_data`). ## Filtrowanie Opcjonalny parametr `filtering` odpowiada polu **Filtry** nad tabelą w raporcie pozycji projektu. To tablica grup; filtry w grupie łączone są operatorem AND (zob. wspólny [`Filter`](/types/filter)). Zwalidowany klucz dla tego endpointu to **`keywords`** (filtr tekstowy przez `items`): ```jsonc filename="żądanie-z-filtrowaniem.jsonc" { "project_id": null, "date_min": "2026-06-17", "date_max": "2026-06-30", "filtering": [ { "filters": [ { "key": "keywords", "items": [{ "value": "pies", "match": "startsWith" }] } ] } ] } ``` Operatory `match` dla `keywords`: `contain`, `startsWith`, `endsWith`, `exact`, `notContain`. > **Informacja:** > Zwalidowane na żywo (projekt `124572`): bez filtra `count` = 395; z filtrem `keywords startsWith "pies"` → `count` = 8 (frazy „pies berneńczyk", „pies rysunek"…). Aplikacja dołącza do filtra opcjonalne `type: "string"` i `filterSourceType: "customFilter"` — API działa też bez nich. ## Odpowiedź Po pomyślnym żądaniu otrzymujesz `data` (tablica wierszy słów kluczowych) oraz `pagination`. Zwróć uwagę, że kilka pól liczbowych zwracanych jest jako **ciągi znaków** (np. `searches`, `current_position`, `diff_position`, `first_position`, `id`), podczas gdy `organic_visibility` / `organic_potential` / `cpc` są liczbami — typowanie jest niespójne. **Skrócona** ```json filename="przykładowa-odpowiedź (skrócona)" { "success": true, "data": [ { "id": "3025571", "keyword": "slowa kluczowe", "current_position": "3", "last_position": 2, "positions": { "2026-06-20": 3, "2026-06-28": 2, "2026-06-29": 2 }, "statistics": { "positions_date_range": { "first": 3, "last": 2, "diff": 1 } } } ], "pagination": { "page_count": 198, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 395, "limit": 2 } } ``` **Pełna** ```json filename="przykładowa-odpowiedź (200)" { "success": true, "data": [ { "id": "3025571", "kid": "95b343081f25ecf51403b940b739bf78", "status": "complete", "organic_visibility": 140, "organic_visibility_old": 228, "organic_visibility_diff": -88, "organic_potential": 463, "organic_pos_diff": "1", "updated": { "date": "2026-06-30", "timestamp": 1782797439 }, "cpc": 6.34, "keyword": "slowa kluczowe", "searches": "1300", "url": "https://www.senuto.com/pl/blog/slowa-kluczowe/", "positions": { "2026-06-20": 3, "2026-06-28": 2, "2026-06-29": 2 }, "current_position": "3", "last_position": 2, "diff_position": "1", "pos_yesterday": "2", "positions_changes": { "grows": 3, "losses": 3, "no_changes": 3 }, "snippets": ["ai_overview", "people_also_ask", "featured_snippets"], "first_position": "27", "first_date": "", "desktop": { "position": 2, "positions_history": { "2026-06-29": 2 }, "history": { "2026-06-29": { "url": "https://www.senuto.com/pl/blog/slowa-kluczowe/", "pos": 2, "v": 227.76, "has_serp": true } } }, "mobile": { "position": null, "positions_history": { "2026-06-29": null }, "history": { "2026-06-29": { "url": null, "pos": null, "v": 0, "has_serp": true } } }, "statistics": { "positions_date_range": { "first": 3, "last": 2, "diff": 1 }, "position": { "first": 27, "current": 3, "previous": 2, "diff": 1, "changes": { "wins": 4, "losses": 3, "no_changes": 2 }, "range_start": 3, "range_end": 2 }, "visibility": { "current": 139.75, "previous": 227.76, "diff": -88.01 }, "cpc": { "current": 6.34 }, "searches": { "current": "1300" }, "url": { "current": "https://www.senuto.com/pl/blog/slowa-kluczowe/" }, "snippets": { "current": ["ai_overview", "people_also_ask"] } }, "serp_features": { "maps_active": null, "maps": null, "direct_answers_active": null, "direct_answers": null } } ], "pagination": { "page_count": 198, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 395, "limit": 2 } } ``` ### Struktura odpowiedzi ```ts type RtPositionsResponse = { /** `true` przy powodzeniu; przy błędzie `false` i koperta z `error` */ success: boolean; /** Zwrócone wiersze słów kluczowych */ data: RtPositionRow[]; /** Metadane paginacji */ pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; limit: number; }; } type RtPositionRow = { /** ID wiersza słowa kluczowego — zwracane jako ciąg znaków */ id: string; kid: string; status: string; organic_visibility: number; organic_visibility_old: number; organic_visibility_diff: number; organic_potential: number; /** Zwracane jako ciąg znaków */ organic_pos_diff: string; updated: { date: string; timestamp: number }; cpc: number; keyword: string; /** Miesięczna liczba wyszukiwań — zwracane jako ciąg znaków */ searches: string; url: string; /** Mapa data -> pozycja dla wybranego zakresu */ positions: Record; /** Zwracane jako ciąg znaków */ current_position: string; last_position: number; /** Zwracane jako ciąg znaków */ diff_position: string; /** Zwracane jako ciąg znaków */ pos_yesterday: string; positions_changes: { grows: number; losses: number; no_changes: number }; snippets: string[]; /** Zwracane jako ciąg znaków */ first_position: string; first_date: string; desktop: RtDeviceBlock; mobile: RtDeviceBlock; statistics: { positions_date_range: { first: number; last: number; diff: number }; position: { first: number; current: number; previous: number; diff: number; changes: { wins: number; losses: number; no_changes: number }; range_start: number; range_end: number }; visibility: { current: number; previous: number; diff: number }; cpc: { current: number }; searches: { current: string }; url: { current: string }; snippets: { current: string[] }; }; serp_features: { maps_active: number | null; maps: unknown | null; direct_answers_active: number | null; direct_answers: unknown | null }; } type RtDeviceBlock = { position: number | null; positions_history: Record; history: Record; } export default RtPositionsResponse ``` ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data, unreachable_data, timeout, database, unknown */ type: string; message: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > **`418`** zwracane jest również dla błędów walidacji — nie tylko dla ograniczeń liczby zapytań. Brakujące lub źle sformatowane `project_id` / `date_min` / `date_max` skutkuje `invalid_data` wraz z mapą `params`. Zwróć uwagę na znany **błąd w treści komunikatu** dla reguły zakresu: brzmi on `date_max must be less or equal than date_min`, ale logika jest poprawna (`date_min <= date_max`). Projekt, do którego nie masz dostępu, zwraca `Unauthorized access`. ## Powiązane akcje - `getData` — bieżące pozycje dla projektu (ta strona) - `getData/wins` / `getData/losses` — słowa kluczowe, które zyskały / straciły pozycje względem wczoraj (segment URL, ta sama treść żądania) - `POST /api/rank_tracker/management/projects/getMyActiveProjects` — pobranie listy swoich projektów, aby uzyskać `project_id` (zwraca `{ id, domain, name }`) - `/api/rank_tracker/reports/exports/positions/getData[/wins|/losses]` — odpowiednik tego raportu w formie eksportu --- # Pozycje: średnie konta (`getAvgData`) **`POST /api/rank_tracker/reports/positions/getAvgData`** Zwraca zagregowane średnie pozycji i widoczności **dla całego konta użytkownika** — endpoint działa na poziomie użytkownika i sumuje dane ze wszystkich jego projektów Rank Tracker. Odpowiedź zawiera trzy sekcje: `history` (dzienne szeregi czasowe z ostatnich 30 dni — pozycje, wzrosty, spadki, widoczność), `statistics` (statystyki zbiorcze i porównania okres do okresu) oraz `projects` (liczba projektów, bilans projektów rosnących/spadających i łączna liczba monitorowanych fraz). --- ## Żądanie `POST` `/api/rank_tracker/reports/positions/getAvgData` Nagłówki: `Authorization: Bearer `, `Content-Type: application/json`. ### Struktura żądania **Podstawowy** ```jsonc filename="żądanie-podstawowe.jsonc" // endpoint nie przyjmuje żadnych parametrów — wyślij puste ciało {} ``` **Rozszerzony** ```jsonc filename="żądanie-rozszerzone.jsonc" // brak wariantu rozszerzonego — nie istnieją parametry opcjonalne; // zakres (ostatnie 30 dni) jest ustalany po stronie serwera {} ``` **cURL** ```bash curl --location --request POST 'https://api.senuto.com/api/rank_tracker/reports/positions/getAvgData' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' \ --data-raw '{}' ``` ### Parametry Endpoint nie przyjmuje żadnych parametrów — wyślij puste ciało `{}`. Zakres kont i projektów wynika wyłącznie z tokenu `Authorization`: agregowane są wszystkie projekty zalogowanego użytkownika, a okres jest stały (ostatnie 30 dni, ustalany po stronie serwera). > **Ostrzeżenie:** > Endpoint przyjmuje **puste ciało żądania** (`{}`) — nie ma żadnych parametrów. Zakres danych jest **stały: ostatnie 30 dni**, ustalany po stronie serwera, i nie da się go zmienić. Nie ma też paginacji. **Pułapka:** klucze map w `history` to **uniksowe timestampy serializowane jako stringi** (np. `"1780358400"`), nie daty `YYYY-MM-DD` — przed wykreśleniem szeregów przekonwertuj je na daty. ## Odpowiedź `data` zawiera trzy sekcje. `history` to siedem map o kluczach będących **uniksowymi timestampami (stringi)** i wartościach liczbowych — po jednym punkcie na dzień, około 30 punktów na mapę. `statistics` podsumowuje okres: sumy wzrostów/spadków, średnie pozycji i widoczności oraz porównania okres do okresu (`older_value` / `recent_value` / `diff` / `percent`). `projects` opisuje portfel: liczbę projektów z rosnącą (`wins.count`) i spadającą (`lost.count`) widocznością, łączną liczbę projektów (`quantity`) i łączną liczbę monitorowanych fraz (`keywords.quantity`). **Skrócona** ```json filename="przykładowa-odpowiedź (skrócona)" { "success": true, "data": { "history": { "positions": { "1780358400": 41.61, "1782950400": 42.13 }, "visibility": { "1780358400": 353.58, "1782950400": 687.5 } }, "statistics": { "positions_avg": 41.76, "visibility_avg": 758.55 }, "projects": { "quantity": 6, "keywords": { "quantity": 752 } } } } ``` **Pełna** ```json filename="przykładowa-odpowiedź (200)" { "success": true, "data": { "history": { "positions": { "1780358400": 41.61, "1780444800": 41.83, "1782950400": 42.13 }, "positions_diff": { "1780358400": 0, "1780444800": 0.22, "1782950400": -0.22 }, "wins": { "1780358400": 3.33, "1780444800": 7.17, "1782950400": 9.33 }, "lost": { "1780358400": 10.67, "1780444800": 9.67, "1782950400": 5.33 }, "wins_lost": { "1780358400": 7, "1780444800": 8.42, "1782950400": 7.33 }, "visibility": { "1780358400": 353.58, "1780444800": 714.7, "1782950400": 687.5 }, "visibility_diff": { "1780358400": 0, "1780444800": 361.12, "1782950400": 22.76 } }, "statistics": { "wins_sum": 1486.98, "lost_sum": 1565.94, "visibility_avg": 758.55, "visibility_diff_avg": { "recent_value": 0.0147, "percent": 1.47 }, "visibility": { "older_value": 687.5, "recent_value": 664.74, "diff": -22.76, "percent": -3.3105 }, "positions_avg": 41.76, "positions_diff_avg": { "recent_value": 0.0004, "percent": 0.04 }, "positions": { "older_value": 42.13, "recent_value": 42.35, "diff": 0.22, "percent": 0.5222 } }, "projects": { "wins": { "count": 1 }, "lost": { "count": 2 }, "quantity": 6, "keywords": { "quantity": 752 } } } } ``` > **Informacja:** > Każda mapa w `history` została w powyższym przykładzie **skrócona do 3 z \~30 punktów dziennych** — realna odpowiedź zawiera po jednym wpisie na każdy dzień ostatnich 30 dni. ### Struktura odpowiedzi ```ts type GetAvgDataResponse = { /** `true` przy powodzeniu; przy błędzie `false` i koperta z `error` */ success: boolean; data: { /** * Dzienne szeregi czasowe z ostatnich 30 dni. * Klucze wszystkich map to **uniksowe timestampy jako stringi** (np. `"1780358400"`). */ history: { /** Średnia pozycja konta danego dnia */ positions: Record; /** Dzienna zmiana średniej pozycji */ positions_diff: Record; /** Średnia liczba wzrostów pozycji danego dnia */ wins: Record; /** Średnia liczba spadków pozycji danego dnia */ lost: Record; /** Łączny bilans wzrostów i spadków */ wins_lost: Record; /** Widoczność konta danego dnia */ visibility: Record; /** Dzienna zmiana widoczności */ visibility_diff: Record; }; /** Statystyki zbiorcze dla okresu 30 dni */ statistics: { /** Suma wzrostów pozycji w okresie */ wins_sum: number; /** Suma spadków pozycji w okresie */ lost_sum: number; /** Średnia widoczność w okresie */ visibility_avg: number; /** Średnia zmiana widoczności */ visibility_diff_avg: { recent_value: number; percent: number; }; /** Porównanie widoczności okres do okresu */ visibility: PeriodComparison; /** Średnia pozycja w okresie */ positions_avg: number; /** Średnia zmiana pozycji */ positions_diff_avg: { recent_value: number; percent: number; }; /** Porównanie średniej pozycji okres do okresu */ positions: PeriodComparison; }; /** Podsumowanie portfela projektów konta */ projects: { /** Liczba projektów z rosnącą widocznością */ wins: { count: number }; /** Liczba projektów ze spadającą widocznością */ lost: { count: number }; /** Łączna liczba projektów użytkownika */ quantity: number; /** Łączna liczba monitorowanych fraz we wszystkich projektach */ keywords: { quantity: number }; }; }; } type PeriodComparison = { /** Wartość ze starszego punktu porównania */ older_value: number; /** Wartość z nowszego punktu porównania */ recent_value: number; /** Różnica (recent - older) */ diff: number; /** Zmiana procentowa */ percent: number; } export default GetAvgDataResponse ``` ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data, unknown */ type: string; message: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > Ponieważ endpoint nie ma parametrów, praktycznie jedyne źródła błędów to uwierzytelnienie: brak lub nieprawidłowy token `Authorization: Bearer` zwraca błąd autoryzacji zamiast danych. Dane zawsze dotyczą konta z tokenu — nie da się wskazać innego użytkownika ani pojedynczego projektu. ## Powiązane akcje - `getData` — pełna, stronicowana lista pozycji projektu (warianty `/wins` i `/losses` jako segmenty URL) - `getKeywordPositions` — szczegóły pojedynczej frazy z dokładnym dopasowaniem - `getAvgData` — średnie pozycji i widoczności zagregowane dla wszystkich projektów konta (ta strona) --- # Frazy: statusy projektów (`getProjectsStatus`) **`POST /api/rank_tracker/reports/keywords/getProjectsStatus`** Zwraca statusy przetwarzania fraz **wszystkich projektów zalogowanego użytkownika**. Akcja działa na poziomie użytkownika — nie przyjmuje żadnych parametrów, a wynikiem jest tablica z jednym wpisem na projekt: identyfikatorem projektu oraz listą statusów z liczbą fraz w każdym z nich. Typowe zastosowanie: sprawdzenie, czy frazy projektu są już przetworzone (`complete`), zanim pobierzesz raporty pozycji (np. `getData`). Wynik nie jest stronicowany. | ID projektu | Status | Liczba fraz | | --- | --- | --- | | 87908 | complete | 58 | | 87913 | complete | 100 | | 87944 | complete | 94 | _2026-07-02 — statusy przetwarzania fraz projektów zalogowanego użytkownika (3 z 6 projektów). Wszystkie adresowalne pola wiersza; tablica statuses zaadresowana przez indeks (statuses.0.*) — pokazano pierwszy status każdego wiersza._ --- ## Żądanie `POST` `/api/rank_tracker/reports/keywords/getProjectsStatus` Nagłówki: `Authorization: Bearer `, `Content-Type: application/json`. ### Struktura żądania **Podstawowy** ```jsonc filename="żądanie-podstawowe.jsonc" {} ``` **cURL** ```bash curl --location --request POST 'https://api.senuto.com/api/rank_tracker/reports/keywords/getProjectsStatus' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' \ --data-raw '{}' ``` ### Parametry Akcja **nie przyjmuje żadnych parametrów** — wyślij puste ciało `{}`. Zakres danych wyznacza wyłącznie zalogowany użytkownik (token `Authorization`): odpowiedź obejmuje wszystkie jego projekty Rank Tracker. > **Ostrzeżenie:** > Endpoint obsługuje metodę **`POST`**, mimo że nie przyjmuje żadnych parametrów — wyślij **puste ciało JSON** (`{}`). Akcja **nie ma walidatorów pól** i działa na zalogowanym użytkowniku (z tokenu `Authorization`). Uwaga na typy w odpowiedzi: zarówno `id` projektu, jak i `count` są zwracane jako **stringi**, nie liczby. ## Odpowiedź Po pomyślnym żądaniu otrzymujesz `data` — tablicę z jednym elementem na każdy projekt użytkownika, bez paginacji. Każdy element zawiera `id` projektu (string) oraz tablicę `statuses` z parami: nazwa statusu (np. `complete`) i liczba fraz w tym statusie (`count`, string). **Skrócona** ```json filename="przykładowa-odpowiedź (skrócona)" { "success": true, "data": [ { "id": "87944", "statuses": [{ "status": "complete", "count": "94" }] } ] } ``` **Pełna** ```json filename="przykładowa-odpowiedź (zwalidowana, 200 — skrócona do 3 z 6 projektów)" { "success": true, "data": [ { "id": "87908", "statuses": [ { "status": "complete", "count": "58" } ] }, { "id": "87913", "statuses": [ { "status": "complete", "count": "100" } ] }, { "id": "87944", "statuses": [ { "status": "complete", "count": "94" } ] } ] } ``` ### Struktura odpowiedzi ```ts type GetProjectsStatusResponse = { /** `true` przy powodzeniu; przy błędzie `false` i koperta z `error` */ success: boolean; /** Statusy przetwarzania fraz — jeden wpis na każdy projekt użytkownika, bez paginacji */ data: ProjectStatus[]; } type ProjectStatus = { /** ID projektu Rank Tracker — zwracane jako string, nie liczba */ id: string; /** Statusy fraz w projekcie wraz z liczebnością */ statuses: KeywordStatus[]; } type KeywordStatus = { /** Nazwa statusu przetwarzania, np. "complete" */ status: string; /** Liczba fraz w tym statusie — zwracana jako string, nie liczba */ count: string; } export default GetProjectsStatusResponse ``` ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data, unknown */ type: string; message: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > Akcja nie ma walidatorów pól ani parametrów wejściowych, więc błędy walidacji (`invalid_data`) tu nie występują. Jedynym praktycznym warunkiem jest poprawny token — akcja działa na zalogowanym użytkowniku, a bez ważnego nagłówka `Authorization` żądanie zostanie odrzucone. ## Powiązane akcje - `getProjectKeywords` — słowa kluczowe całego projektu (`POST`, `project_id`) - `getGroupKeywords` — lekka lista (`id` + `keyword`) ograniczona do jednej grupy - `getData` — pełne statystyki pozycji (`POST`, `project_id` + `group_id` + `filtering` + `order`) - `getSerpHtml` — zapis HTML wyników SERP (`POST`, `keyword_id` + `date` + `project_id`) - `getBestKeywords` — najlepsze frazy projektu (zawsze 10 wierszy) - `getProjectsStatus` — statusy przetwarzania fraz wszystkich projektów użytkownika (ta strona) --- # Frazy: zrzut SERP (`getSerpHtml`) **`POST /api/rank_tracker/reports/keywords/getSerpHtml`** Zwraca zapisany zrzut HTML strony wyników Google (SERP) dla wskazanej frazy projektu Rank Tracker z danego dnia. Odpowiedź zawiera pojedyncze pole `data.html` — pełny kod HTML strony wyników albo `null`, jeśli zrzut dla danej frazy i daty nie jest przechowywany. --- ## Żądanie `POST` `/api/rank_tracker/reports/keywords/getSerpHtml` Nagłówki: `Authorization: Bearer `, `Content-Type: application/json`. ### Struktura żądania **Podstawowy** ```jsonc filename="żądanie-podstawowe.jsonc" { "project_id": null, "keyword_id": null, "date": "2026-07-01" } ``` **cURL** ```bash curl --location --request POST 'https://api.senuto.com/api/rank_tracker/reports/keywords/getSerpHtml' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' \ --data-raw '{ "project_id": null, "keyword_id": null, "date": "2026-07-01" }' ``` ### Parametry ```ts type GetSerpHtmlRequest = { /** * **Wymagane**. ID projektu Rank Tracker (walidator `SerpHtmlValidator`). * Realny `project_id` pobierzesz z `POST /api/rank_tracker/management/projects/getMyActiveProjects`. */ project_id: number; /** * **Wymagane**. ID frazy w projekcie. Identyfikatory fraz pobierzesz np. z akcji * `getProjectKeywords` lub `getGroupKeywords` w tym samym kontrolerze. * Realny `keyword_id` pobierzesz z `POST /api/rank_tracker/reports/keywords/getData`. */ keyword_id: number; /** * **Wymagane**. Dzień, z którego chcesz pobrać zrzut SERP, w formacie `YYYY-MM-DD`. * Głębokość historii ogranicza limit planu `monitoring_serp_html_history_limit` — data spoza limitu skutkuje `418`. */ date: string; } export default GetSerpHtmlRequest ``` > **Ostrzeżenie:** > Poza walidacją pól (`SerpHtmlValidator` wymaga `project_id`, `keyword_id` oraz `date` w formacie `YYYY-MM-DD`) działa tu limit planu **`monitoring_serp_html_history_limit`** — określa on głębokość historii zrzutów, do której możesz sięgać. Żądanie daty spoza dozwolonej głębokości historii kończy się błędem **`418`**. Data mieszcząca się w limicie, ale bez zapisanego zrzutu, zwraca `HTTP 200` z `html: null` — brak zrzutu nie jest sygnalizowany błędem. ## Odpowiedź Po pomyślnym żądaniu otrzymujesz `data.html` — kod HTML strony wyników Google zapisany dla frazy w podanym dniu, albo `null`, gdy zrzut nie jest przechowywany (np. plan bez historii zrzutów SERP lub brak zapisu z tego dnia). **Pełna (zwalidowana)** ```json filename="przykładowa-odpowiedź (200)" { "success": true, "data": { "html": null } } ``` ### Struktura odpowiedzi ```ts type GetSerpHtmlResponse = { /** `true` przy powodzeniu; przy błędzie `false` i koperta z `error` */ success: boolean; data: { /** * Zapisany kod HTML strony wyników Google dla frazy z podanego dnia, * albo `null`, gdy zrzut nie jest przechowywany dla tego projektu/planu lub daty. */ html: string | null; }; } export default GetSerpHtmlResponse ``` ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data, unknown */ type: string; message: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > Brak któregokolwiek z wymaganych pól (`project_id`, `keyword_id`, `date`) lub niepoprawny format daty skutkuje błędem walidacji `invalid_data` (**`418`**). Kod **`418`** zwracany jest także przy przekroczeniu głębokości historii zrzutów wyznaczonej przez limit planu `monitoring_serp_html_history_limit`. Brak zapisanego zrzutu dla poprawnej daty **nie jest** błędem — otrzymasz `HTTP 200` z `html: null`. ## Powiązane akcje - `getProjectKeywords` — słowa kluczowe całego projektu (`POST`, `project_id`) - `getGroupKeywords` — lekka lista (`id` + `keyword`) ograniczona do jednej grupy - `getData` — pełne statystyki pozycji (`POST`, `project_id` + `group_id` + `filtering` + `order`) - `getSerpHtml` — zapis HTML wyników SERP dla frazy i dnia (ta strona) - `getBestKeywords` / `getProjectsStatus` — pozostałe akcje pomocnicze kontrolera --- # Frazy: słowa kluczowe projektu (`getProjectKeywords`) **`POST /api/rank_tracker/reports/keywords/getProjectKeywords`** Zwraca listę słów kluczowych przypisanych do projektu Rank Tracker (Monitoring) — wyłącznie pary `id` + `keyword`, wraz z metadanymi paginacji. Endpoint nie zwraca danych pozycyjnych ani czasowych; służy do pobrania pełnego zbioru fraz monitorowanych w projekcie. | Fraza | ID | | --- | --- | | jak wychowac szczeniaka | 690709 | | karma dla szczeniaka maltanczyka | 704922 | _projekt Rank Trackera, limit: 2, strona 2. Endpoint zwraca wyłącznie pary id + keyword. Wszystkie adresowalne pola wiersza._ --- ## Żądanie `POST` `/api/rank_tracker/reports/keywords/getProjectKeywords` Nagłówki: `Authorization: Bearer `, `Content-Type: application/json`. Parametry przekazuje się w ciele żądania (JSON). ### Struktura żądania **Podstawowy** ```jsonc filename="żądanie-podstawowe.jsonc" { "project_id": null } ``` **Rozszerzony** ```jsonc filename="żądanie-rozszerzone.jsonc" { "project_id": null, "limit": 2, "page": 2 } ``` **cURL** ```bash curl --location --request POST 'https://api.senuto.com/api/rank_tracker/reports/keywords/getProjectKeywords' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' \ --data-raw '{ "project_id": null, "limit": 2, "page": 2 }' ``` ### Parametry ```ts type GetProjectKeywordsRequest = { /** * **Wymagane**. ID projektu Rank Tracker (Monitoring). Pobierz przez * `/api/rank_tracker/management/projects/getMyActiveProjects`. * Walidacja: nonNegativeInteger + sprawdzenie uprawnień * (właściciel / rola admina / współdzielenie ACL). * Brak dostępu = `418` z komunikatem `Unauthorized access`. */ project_id: number; /** * Liczba słów kluczowych na stronę (paginacja). Nieujemna liczba całkowita. * Maksymalny limit po stronie kontrolera = 100. * @default 10 */ limit?: number; /** * Numer strony (paginacja). Nieujemna liczba całkowita. * @default 1 */ page?: number; } export default GetProjectKeywordsRequest ``` > **Ostrzeżenie:** > Jeśli pominiesz `limit`, paginacja używa wartości domyślnej `10` (od niej liczone jest `page_count`), ale pole `pagination.limit` w odpowiedzi zwraca wtedy `null` zamiast `10` — paginator zwraca surową wartość z żądania, a nie efektywny limit. Przy jawnie podanym `limit` (np. `limit: 2`) pole `pagination.limit` jest poprawne. > **Ostrzeżenie:** > Metoda to **`POST`** — kontroler oraz walidator czytają dane z **ciała żądania** (`getData`), nie z query stringu. Jedynym wymaganym parametrem jest **`project_id`**; jego pominięcie zwraca `418` z `invalid_data`. Brak dostępu do projektu (nie jesteś właścicielem, nie masz roli admina ani współdzielenia ACL) również zwraca `418` z komunikatem `Unauthorized access`. Wbrew starszej dokumentacji ta akcja **nie obsługuje** parametrów `date_min` / `date_max` — są one ignorowane i nie mają wpływu na wynik. ## Odpowiedź Po pomyślnym żądaniu otrzymujesz `data` (tablicę słów kluczowych) oraz `pagination`. Pole `count` w `pagination` to całkowita liczba słów kluczowych w projekcie. **Skrócona** ```json filename="przykładowa-odpowiedź (skrócona)" { "success": true, "data": [ { "id": 690709, "keyword": "jak wychowac szczeniaka" } ], "pagination": { "page_count": 47, "current_page": 2, "count": 94, "limit": 2 } } ``` **Pełna** ```json filename="przykładowa-odpowiedź (200)" { "success": true, "data": [ { "id": 690709, "keyword": "jak wychowac szczeniaka" }, { "id": 704922, "keyword": "karma dla szczeniaka maltanczyka" } ], "pagination": { "page_count": 47, "current_page": 2, "has_next_page": true, "has_prev_page": true, "count": 94, "limit": 2 } } ``` ### Struktura odpowiedzi ```ts type GetProjectKeywordsResponse = { /** `true` przy powodzeniu; przy błędzie `false` i koperta z `error` */ success: boolean; /** Zwrócone słowa kluczowe projektu */ data: ProjectKeyword[]; /** Metadane paginacji */ pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; /** Całkowita liczba słów kluczowych w projekcie */ count: number; /** Efektywny limit; `null`, gdy `limit` nie został podany w żądaniu */ limit: number | null; }; } type ProjectKeyword = { /** ID słowa kluczowego w Rank Tracker */ id: number; /** Treść frazy */ keyword: string; } export default GetProjectKeywordsResponse ``` ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data, unauthorized, unknown */ type: string; message?: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > **`418`** jest zwracane również w przypadku błędów walidacji oraz braku uprawnień — nie tylko przy ograniczaniu liczby żądań. Brak `project_id` → > `{"success":false,"data":{"error":{"type":"invalid_data","params":{"project_id":{"_required":"This field is required"}}}}}`. > Brak dostępu do projektu → > `{"success":false,"data":{"error":{"type":"unauthorized","message":"Unauthorized access"}}}`. ## Powiązane akcje - `getProjectKeywords` — lista słów kluczowych projektu (ta strona) - `getGroupKeywords` — lista słów kluczowych grupy (wymaga `group_id`) - `getData` — raport słów kluczowych z filtrowaniem i sortowaniem (`group_id` + `project_id`) - `getSerpHtml` — zapisany HTML SERP dla frazy (`keyword_id` + `date`) - `getBestKeywords` — najlepsze frazy projektu - `getProjectsStatus` — status projektów --- # Frazy: słowa kluczowe grupy (`getGroupKeywords`) **`POST /api/rank_tracker/reports/keywords/getGroupKeywords`** Zwraca lekką listę słów kluczowych należących do wskazanej grupy w projekcie Rank Tracker. Każdy element to wyłącznie `id` słowa kluczowego oraz jego nazwa (`keyword`) — bez danych o pozycjach czy statystykach (te udostępnia osobna akcja, np. `getData` w tym samym kontrolerze). Wynik jest stronicowany. | Fraza | ID | | --- | --- | | jak wychować szczeniaka | 690709 | | karma dla szczeniaka maltańczyka | 704922 | _projekt i grupa Rank Trackera, limit: 2, strona 2. Każdy wiersz to wyłącznie para id + keyword. Wszystkie adresowalne pola wiersza._ --- ## Żądanie `POST` `/api/rank_tracker/reports/keywords/getGroupKeywords` Nagłówki: `Authorization: Bearer `, `Content-Type: application/json`. ### Struktura żądania **Podstawowy** ```jsonc filename="żądanie-podstawowe.jsonc" { "project_id": null, "group_id": null } ``` **Rozszerzony** ```jsonc filename="żądanie-rozszerzone.jsonc" { "project_id": null, "group_id": null, "limit": 2, "page": 2 } ``` **cURL** ```bash curl --location --request POST 'https://api.senuto.com/api/rank_tracker/reports/keywords/getGroupKeywords' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' \ --data-raw '{ "project_id": null, "group_id": null, "limit": 2, "page": 2 }' ``` ### Parametry ```ts type GetGroupKeywordsRequest = { /** * **Wymagane**. ID projektu Rank Tracker. Jedyne pole twardo wymagane przez walidator (`ProjectAccessRules::requirePresence`). * Musi należeć do użytkownika (lub być udostępnione przez `AclUsersRoles`), inaczej zwracane jest `Unauthorized access`. * Realny `project_id` pobierzesz z `POST /api/rank_tracker/management/projects/getMyActiveProjects`. */ project_id: number; /** * **Wymagane**. ID grupy słów kluczowych w projekcie. Formalnie nie jest wymuszane przez `requirePresence`, * ale bez niego zapytanie filtruje po `group_id = null` i zwraca pustą listę (`data: []`, `count: 0`) z `HTTP 200` — * więc funkcjonalnie jest wymagane. Listę grup pobierzesz z `GET /api/rank_tracker/management/groups/list?project_id=`. * `group_id` musi należeć do podanego `project_id`, inaczej zwracane jest `Unauthorized access`. */ group_id: number; /** * Rozmiar strony paginacji (nieujemna liczba całkowita, `maxLimit = 100`). Odbija się w `pagination.limit`. * @default 10 */ limit?: number; /** * Numer strony paginacji (nieujemna liczba całkowita). * @default 1 */ page?: number; } export default GetGroupKeywordsRequest ``` > **Ostrzeżenie:** > Choć `group_id` nie jest wymuszane przez walidator (`requirePresence` działa dla niego warunkowo — tylko gdy pole już jest w danych), w praktyce zawsze je podawaj: bez `group_id` otrzymasz `HTTP 200` z pustą listą, a nie błąd. Podanie `group_id` nienależącego do `project_id` skutkuje `Unauthorized access`. > **Ostrzeżenie:** > Endpoint obsługuje wyłącznie metodę **`POST`** — parametry przekazuj w **ciele żądania** (`getData()` czyta `body` zarówno w walidatorze, jak i w warunku `WHERE GroupsKeywords.group_id`). **Pułapka:** ten sam URL wywołany przez `GET` zwraca `HTTP 200`, ale **zawsze** `data: []` i `count: 0` — nawet z poprawnym `project_id`/`group_id` w query stringu — ponieważ przy `GET` ciało jest puste, a filtr leci po `group_id = null`. Twardo wymagane jest tylko **`project_id`**; brak **`group_id`** nie powoduje błędu walidacji, lecz zwraca pustą listę z `HTTP 200`, więc funkcjonalnie `group_id` jest również wymagane. Brak `project_id` → `418` z `invalid_data`. ## Odpowiedź Po pomyślnym żądaniu otrzymujesz `data` (tablicę słów kluczowych grupy) oraz `pagination`. Każdy element `data` zawiera wyłącznie `id` (liczba całkowita) i `keyword` (nazwa frazy). `count` w `pagination` odzwierciedla rzeczywistą liczbę słów kluczowych w grupie. **Skrócona** ```json filename="przykładowa-odpowiedź (skrócona)" { "success": true, "data": [ { "id": 690709, "keyword": "jak wychować szczeniaka" } ], "pagination": { "page_count": 31, "current_page": 2, "has_next_page": true, "has_prev_page": true, "count": 62, "limit": 2 } } ``` **Pełna** ```json filename="przykładowa-odpowiedź (200)" { "success": true, "data": [ { "id": 690709, "keyword": "jak wychować szczeniaka" }, { "id": 704922, "keyword": "karma dla szczeniaka maltańczyka" } ], "pagination": { "page_count": 31, "current_page": 2, "has_next_page": true, "has_prev_page": true, "count": 62, "limit": 2 } } ``` ### Struktura odpowiedzi ```ts type GetGroupKeywordsResponse = { /** `true` przy powodzeniu; przy błędzie `false` i koperta z `error` */ success: boolean; /** Słowa kluczowe należące do grupy */ data: GroupKeyword[]; /** Metadane paginacji */ pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; /** Wartość przekazanego `limit`; `null`, gdy nie podano */ limit: number | null; }; } type GroupKeyword = { /** ID słowa kluczowego */ id: number; /** Nazwa frazy */ keyword: string; } export default GetGroupKeywordsResponse ``` ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data, unknown */ type: string; message: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > **`418`** jest zwracane również w przypadku błędów walidacji — nie tylko przy ograniczaniu liczby żądań. Brak `project_id` → > `{"success":false,"data":{"error":{"type":"invalid_data","params":{"project_id":{"_required":"This field is required"},"group_id":{"unauthorized":"Unauthorized access"}}}}}` > (bez `project_id` nie da się potwierdzić dostępu do grupy). Cudzy lub nieistniejący `project_id` / `group_id` zwraca `Unauthorized access` (`418`), a nie `404`. Konto administratora (`role_id = 1`) omija obie kontrole dostępu. ## Powiązane akcje - `getGroupKeywords` — lekka lista (`id` + `keyword`) ograniczona do jednej grupy (ta strona) - `getProjectKeywords` — słowa kluczowe całego projektu (`POST`, `project_id`, bez `group_id`) - `getData` — pełne statystyki pozycji (`POST`, `project_id` + `group_id` + `filtering` + `order`) - `getSerpHtml` — zapis HTML wyników SERP (`POST`, `keyword_id` + `date` + `project_id`) - `getBestKeywords` / `getProjectsStatus` — pozostałe akcje pomocnicze kontrolera --- # Frazy: przypisania grup (`getData`) **`POST /api/rank_tracker/reports/keywords/getData`** Zwraca listę fraz projektu Rank Tracker wraz z informacją o ich przynależności do grup. Każdy element zawiera `id` frazy, jej nazwę (`keyword`) oraz przypisane grupy w dwóch formach: `keyword_groups` (string) i `groups` (tablica). Endpoint **nie zwraca statystyk pozycji** — te udostępnia akcja `getData` kontrolera `positions` (`/api/rank_tracker/reports/positions/getData`). Wynik jest stronicowany. | Fraza | ID | Grupy (string) | Grupy (tablica) | | --- | --- | --- | --- | | kiedy pierwsza cieczka u psa | 8301076 | piesek | ["piesek"] | | jak wozic psa w aucie | 19940324 | piesek | ["piesek"] | _projekt i grupa Rank Trackera, limit: 2. Zwróć uwagę: id frazy to string, a te same nazwy grup zwracane są w dwóch formach — keyword_groups (string) i groups (tablica). Wszystkie adresowalne pola wiersza._ --- ## Żądanie `POST` `/api/rank_tracker/reports/keywords/getData` Nagłówki: `Authorization: Bearer `, `Content-Type: application/json`. ### Struktura żądania **Podstawowy** ```jsonc filename="żądanie-podstawowe.jsonc" // bez group_id — wszystkie frazy projektu { "project_id": null, "limit": 2 } ``` **Rozszerzony** ```jsonc filename="żądanie-rozszerzone.jsonc" // z group_id — tylko frazy wskazanej grupy { "project_id": null, "group_id": null, "limit": 2 } ``` **cURL** ```bash curl --location --request POST 'https://api.senuto.com/api/rank_tracker/reports/keywords/getData' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' \ --data-raw '{ "project_id": null, "group_id": null, "limit": 2 }' ``` ### Parametry ```ts type GetKeywordsDataRequest = { /** * **Wymagane**. ID projektu Rank Tracker — jedyne pole twardo wymagane przez walidator (`GroupKeywordsValidator`). * Musi należeć do użytkownika, inaczej zwracane jest `418` z `Unauthorized access`. * Realny `project_id` pobierzesz z `POST /api/rank_tracker/management/projects/getMyActiveProjects`. */ project_id: number; /** * Opcjonalne. ID grupy słów kluczowych. Bez tego pola endpoint zwraca **wszystkie** frazy projektu * (zwalidowane: `count: "94"`); z nim — tylko frazy wskazanej grupy (zwalidowane: `count: "62"`). */ group_id?: number; /** * Rozmiar strony paginacji. Odbija się w `pagination.limit`. * @default 10 */ limit?: number; /** * Numer strony paginacji. * @default 1 */ page?: number; } export default GetKeywordsDataRequest ``` > **Błąd:** > **`filtering` działa tylko dla pola `keyword`.** Filtr po `keyword` zawęża wynik poprawnie, > ale filtr po `groups` — mimo że to pole jest w odpowiedzi — kończy się `500`. Nieznany klucz > również zwraca `500`, a nie `418`. Puste `filtering` (`[]` albo grupa bez warunków) jest > bezpieczne i nie zmienia wyniku. > **Ostrzeżenie:** > **`order` nie ma efektu na tej akcji** — `dir: "ASC"` i `"DESC"` zwracają wynik w tej samej, > domyślnej kolejności. Sortuj po swojej stronie. > **Ostrzeżenie:** > **Pułapki potwierdzone na produkcji:** > > - `id` frazy oraz `pagination.count` są zwracane jako **stringi** (`"8301076"`, `"94"`) — w odróżnieniu od pozostałych pól paginacji, które są liczbami. Rzutuj je po swojej stronie. > - `keyword_groups` to **jeden string** z nazwami grup rozdzielonymi znakiem **nowej linii** (`\n`), np. `"szcze\nBez grupy"`. Pole `groups` zawiera tę samą informację jako tablicę — używaj `groups`, jeśli nie chcesz parsować stringa. > - `group_id` jest **opcjonalne**: bez niego endpoint zwraca **wszystkie** frazy projektu (`count` podaje ich łączną liczbę), a z nim zawęża wynik do fraz wskazanej grupy (`count: "62"`). ## Odpowiedź Po pomyślnym żądaniu otrzymujesz `data` (tablicę fraz z przypisaniami grup) oraz `pagination`. Zwróć uwagę na typy: `id` frazy i `pagination.count` to stringi, a `keyword_groups` to string wieloliniowy (separator `\n`) — jego tablicowym odpowiednikiem jest `groups`. **Skrócona** ```json filename="przykładowa-odpowiedź (skrócona, z group_id)" { "success": true, "data": [ { "id": "8301076", "keyword": "kiedy pierwsza cieczka u psa", "keyword_groups": "piesek", "groups": ["piesek"] } ], "pagination": { "page_count": 31, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": "62", "limit": 2 } } ``` **Pełna** ```json filename="przykładowa-odpowiedź (zwalidowana, 200, bez group_id)" { "success": true, "data": [ { "id": "1053348", "keyword": "owczarek szkocki", "keyword_groups": "szcze\nBez grupy", "groups": ["szcze", "Bez grupy"] }, { "id": "8290613", "keyword": "cieczka u psa co ile", "keyword_groups": "szcze\nBez grupy", "groups": ["szcze", "Bez grupy"] } ], "pagination": { "page_count": 47, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": "94", "limit": 2 } } ``` ### Struktura odpowiedzi ```ts type GetKeywordsDataResponse = { /** `true` przy powodzeniu; przy błędzie `false` i koperta z `error` */ success: boolean; /** Frazy projektu (lub grupy, jeśli podano `group_id`) z przypisaniami grup */ data: KeywordWithGroups[]; /** Metadane paginacji */ pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; /** **Uwaga:** string, nie liczba (np. `"94"`) */ count: string; limit: number; }; } type KeywordWithGroups = { /** ID frazy — **uwaga:** string, nie liczba (np. `"8301076"`) */ id: string; /** Nazwa frazy */ keyword: string; /** Nazwy grup jako jeden string rozdzielony znakiem nowej linii (`\n`), np. `"szcze\nBez grupy"` */ keyword_groups: string; /** Te same nazwy grup jako tablica — preferowana forma do przetwarzania */ groups: string[]; } export default GetKeywordsDataResponse ``` ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data, unknown */ type: string; message: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > **`418`** jest zwracane przy błędach walidacji (`invalid_data`) — nie tylko przy ograniczaniu liczby żądań. Brak `project_id` → `418` z `{"project_id":{"_required":"This field is required"}}`. Cudzy lub nieistniejący `project_id` zwraca `418` z `Unauthorized access`, a nie `404`. ## Powiązane akcje - `getData` — frazy projektu z przypisaniami grup, bez statystyk pozycji (ta strona) - `getProjectKeywords` — słowa kluczowe całego projektu - `getGroupKeywords` — lekka lista (`id` + `keyword`) ograniczona do jednej grupy - `getSerpHtml` — zapis HTML wyników SERP - `getBestKeywords` / `getProjectsStatus` — pozostałe akcje pomocnicze kontrolera --- # Konkurenci: ranking (`getRanking`) **`POST /api/rank_tracker/reports/competitors/getRanking`** Zwraca ranking domeny projektu oraz jej konkurentów według statystyk pozycji i widoczności — porównanie dwóch snapshotów pomiarowych. Dla każdej domeny otrzymujesz zestaw około 26 metryk (rozkład pozycji TOP3/TOP10/TOP50, widoczność organiczna, potencjał, budżet itd.), a każda metryka zawiera wartość starszą, nowszą, różnicę i zmianę procentową. Wynik jest stronicowany po domenach. | Domena | ID | Domena projektu | Widoczność org. | Średnia pozycja | TOP3 | TOP10 | TOP50 | Zyskane | Utracone | Potencjał org. | Koszt PPC | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | pies.pl | 87944 | true | 0 | 50 | 0 | 0 | 0 | 0 | 0 | 1100.0399999999984 | 0 | | fajnyzwierzak.pl | 55578 | | | 45.99 | | | 22 | 14 | 8 | | | _wiersze z data.data — podwójna koperta: domena projektu (project_domain: true) i konkurent. Pokazano identyfikatory i najważniejsze metryki (recent_value); komplet ~24 metryk (starsza/nowsza/różnica/%) jest w JSON i sekcji „Struktura odpowiedzi”._ --- ## Żądanie `POST` `/api/rank_tracker/reports/competitors/getRanking` Nagłówki: `Authorization: Bearer `, `Content-Type: application/json`. ### Struktura żądania **Podstawowy** ```jsonc filename="żądanie-podstawowe.jsonc" { "project_id": null, "date_min": "2026-06-20", "date_max": "2026-06-29" } ``` **Rozszerzony** ```jsonc filename="żądanie-rozszerzone.jsonc" { "project_id": null, "date_min": "2026-06-20", "date_max": "2026-06-29", "limit": 2 } ``` **cURL** ```bash curl --location --request POST 'https://api.senuto.com/api/rank_tracker/reports/competitors/getRanking' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' \ --data-raw '{ "project_id": null, "date_min": "2026-06-20", "date_max": "2026-06-29", "limit": 2 }' ``` ### Parametry ```ts type GetRankingRequest = { /** * **Wymagane** (walidator `CompetitorsValidator`). ID projektu Rank Tracker. * Musi należeć do użytkownika — w przeciwnym razie `418` z komunikatem `Unauthorized access`. * Realny `project_id` pobierzesz z `POST /api/rank_tracker/management/projects/getMyActiveProjects`. */ project_id: number; /** * **Wymagane**. Początek zakresu porównania w formacie `YYYY-MM-DD`. Nie może być późniejszy niż dziś * ani późniejszy niż `date_max`. Uwaga: jako starszy snapshot API bierze **poprzedni dostępny pomiar** * względem `date_max` (patrz pole `dates` w odpowiedzi), więc `date_min` nie zawsze będzie datą, * z którą realnie porównano wyniki. */ date_min: string; /** * **Wymagane**. Koniec zakresu porównania w formacie `YYYY-MM-DD` (≤ dziś, ≥ `date_min`). * Odpowiada `dates.last_date` w odpowiedzi. */ date_max: string; /** * Rozmiar strony paginacji — liczonej po **domenach** (projekt + konkurenci). * @default 10 */ limit?: number; /** * Numer strony paginacji. * @default 1 */ page?: number; } export default GetRankingRequest ``` > **Ostrzeżenie:** > **Znany bug walidatora:** gdy `date_min` jest późniejsze niż `date_max`, komunikaty błędów z `DateRangeRules` są **odwrócone** (komunikat o `date_min` dotyczy `date_max` i odwrotnie). Interpretuj wtedy błąd „na krzyż". > **Ostrzeżenie:** > **Pułapki tego endpointu:** > > 1. **Podwójna koperta** — zewnętrzne pole `data` zawiera **własny obiekt** z polami `{success, rows, dates, data[]}`. Lista domen siedzi więc w `data.data`, a nie bezpośrednio w `data`. > 2. **`dates` pokazuje faktycznie porównane snapshoty, a nie Twój zakres.** `last_date` odpowiada `date_max`, ale `previous_data` to **poprzedni dostępny pomiar** — w zwalidowanym przykładzie `2026-06-28`, mimo że w żądaniu podano `date_min: 2026-06-20`. Każde z pól to obiekt `{timestamp, formatted}`. > 3. **Wiersz domeny projektu różni się od wierszy konkurentów** — ma `project_domain: true` i `selectable: false`, natomiast wiersze konkurentów mają zamiast tego tablice `categories[]` i `technologies[]`. > 4. **Duplikaty i równoległe konwencje w `statistics`** — metryki `out_50` i `out50` występują jednocześnie, podobnie jak przedziały w dwóch notacjach (`top3`/`top4_10`/`top11_20`/`top21_50` obok `1_3`/`4_10`/`11_20`/`21_50`). > 5. **Paginacja działa na poziomie zewnętrznym** i liczy **domeny** (`count: 4` = domena projektu + 3 konkurentów), a nie frazy. ## Odpowiedź Zwróć uwagę na **podwójną kopertę**: zewnętrzne `data` to obiekt z własnymi polami `success`, `rows` (łączna liczba domen), `dates` (faktycznie porównane snapshoty) i `data` (lista domen). Metadane `pagination` leżą na poziomie zewnętrznym, obok koperty. W skróconym przykładzie poniżej drugi wiersz (konkurent) pokazuje tylko część metryk — w oryginalnej odpowiedzi każdy wiersz zawiera **komplet tych samych około 26 metryk** co wiersz domeny projektu, każda w formacie `{older_value, recent_value, diff, percent}`. **Skrócona** ```json filename="przykładowa-odpowiedź (skrócona)" { "success": true, "data": { "success": true, "rows": 4, "dates": { "last_date": { "timestamp": 1782691200, "formatted": "2026-06-29" }, "previous_data": { "timestamp": 1782604800, "formatted": "2026-06-28" } }, "data": [ { "id": 87944, "domain": "pies.pl", "project_domain": true, "selectable": false, "statistics": { "organic_potential": { "older_value": 1100.0399999999984, "recent_value": 1100.0399999999984, "diff": 0, "percent": 0 }, "avg_pos": { "older_value": 50, "recent_value": 50, "diff": 0, "percent": 0 }, "out_50": { "older_value": 94, "recent_value": 94, "diff": 0, "percent": 0 } } } ] }, "pagination": { "page_count": 2, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 4, "limit": 2 } } ``` **Pełna** ```json filename="przykładowa-odpowiedź (zwalidowana, 200; 2 z 4 domen)" { "success": true, "data": { "success": true, "rows": 4, "dates": { "last_date": { "timestamp": 1782691200, "formatted": "2026-06-29" }, "previous_data": { "timestamp": 1782604800, "formatted": "2026-06-28" } }, "data": [ { "id": 87944, "domain": "pies.pl", "project_domain": true, "selectable": false, "statistics": { "organic_potential": { "older_value": 1100.0399999999984, "recent_value": 1100.0399999999984, "diff": 0, "percent": 0 }, "ad_keywords": { "older_value": 0, "recent_value": 0, "diff": 0, "percent": 0 }, "ad_pos_avg": { "older_value": 0, "recent_value": 0, "diff": 0, "percent": 0 }, "ad_visibility": { "older_value": 0, "recent_value": 0, "diff": 0, "percent": 0 }, "lost": { "older_value": 0, "recent_value": 0, "diff": 0, "percent": 0 }, "no_change": { "older_value": 94, "recent_value": 94, "diff": 0, "percent": 0 }, "out_50": { "older_value": 94, "recent_value": 94, "diff": 0, "percent": 0 }, "avg_pos": { "older_value": 50, "recent_value": 50, "diff": 0, "percent": 0 }, "ppc_cost": { "older_value": 0, "recent_value": 0, "diff": 0, "percent": 0 }, "top10": { "older_value": 0, "recent_value": 0, "diff": 0, "percent": 0 }, "top11_20": { "older_value": 0, "recent_value": 0, "diff": 0, "percent": 0 }, "top21_50": { "older_value": 0, "recent_value": 0, "diff": 0, "percent": 0 }, "top3": { "older_value": 0, "recent_value": 0, "diff": 0, "percent": 0 }, "top4_10": { "older_value": 0, "recent_value": 0, "diff": 0, "percent": 0 }, "top50": { "older_value": 0, "recent_value": 0, "diff": 0, "percent": 0 }, "organic_visibility": { "older_value": 0, "recent_value": 0, "diff": 0, "percent": 0 }, "wins": { "older_value": 0, "recent_value": 0, "diff": 0, "percent": 0 }, "1_3": { "older_value": 0, "recent_value": 0, "diff": 0, "percent": 0 }, "4_10": { "older_value": 0, "recent_value": 0, "diff": 0, "percent": 0 }, "11_20": { "older_value": 0, "recent_value": 0, "diff": 0, "percent": 0 }, "21_50": { "older_value": 0, "recent_value": 0, "diff": 0, "percent": 0 }, "out50": { "older_value": 94, "recent_value": 94, "diff": 0, "percent": 0 }, "budget": { "older_value": 0, "recent_value": 0, "diff": 0, "percent": 0 }, "organic_potential_coverage": { "older_value": 0, "recent_value": 0, "diff": 0, "percent": 0 } } }, { "id": 55578, "domain": "fajnyzwierzak.pl", "categories": [], "technologies": [], "statistics": { "lost": { "older_value": 8, "recent_value": 8, "diff": 0, "percent": 0 }, "no_change": { "older_value": 72, "recent_value": 72, "diff": 0, "percent": 0 }, "out_50": { "older_value": 75, "recent_value": 72, "diff": -3, "percent": -0.04 }, "avg_pos": { "older_value": 46.33, "recent_value": 45.99, "diff": -0.34, "percent": -0.0073 }, "top11_20": { "older_value": 4, "recent_value": 2, "diff": -2, "percent": -0.5 }, "top21_50": { "older_value": 15, "recent_value": 20, "diff": 5, "percent": 0.3333 }, "top50": { "older_value": 19, "recent_value": 22, "diff": 3, "percent": 0.1579 }, "wins": { "older_value": 14, "recent_value": 14, "diff": 0, "percent": 0 } } } ] }, "pagination": { "page_count": 2, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 4, "limit": 2 } } ``` ### Struktura odpowiedzi ```ts type GetRankingResponse = { /** Flaga przetworzenia żądania (poziom zewnętrzny) */ success: boolean; /** Wewnętrzna koperta — uwaga na podwójne zagnieżdżenie */ data: { /** Flaga przetworzenia (poziom wewnętrzny) */ success: boolean; /** Łączna liczba domen w rankingu (projekt + konkurenci) */ rows: number; /** * Faktycznie porównane snapshoty. `last_date` = `date_max` z żądania; * `previous_data` = poprzedni dostępny pomiar (niekoniecznie `date_min`!). */ dates: { last_date: SnapshotDate; previous_data: SnapshotDate; }; /** Wiersze rankingu — jedna pozycja na domenę */ data: RankingRow[]; }; /** Paginacja na poziomie zewnętrznym; `count` = liczba domen */ pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; limit: number | null; }; } type SnapshotDate = { /** Uniksowy znacznik czasu snapshotu */ timestamp: number; /** Data w formacie YYYY-MM-DD */ formatted: string; } type RankingRow = { /** ID projektu (dla domeny projektu) lub ID konkurenta */ id: number; /** Nazwa domeny */ domain: string; /** `true` tylko w wierszu domeny projektu */ project_domain?: boolean; /** `false` w wierszu domeny projektu (nie można jej odznaczyć) */ selectable?: boolean; /** Tylko w wierszach konkurentów */ categories?: string[]; /** Tylko w wierszach konkurentów */ technologies?: string[]; /** * Około 26 metryk: organic_potential, ad_keywords, ad_pos_avg, ad_visibility, lost, no_change, * out_50, avg_pos, ppc_cost, top10, top11_20, top21_50, top3, top4_10, top50, organic_visibility, * wins, 1_3, 4_10, 11_20, 21_50, out50, budget, organic_potential_coverage. * Uwaga: `out_50`/`out50` to duplikaty, a przedziały pozycji występują równolegle * w notacji `topX` i `X_Y`. */ statistics: Record; } type MetricComparison = { /** Wartość w starszym snapshocie (`dates.previous_data`) */ older_value: number; /** Wartość w nowszym snapshocie (`dates.last_date`) */ recent_value: number; /** Różnica: recent_value - older_value */ diff: number; /** Zmiana procentowa jako ułamek (np. -0.04 = -4%) */ percent: number; } export default GetRankingResponse ``` ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data, unknown */ type: string; message: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > Błędy walidacji zwracane są jako **`418`**. Brak `project_id`, `date_min` lub `date_max` → `invalid_data`. Daty muszą być w formacie `YYYY-MM-DD`, nie późniejsze niż dziś, a `date_min` ≤ `date_max` — przy odwróconym zakresie pamiętaj o **zamienionych komunikatach** z `DateRangeRules`. Cudzy lub nieistniejący `project_id` → `418` z `Unauthorized access`, a nie `404`. ## Powiązane akcje - `getRanking` — ranking domen wg statystyk pozycji/widoczności dla dwóch snapshotów (ta strona) - `getMatrix` — macierz fraza × domena z pozycjami projektu i konkurentów w dwóch datach (`POST`, te same wymagane parametry) - Lista konkurentów projektu (domeny widoczne w obu raportach) pochodzi z `GET /api/rank_tracker/management/competitors/list` --- # Konkurenci: macierz pozycji (`getMatrix`) **`POST /api/rank_tracker/reports/competitors/getMatrix`** Zwraca macierz fraza × domena: dla każdej frazy monitorowanej w projekcie Rank Tracker otrzymujesz pozycje domeny projektu oraz wszystkich jej konkurentów w dwóch datach (`date_min` i `date_max`). Oprócz pozycji każdy wiersz zawiera podstawowe metryki frazy (widoczność, potencjał, CPC, liczbę wyszukiwań, snippety SERP). Wynik jest stronicowany po frazach. --- ## Żądanie `POST` `/api/rank_tracker/reports/competitors/getMatrix` Nagłówki: `Authorization: Bearer `, `Content-Type: application/json`. ### Struktura żądania **Podstawowy** ```jsonc filename="żądanie-podstawowe.jsonc" { "project_id": null, "date_min": "2026-06-20", "date_max": "2026-06-29" } ``` **Rozszerzony** ```jsonc filename="żądanie-rozszerzone.jsonc" { "project_id": null, "date_min": "2026-06-20", "date_max": "2026-06-29", "limit": 2 } ``` **cURL** ```bash curl --location --request POST 'https://api.senuto.com/api/rank_tracker/reports/competitors/getMatrix' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' \ --data-raw '{ "project_id": null, "date_min": "2026-06-20", "date_max": "2026-06-29", "limit": 2 }' ``` ### Parametry ```ts type GetMatrixRequest = { /** * **Wymagane** (walidator `CompetitorsValidator`). ID projektu Rank Tracker. * Musi należeć do użytkownika — w przeciwnym razie `418` z komunikatem `Unauthorized access`. * Realny `project_id` pobierzesz z `POST /api/rank_tracker/management/projects/getMyActiveProjects`. */ project_id: number; /** * **Wymagane**. Starsza z dwóch porównywanych dat w formacie `YYYY-MM-DD` (≤ dziś, ≤ `date_max`). * W odpowiedzi staje się kluczem pierwszego snapshotu w mapie każdej domeny. */ date_min: string; /** * **Wymagane**. Nowsza z dwóch porównywanych dat w formacie `YYYY-MM-DD` (≤ dziś, ≥ `date_min`). * W odpowiedzi staje się kluczem drugiego snapshotu — to przy nim pojawiają się * `has_changes`, `is_grown` i `diff`. */ date_max: string; /** * Filtrowanie po **frazach** macierzy. Dozwolone klucze m.in.: `position`, `current_position`, * `last_position`, `searches`, `cpc`, `visibility`, `keywords`, `url`. * Operatory liczbowe: `gt` | `gte` | `lt` | `lte` | `eq`. * ⚠️ **Uwaga:** ten endpoint (backend MySQL) na **nieznany lub źle sformułowany** filtr zwraca * `HTTP 500` (nie `418`) — trzymaj się kluczy z listy i poprawnych typów wartości. */ filtering?: Array<{ filters: Array<{ key: string; match?: 'gt' | 'gte' | 'lt' | 'lte' | 'eq'; value: string | number | Array; complement?: boolean }>; conjunction?: 'and' | 'or'; }>; /** * Rozmiar strony paginacji — liczonej po **frazach**. * @default 10 */ limit?: number; /** * Numer strony paginacji. * @default 1 */ page?: number; } export default GetMatrixRequest ``` > **Ostrzeżenie:** > **Znany bug walidatora:** gdy `date_min` jest późniejsze niż `date_max`, komunikaty błędów z `DateRangeRules` są **odwrócone** (komunikat o `date_min` dotyczy `date_max` i odwrotnie). Interpretuj wtedy błąd „na krzyż". > **Ostrzeżenie:** > **Pułapki tego endpointu:** > > 1. **Klucze dynamiczne** — każdy wiersz ma po jednym polu **na każdą domenę** (np. `"pies.pl"`, `"psy.pl"`, `"wamiz.pl"`, `"fajnyzwierzak.pl"`). Wartością jest mapa data → snapshot: pod kluczem `date_min` znajdziesz tylko `{position}`, a pod kluczem `date_max` — `{position, has_changes, is_grown, diff}`, przy czym `is_grown` pojawia się **tylko gdy** `has_changes: true`. Nie da się więc zdeserializować wiersza do sztywnego typu — użyj mapy. > 2. **`position: 0` oznacza brak domeny w TOP50** dla danej frazy — to nie jest pozycja pierwsza ani błąd. > 3. **`searches` to string** (np. `"30"`), a nie liczba — dotyczy to zarówno pola na poziomie wiersza, jak i `statistics.searches.current`. > 4. **`status: "complete"`** oznacza, że fraza została w pełni przetworzona dla porównywanych dat. > 5. **Paginacja liczona jest po frazach** (`count: 94` = liczba fraz w projekcie), inaczej niż w `getRanking`, gdzie liczy domeny. ## Odpowiedź `data` to tablica wierszy — po jednym na frazę. Każdy wiersz łączy stałe pola frazy (`keyword`, `status`, `searches`, `snippets`, metryki) z **dynamicznymi kluczami domen**: domena projektu i każdy konkurent to osobne pole, którego nazwa jest nazwą domeny, a wartość — mapą data → snapshot pozycji. Pamiętaj, że `position: 0` oznacza brak w TOP50, a `searches` jest stringiem. **Skrócona** ```json filename="przykładowa-odpowiedź (skrócona)" { "success": true, "data": [ { "keyword": "ridgeback tajski", "status": "complete", "searches": "30", "pies.pl": { "2026-06-20": { "position": 0 }, "2026-06-29": { "position": 0, "has_changes": false, "diff": 0 } }, "psy.pl": { "2026-06-20": { "position": 2 }, "2026-06-29": { "position": 10, "has_changes": true, "is_grown": false, "diff": 8 } } } ], "pagination": { "page_count": 47, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 94, "limit": 2 } } ``` **Pełna** ```json filename="przykładowa-odpowiedź (zwalidowana, 200; 2 z 94 wierszy)" { "success": true, "data": [ { "organic_visibility": 0, "organic_potential": 11, "cpc": 0, "keyword": "ridgeback tajski", "status": "complete", "searches": "30", "snippets": ["people_also_ask", "people_also_search_products", "related_searches", "spell", "wiki_right"], "pies.pl": { "2026-06-20": { "position": 0 }, "2026-06-29": { "position": 0, "has_changes": false, "diff": 0 } }, "psy.pl": { "2026-06-20": { "position": 2 }, "2026-06-29": { "position": 10, "has_changes": true, "is_grown": false, "diff": 8 } }, "wamiz.pl": { "2026-06-20": { "position": 0 }, "2026-06-29": { "position": 0, "has_changes": false, "diff": 0 } }, "fajnyzwierzak.pl": { "2026-06-20": { "position": 0 }, "2026-06-29": { "position": 0, "has_changes": false, "diff": 0 } }, "statistics": { "visibility": { "current": 0 }, "cpc": { "current": 0 }, "searches": { "current": "30" }, "snippets": { "current": ["people_also_ask", "people_also_search_products", "related_searches", "spell", "wiki_right"] } } }, { "organic_visibility": 0, "organic_potential": 11, "cpc": 0, "keyword": "pies się trzęsie", "status": "complete", "searches": "30", "snippets": ["people_also_ask", "related_searches"], "pies.pl": { "2026-06-20": { "position": 0 }, "2026-06-29": { "position": 0, "has_changes": false, "diff": 0 } }, "psy.pl": { "2026-06-20": { "position": 14 }, "2026-06-29": { "position": 14, "has_changes": false, "diff": 0 } }, "wamiz.pl": { "2026-06-20": { "position": 15 }, "2026-06-29": { "position": 15, "has_changes": false, "diff": 0 } }, "fajnyzwierzak.pl": { "2026-06-20": { "position": 26 }, "2026-06-29": { "position": 35, "has_changes": true, "is_grown": false, "diff": 9 } }, "statistics": { "visibility": { "current": 0 }, "cpc": { "current": 0 }, "searches": { "current": "30" }, "snippets": { "current": ["people_also_ask", "related_searches"] } } } ], "pagination": { "page_count": 47, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 94, "limit": 2 } } ``` ### Struktura odpowiedzi ```ts type GetMatrixResponse = { /** `true` przy powodzeniu; przy błędzie `false` i koperta z `error` */ success: boolean; /** Wiersze macierzy — jeden na frazę */ data: MatrixRow[]; /** Paginacja liczona po frazach (`count` = liczba fraz w projekcie) */ pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; limit: number | null; }; } /** * Uwaga: oprócz pól stałych każdy wiersz zawiera **dynamiczne klucze domen** * (np. "pies.pl", "psy.pl") — po jednym na domenę projektu i każdego konkurenta. */ type MatrixRow = { /** Monitorowana fraza */ keyword: string; /** "complete" = fraza w pełni przetworzona dla porównywanych dat */ status: string; /** Liczba wyszukiwań miesięcznie — UWAGA: string, np. "30" */ searches: string; /** Widoczność organiczna frazy */ organic_visibility: number; /** Potencjał organiczny frazy */ organic_potential: number; /** Koszt kliknięcia */ cpc: number; /** Elementy SERP obecne dla frazy, np. "people_also_ask", "related_searches" */ snippets: string[]; /** Bieżące metryki frazy w formacie `{ current: ... }` */ statistics: { visibility: { current: number }; cpc: { current: number }; /** UWAGA: string */ searches: { current: string }; snippets: { current: string[] }; }; /** * Dynamiczne klucze domen: mapa data (YYYY-MM-DD) → snapshot pozycji. * Pod kluczem `date_min` tylko `{ position }`; pod kluczem `date_max` pełny `PositionSnapshot`. */ [domain: string]: Record | unknown; } type PositionSnapshot = { /** Pozycja w SERP; 0 = brak domeny w TOP50 */ position: number; /** Tylko pod kluczem `date_max`: czy pozycja zmieniła się względem `date_min` */ has_changes?: boolean; /** Tylko gdy `has_changes: true`: `true` = wzrost (pozycja bliżej 1), `false` = spadek */ is_grown?: boolean; /** Tylko pod kluczem `date_max`: bezwzględna wielkość zmiany pozycji */ diff?: number; } export default GetMatrixResponse ``` ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data, unknown */ type: string; message: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > Błędy walidacji zwracane są jako **`418`**. Brak `project_id`, `date_min` lub `date_max` → `invalid_data`. Daty muszą być w formacie `YYYY-MM-DD`, nie późniejsze niż dziś, a `date_min` ≤ `date_max` — przy odwróconym zakresie pamiętaj o **zamienionych komunikatach** z `DateRangeRules`. Cudzy lub nieistniejący `project_id` → `418` z `Unauthorized access`, a nie `404`. ## Powiązane akcje - `getMatrix` — macierz fraza × domena z pozycjami projektu i konkurentów w dwóch datach (ta strona) - `getRanking` — ranking domen wg statystyk pozycji/widoczności dla dwóch snapshotów (`POST`, te same wymagane parametry) - Lista konkurentów projektu (domeny widoczne w obu raportach) pochodzi z `GET /api/rank_tracker/management/competitors/list` --- # Lista konkurentów (`list`) **`GET /api/rank_tracker/management/competitors/list`** Zwraca listę konkurentów skonfigurowanych dla danego projektu Monitoringu (Rank Tracker). Konkurenci to domeny, które porównujesz z własnym projektem w raportach pozycji i widoczności. Odpowiedź zawiera tablicę `data` oraz metadane `pagination`. --- ## Żądanie `GET` `/api/rank_tracker/management/competitors/list` Nagłówki: `Authorization: Bearer `. Parametry przekazywane w query stringu. ### Struktura żądania **Podstawowy** ```jsonc filename="żądanie-podstawowe.jsonc" { "project_id": null } ``` **Rozszerzony** ```jsonc filename="żądanie-rozszerzone.jsonc" { "project_id": null, "limit": 50, "page": 1 } ``` **cURL** ```bash curl --location --request GET 'https://api.senuto.com/api/rank_tracker/management/competitors/list?project_id=124572' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' ``` ### Parametry ```ts type CompetitorsListRequest = { /** * **Wymagane**. Identyfikator projektu Monitoringu, dla którego zwracana jest lista konkurentów. * Przekazywany w query stringu (`?project_id=…`). * Realny `project_id` pobierzesz z `POST /api/rank_tracker/management/projects/getMyActiveProjects`. */ project_id: number; /** * Liczba wierszy na stronę. Nieujemna liczba całkowita. Gdy pominięty, API może zwrócić wszystkie * wiersze (`limit: null`). */ limit?: number; /** * Numer strony. Nieujemna liczba całkowita. * @default 1 */ page?: number; } export default CompetitorsListRequest ``` > **Ostrzeżenie:** > Endpoint obsługuje metodę **`GET`** — parametr **`project_id`** jest **wymagany** i przekazywany w **query stringu** (np. `?project_id=124572`), a nie w treści żądania. Wysłanie danych w body skutkuje odpowiedzią `418` / `405`. > **Ostrzeżenie:** > Przykładowy projekt nie zwrócił danych dla tego raportu (`data: []`) — nie miał skonfigurowanych konkurentów. Poniżej udokumentowano **strukturę** odpowiedzi i pojedynczego wiersza konkurenta na podstawie analizy schematu; nie zawiera ona zmyślonych wartości. ## Odpowiedź Po pomyślnym żądaniu otrzymujesz `data` (tablicę konkurentów) oraz `pagination`. Jeśli projekt nie ma skonfigurowanych konkurentów, `data` jest pustą tablicą, a `pagination.count` wynosi `0`. **Skrócona** ```json filename="przykładowa-odpowiedź (skrócona)" { "success": true, "data": [], "pagination": { "count": 0, "current_page": 1, "page_count": 1 } } ``` **Pełna** ```json filename="przykładowa-odpowiedź (200)" { "success": true, "data": [], "pagination": { "page_count": 1, "current_page": 1, "has_next_page": false, "has_prev_page": false, "count": 0, "limit": null } } ``` ### Struktura odpowiedzi ```ts type CompetitorsListResponse = { /** `true` przy powodzeniu; przy błędzie `false` i koperta z `error` */ success: boolean; /** Lista konkurentów projektu (pusta, gdy żaden nie został skonfigurowany) */ data: Competitor[]; /** Metadane paginacji */ pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; /** Łączna liczba konkurentów */ count: number; /** Limit wierszy na stronę; `null`, gdy nie ustawiono */ limit: number | null; }; } // Struktura pojedynczego wiersza konkurenta (gdy `data` nie jest puste). // Pola mogą się różnić w zależności od konfiguracji projektu. type Competitor = { /** Identyfikator konkurenta w obrębie projektu */ id: number; /** Identyfikator projektu, do którego należy konkurent */ project_id: number; /** Domena konkurenta */ domain: string; } export default CompetitorsListResponse ``` ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data, not_found, unreachable_data, timeout, database, unknown */ type: string; message: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > **`418`** jest zwracane również w przypadku błędów walidacji — nie tylko przy ograniczaniu liczby żądań. Brak `project_id` lub przekazanie parametrów w treści żądania zamiast w query stringu skutkuje błędem walidacji (`invalid_data`) lub `405`. ## Powiązane akcje - `list` — lista konkurentów projektu (ta strona) - `getRanking` (`/api/rank_tracker/reports/competitors/getRanking`) — ranking konkurentów względem projektu - `getMatrix` (`/api/rank_tracker/reports/competitors/getMatrix`) — macierz pokrycia fraz przez konkurentów --- # Snippety SERP: statystyki (`getStatistics`) **`POST /api/rank_tracker/reports/snippets/getStatistics`** Zwraca statystyki elementów SERP (snippetów) wykrytych dla fraz projektu Rank Tracker. Dla każdego typu snippetu akcja porównuje dwa snapshoty — `date_min` (wartości `previous`) i `date_max` (wartości `recent`) — i raportuje, ile fraz projektu ma dany snippet w SERP (`all_keywords`) oraz w ilu z nich widoczna jest domena projektu (`visible_keywords`), wraz z różnicami bezwzględnymi i procentowymi oraz procentowym pokryciem SERP (`serp_coverage`). Powyższy przykład w playgroundzie został **skrócony do 4 typów snippetów** dla czytelności — pełną, zwalidowaną odpowiedź (14 typów) znajdziesz niżej w sekcji „Odpowiedź". --- ## Żądanie `POST` `/api/rank_tracker/reports/snippets/getStatistics` Nagłówki: `Authorization: Bearer `, `Content-Type: application/json`. ### Struktura żądania **Podstawowy** ```jsonc filename="żądanie-podstawowe.jsonc" { "project_id": null, "date_min": "2026-06-20", "date_max": "2026-06-29" } ``` **cURL** ```bash curl --location --request POST 'https://api.senuto.com/api/rank_tracker/reports/snippets/getStatistics' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' \ --data-raw '{ "project_id": null, "date_min": "2026-06-20", "date_max": "2026-06-29" }' ``` ### Parametry ```ts type GetSnippetsStatisticsRequest = { /** * **Wymagane**. ID projektu Rank Tracker (walidator `SnippetsValidator`). * Musi należeć do użytkownika, inaczej zwracane jest `418` z komunikatem `Unauthorized access`. * Realny `project_id` pobierzesz z `POST /api/rank_tracker/management/projects/getMyActiveProjects`. */ project_id: number; /** * **Wymagane**. Data początkowa zakresu w formacie `YYYY-MM-DD` — snapshot bazowy, * którego wartości trafiają do pól `previous`. */ date_min: string; /** * **Wymagane**. Data końcowa zakresu w formacie `YYYY-MM-DD` — snapshot bieżący, * którego wartości trafiają do pól `recent`. */ date_max: string; } export default GetSnippetsStatisticsRequest ``` > **Ostrzeżenie:** > Znany bug walidacji: gdy `date_min` jest **późniejsze** niż `date_max`, reguły `DateRangeRules` zwracają błąd z **odwróconym komunikatem** (treść sugeruje odwrotny kierunek naruszenia). Pilnuj poprawnej kolejności dat po swojej stronie. > **Ostrzeżenie:** > **Pułapki tego endpointu:** (1) `data` to **obiekt keyed by typ snippetu** (klucze dynamiczne, np. `image_thumbs`, `wiki_right`), a nie tablica — iteruj po kluczach, nie po indeksach. (2) `serp_coverage` to **string** (np. `"29.79"` = procent pokrycia SERP), nie liczba. (3) `diff_percent` bywa **liczbą `0` albo stringiem** (np. `"-24.32"`) — parsuj defensywnie. (4) Odpowiedź **nie ma paginacji**. (5) Zbiór typów snippetów w odpowiedzi może być **podzbiorem** wszystkich typów — siostrzana akcja `getHistory` dla tego samego projektu zwróciła dodatkowo m.in. `people_also_ask`, `related_searches`, `video_thumbs`, `videos_pack`, `featured_snippets` i `answer_box`; zbiór kluczy traktuj jako dynamiczny. ## Odpowiedź Po pomyślnym żądaniu otrzymujesz `data` — **obiekt**, którego kluczami są typy snippetów. Każdy wpis zawiera `serp_coverage` (string z procentem pokrycia SERP), powtórzoną nazwę typu w polu `snippet` oraz dwa bloki liczników: `all_keywords` (frazy projektu z danym snippetem w SERP) i `visible_keywords` (frazy, w których snippecie widoczna jest domena projektu). Każdy blok liczników ma `recent` (stan z `date_max`), `previous` (stan z `date_min`), `diff` oraz `diff_percent`. **Skrócona** ```json filename="przykładowa-odpowiedź (skrócona)" { "success": true, "data": { "image_thumbs": { "serp_coverage": "29.79", "snippet": "image_thumbs", "all_keywords": { "recent": 28, "previous": 37, "diff": -9, "diff_percent": "-24.32" }, "visible_keywords": { "recent": 0, "previous": 0, "diff": 0, "diff_percent": 0 } }, "wiki_right": { "serp_coverage": "12.77", "snippet": "wiki_right", "all_keywords": { "recent": 12, "previous": 9, "diff": 3, "diff_percent": "33.33" }, "visible_keywords": { "recent": 0, "previous": 0, "diff": 0, "diff_percent": 0 } } } } ``` **Pełna** ```json filename="przykładowa-odpowiedź (200)" { "success": true, "data": { "news": { "serp_coverage": "0.00", "snippet": "news", "all_keywords": { "recent": 0, "previous": 0, "diff": 0, "diff_percent": 0 }, "visible_keywords": { "recent": 0, "previous": 0, "diff": 0, "diff_percent": 0 } }, "image_thumbs": { "serp_coverage": "29.79", "snippet": "image_thumbs", "all_keywords": { "recent": 28, "previous": 37, "diff": -9, "diff_percent": "-24.32" }, "visible_keywords": { "recent": 0, "previous": 0, "diff": 0, "diff_percent": 0 } }, "wiki_right": { "serp_coverage": "12.77", "snippet": "wiki_right", "all_keywords": { "recent": 12, "previous": 9, "diff": 3, "diff_percent": "33.33" }, "visible_keywords": { "recent": 0, "previous": 0, "diff": 0, "diff_percent": 0 } }, "map": { "serp_coverage": "0.00", "snippet": "map", "all_keywords": { "recent": 0, "previous": 1, "diff": -1, "diff_percent": "-100.00" }, "visible_keywords": { "recent": 0, "previous": 0, "diff": 0, "diff_percent": 0 } }, "top_video": { "serp_coverage": "0.00", "snippet": "top_video", "all_keywords": { "recent": 0, "previous": 0, "diff": 0, "diff_percent": 0 }, "visible_keywords": { "recent": 0, "previous": 0, "diff": 0, "diff_percent": 0 } }, "spell": { "serp_coverage": "3.19", "snippet": "spell", "all_keywords": { "recent": 3, "previous": 3, "diff": 0, "diff_percent": "0.00" }, "visible_keywords": { "recent": 0, "previous": 0, "diff": 0, "diff_percent": 0 } }, "top_info": { "serp_coverage": "0.00", "snippet": "top_info", "all_keywords": { "recent": 0, "previous": 0, "diff": 0, "diff_percent": 0 }, "visible_keywords": { "recent": 0, "previous": 0, "diff": 0, "diff_percent": 0 } }, "top_bar": { "serp_coverage": "0.00", "snippet": "top_bar", "all_keywords": { "recent": 0, "previous": 0, "diff": 0, "diff_percent": 0 }, "visible_keywords": { "recent": 0, "previous": 0, "diff": 0, "diff_percent": 0 } }, "top_scholar": { "serp_coverage": "0.00", "snippet": "top_scholar", "all_keywords": { "recent": 0, "previous": 0, "diff": 0, "diff_percent": 0 }, "visible_keywords": { "recent": 0, "previous": 0, "diff": 0, "diff_percent": 0 } }, "top_number_results": { "serp_coverage": "0.00", "snippet": "top_number_results", "all_keywords": { "recent": 0, "previous": 0, "diff": 0, "diff_percent": 0 }, "visible_keywords": { "recent": 0, "previous": 0, "diff": 0, "diff_percent": 0 } }, "top_public_data": { "serp_coverage": "0.00", "snippet": "top_public_data", "all_keywords": { "recent": 0, "previous": 0, "diff": 0, "diff_percent": 0 }, "visible_keywords": { "recent": 0, "previous": 0, "diff": 0, "diff_percent": 0 } }, "site_links": { "serp_coverage": "0.00", "snippet": "site_links", "all_keywords": { "recent": 0, "previous": 0, "diff": 0, "diff_percent": 0 }, "visible_keywords": { "recent": 0, "previous": 0, "diff": 0, "diff_percent": 0 } }, "featured_answer": { "serp_coverage": "0.00", "snippet": "featured_answer", "all_keywords": { "recent": 0, "previous": 0, "diff": 0, "diff_percent": 0 }, "visible_keywords": { "recent": 0, "previous": 0, "diff": 0, "diff_percent": 0 } }, "adwords": { "serp_coverage": "0.00", "snippet": "adwords", "all_keywords": { "recent": 0, "previous": 0, "diff": 0, "diff_percent": 0 }, "visible_keywords": { "recent": 0, "previous": 0, "diff": 0, "diff_percent": 0 } } } } ``` ### Struktura odpowiedzi ```ts type GetSnippetsStatisticsResponse = { /** `true` przy powodzeniu; przy błędzie `false` i koperta z `error` */ success: boolean; /** * Obiekt keyed by typ snippetu (klucze dynamiczne, np. `image_thumbs`, `wiki_right`, `map`). * To NIE jest tablica. Zbiór kluczy może być podzbiorem wszystkich typów snippetów. */ data: Record; } type SnippetStatistics = { /** Procent pokrycia SERP jako STRING, np. "29.79" */ serp_coverage: string; /** Nazwa typu snippetu (powtórzenie klucza) */ snippet: string; /** Frazy projektu, dla których dany snippet występuje w SERP */ all_keywords: SnippetCounters; /** Frazy, w których snippecie widoczna jest domena projektu */ visible_keywords: SnippetCounters; } type SnippetCounters = { /** Stan ze snapshotu `date_max` */ recent: number; /** Stan ze snapshotu `date_min` */ previous: number; /** Różnica bezwzględna: recent - previous */ diff: number; /** Różnica procentowa — liczba `0` albo STRING, np. "-24.32" */ diff_percent: number | string; } export default GetSnippetsStatisticsResponse ``` ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data, unknown */ type: string; message: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > Błędy walidacji zwracane są ze statusem **`418`**. Brak któregokolwiek z pól `project_id`, `date_min`, `date_max` → `invalid_data`. `project_id` nienależący do użytkownika → `Unauthorized access` (`418`), a nie `404`. Znany bug: przy `date_min` późniejszym niż `date_max` komunikat błędu z `DateRangeRules` jest odwrócony względem faktycznego naruszenia. ## Powiązane akcje - `getStatistics` — porównanie dwóch snapshotów per typ snippetu, z pokryciem SERP i widocznością domeny (ta strona) - `getHistory` — liczba fraz z danym typem snippetu w SERP, per data pomiaru (`POST`, `project_id` + `date_min` + `date_max`) --- # Snippety SERP: historia (`getHistory`) **`POST /api/rank_tracker/reports/snippets/getHistory`** Zwraca historię elementów SERP (snippetów) wykrytych dla fraz projektu Rank Tracker: dla każdej daty pomiaru z zakresu `date_min`–`date_max` raportowana jest liczba fraz projektu, dla których dany typ snippetu występuje w SERP. Wynik pozwala śledzić, jak zmienia się obecność poszczególnych typów snippetów (np. `people_also_ask`, `image_thumbs`, `featured_snippets`) w czasie. --- ## Żądanie `POST` `/api/rank_tracker/reports/snippets/getHistory` Nagłówki: `Authorization: Bearer `, `Content-Type: application/json`. ### Struktura żądania **Podstawowy** ```jsonc filename="żądanie-podstawowe.jsonc" { "project_id": null, "date_min": "2026-06-20", "date_max": "2026-06-29" } ``` **cURL** ```bash curl --location --request POST 'https://api.senuto.com/api/rank_tracker/reports/snippets/getHistory' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' \ --data-raw '{ "project_id": null, "date_min": "2026-06-20", "date_max": "2026-06-29" }' ``` ### Parametry ```ts type GetSnippetsHistoryRequest = { /** * **Wymagane**. ID projektu Rank Tracker (walidator `SnippetsValidator`). * Musi należeć do użytkownika, inaczej zwracane jest `418` z komunikatem `Unauthorized access`. * Realny `project_id` pobierzesz z `POST /api/rank_tracker/management/projects/getMyActiveProjects`. */ project_id: number; /** * **Wymagane**. Data początkowa zakresu w formacie `YYYY-MM-DD`. */ date_min: string; /** * **Wymagane**. Data końcowa zakresu w formacie `YYYY-MM-DD`. */ date_max: string; } export default GetSnippetsHistoryRequest ``` > **Ostrzeżenie:** > Znany bug walidacji: gdy `date_min` jest **późniejsze** niż `date_max`, reguły `DateRangeRules` zwracają błąd z **odwróconym komunikatem** (treść sugeruje odwrotny kierunek naruszenia). Pilnuj poprawnej kolejności dat po swojej stronie. > **Ostrzeżenie:** > **Pułapki tego endpointu:** (1) `data` to **obiekt keyed by daty pomiaru** (`YYYY-MM-DD`) — w zwalidowanej odpowiedzi znalazły się dokładnie dwa snapshoty, `date_max` i `date_min` (w tej kolejności), ale **kolejności kluczy nie traktuj jako gwarantowanej**. (2) Każdy snapshot to **mapa typ snippetu → liczba fraz** (klucze dynamiczne). (3) Zbiór typów snippetów **może się różnić między datami** — np. snapshot `2026-06-20` nie zawiera kluczy `featured_snippets` ani `answer_box`, które występują w `2026-06-29`; brakującego klucza nie interpretuj automatycznie jako zera bez własnej decyzji. (4) Odpowiedź **nie ma paginacji**. ## Odpowiedź Po pomyślnym żądaniu otrzymujesz `data` — **obiekt keyed by daty pomiaru**. Każdy snapshot to mapa: typ snippetu → liczba fraz projektu, dla których ten snippet występował w SERP w danym dniu. Zwróć uwagę, że listy typów mogą się różnić między datami (poniżej `2026-06-20` nie ma `featured_snippets` ani `answer_box`). **Skrócona** ```json filename="przykładowa-odpowiedź (skrócona)" { "success": true, "data": { "2026-06-29": { "image_thumbs": 28, "people_also_ask": 83, "related_searches": 94, "featured_snippets": 4 }, "2026-06-20": { "image_thumbs": 37, "people_also_ask": 71, "related_searches": 89 } } } ``` **Pełna** ```json filename="przykładowa-odpowiedź (200)" { "success": true, "data": { "2026-06-29": { "news": 0, "image_thumbs": 28, "wiki_right": 12, "map": 0, "top_video": 0, "spell": 3, "top_info": 0, "top_bar": 0, "top_scholar": 0, "top_number_results": 0, "top_public_data": 0, "site_links": 0, "featured_answer": 0, "adwords": 0, "people_also_ask": 83, "people_also_search_products": 2, "related_searches": 94, "video_thumbs": 35, "videos_pack": 35, "featured_snippets": 4, "answer_box": 1 }, "2026-06-20": { "news": 0, "image_thumbs": 37, "wiki_right": 9, "map": 1, "top_video": 0, "spell": 3, "top_info": 0, "top_bar": 0, "top_scholar": 0, "top_number_results": 0, "top_public_data": 0, "site_links": 0, "featured_answer": 0, "adwords": 0, "related_searches": 89, "people_also_ask": 71, "video_thumbs": 28, "videos_pack": 28, "people_also_search_products": 1 } } } ``` ### Struktura odpowiedzi ```ts type GetSnippetsHistoryResponse = { /** `true` przy powodzeniu; przy błędzie `false` i koperta z `error` */ success: boolean; /** * Obiekt keyed by data pomiaru (`YYYY-MM-DD`). To NIE jest tablica i NIE ma paginacji. * Kolejność kluczy nie jest gwarantowana. */ data: Record; } /** * Mapa: typ snippetu (klucz dynamiczny, np. `people_also_ask`, `image_thumbs`) * -> liczba fraz projektu z tym snippetem w SERP danego dnia. * Zbiór kluczy może się różnić między datami. */ type SnippetHistorySnapshot = Record; export default GetSnippetsHistoryResponse ``` ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data, unknown */ type: string; message: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > Błędy walidacji zwracane są ze statusem **`418`**. Brak któregokolwiek z pól `project_id`, `date_min`, `date_max` → `invalid_data`. `project_id` nienależący do użytkownika → `Unauthorized access` (`418`), a nie `404`. Znany bug: przy `date_min` późniejszym niż `date_max` komunikat błędu z `DateRangeRules` jest odwrócony względem faktycznego naruszenia. ## Powiązane akcje - `getHistory` — liczba fraz z danym typem snippetu w SERP, per data pomiaru (ta strona) - `getStatistics` — porównanie dwóch snapshotów per typ snippetu, z pokryciem SERP (`serp_coverage`) i widocznością domeny (`POST`, `project_id` + `date_min` + `date_max`) --- # AI Overviews: statystyki (`getStatistics`) **`GET /api/rank_tracker/reports/ai_overviews/getStatistics`** Zwraca zbiorcze statystyki obecności domeny projektu w blokach **AI Overviews** Google dla fraz monitorowanych w projekcie Rank Tracker. Odpowiedź to obiekt z dziewięcioma metrykami (m.in. `aio_visibility`, `aio_count`, `aio_sov`), z których każda zawiera wartość bieżącą, poprzednią, różnicę, zmianę procentową oraz historię dzienną. To **inny raport** niż wycofany raport AI Overviews w Analizie widoczności — ten endpoint **nie jest** oznaczony jako deprecated. Bez paginacji. --- ## Żądanie `GET` `/api/rank_tracker/reports/ai_overviews/getStatistics` Nagłówki: `Authorization: Bearer `. Parametry przekazuj w **query stringu** (np. `?project_id=87944`). Realny `project_id` pobierzesz z `POST /api/rank_tracker/management/projects/getMyActiveProjects`. ### Struktura żądania **Podstawowy** ```jsonc filename="żądanie-podstawowe.jsonc" { "project_id": null } ``` **cURL** ```bash curl --location --request GET 'https://api.senuto.com/api/rank_tracker/reports/ai_overviews/getStatistics?project_id=87944' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' ``` ### Parametry ```ts type AiOverviewsGetStatisticsRequest = { /** * **Wymagane**. ID projektu Rank Tracker. Użytkownik musi mieć dostęp do projektu * (reguły `ProjectAccessRules`); cudzy lub nieistniejący projekt → `418` `Unauthorized access`. * Realny `project_id` pobierzesz z `POST /api/rank_tracker/management/projects/getMyActiveProjects`. */ project_id: number; } export default AiOverviewsGetStatisticsRequest ``` > **Ostrzeżenie:** > Ten endpoint używa metody **`GET`** — parametry należy przekazywać w **query stringu** (kontroler wymusza `allowMethod('get')`; żądanie `POST` zwraca `405`). Wymagany jest wyłącznie **`project_id`**; wskazanie cudzego lub nieistniejącego projektu skutkuje `418` (`Unauthorized access`) — dostęp weryfikują reguły `ProjectAccessRules`. ## Odpowiedź Po pomyślnym żądaniu otrzymujesz `data` będące obiektem z dziewięcioma metrykami AI Overviews. Każda metryka ma pola `current`, `previous`, `diff`, `percent` oraz `history` — mapę, w której kluczem jest **uniksowy timestamp jako string**, a wartością wartość metryki w danym dniu. Metryka `aio_sov` zawiera dodatkowo `competitorsAvgSov` i `competitorsMaxSov`. Odpowiedź nie ma paginacji. `history` każdej metryki zawiera punkt na każdy dzień zakresu (w przykładach poniżej skrócono ją do 3 punktów). Projekt bez obecności w AI Overviews ma wartości metryk równe `0`; struktura pozostaje taka sama. **Skrócona** ```json filename="przykładowa-odpowiedź (skrócona)" { "success": true, "data": { "aio_visibility": { "current": 0, "previous": 0, "diff": 0, "percent": 0, "history": { "1775088000": 0, "1775174400": 0, "1782950400": 0 } }, "aio_sov": { "competitorsAvgSov": 0, "competitorsMaxSov": 0, "current": 0, "previous": 0, "diff": 0, "percent": 0, "history": { "1775088000": 0, "1775174400": 0, "1782950400": 0 } }, "aio_potential": { "current": 0, "previous": 0, "diff": 0, "percent": 0, "history": { "1775088000": 859.5, "1775174400": 850.5, "1782950400": 0 } } } } ``` **Pełna** ```json filename="przykładowa-odpowiedź (zwalidowana, 200; history skrócone do 3 z 92 punktów)" { "success": true, "data": { "aio_visibility": { "current": 0, "previous": 0, "diff": 0, "percent": 0, "history": { "1775088000": 0, "1775174400": 0, "1782950400": 0 } }, "aio_count": { "current": 0, "previous": 0, "diff": 0, "percent": 0, "history": { "1775088000": 0, "1775174400": 0, "1782950400": 0 } }, "aio_avg_pos": { "current": 0, "previous": 0, "diff": 0, "percent": 0, "history": { "1775088000": 0, "1775174400": 0, "1782950400": 0 } }, "aio_citations": { "current": 0, "previous": 0, "diff": 0, "percent": 0, "history": { "1775088000": 0, "1775174400": 0, "1782950400": 0 } }, "aio_keywords_total": { "current": 0, "previous": 0, "diff": 0, "percent": 0, "history": { "1775088000": 84, "1775174400": 83, "1782950400": 0 } }, "aio_sov": { "competitorsAvgSov": 0, "competitorsMaxSov": 0, "current": 0, "previous": 0, "diff": 0, "percent": 0, "history": { "1775088000": 0, "1775174400": 0, "1782950400": 0 } }, "aio_potential": { "current": 0, "previous": 0, "diff": 0, "percent": 0, "history": { "1775088000": 859.5, "1775174400": 850.5, "1782950400": 0 } }, "aio_utilized_potential": { "current": 0, "previous": 0, "diff": 0, "percent": 0, "history": { "1775088000": 0, "1775174400": 0, "1782950400": 0 } }, "aio_unique_keywords": { "current": 0, "previous": 0, "diff": 0, "percent": 0, "history": { "1775088000": 0, "1775174400": 0, "1782950400": 0 } } } } ``` ### Struktura odpowiedzi ```ts type AiOverviewsGetStatisticsResponse = { /** `true` przy powodzeniu; przy błędzie `false` i koperta z `error` */ success: boolean; /** Zbiorcze metryki obecności domeny projektu w AI Overviews */ data: { /** Widoczność domeny w AI Overviews */ aio_visibility: AioMetric; /** Liczba wystąpień domeny w blokach AI Overviews */ aio_count: AioMetric; /** Średnia pozycja domeny w blokach AI Overviews */ aio_avg_pos: AioMetric; /** Liczba cytowań domeny jako źródła w AI Overviews */ aio_citations: AioMetric; /** Łączna liczba fraz projektu wyzwalających AI Overviews */ aio_keywords_total: AioMetric; /** Share of Voice w AI Overviews; dodatkowo wartości konkurencji */ aio_sov: AioSovMetric; /** Potencjał obecności w AI Overviews */ aio_potential: AioMetric; /** Wykorzystany potencjał obecności w AI Overviews */ aio_utilized_potential: AioMetric; /** Liczba unikalnych fraz z obecnością domeny w AI Overviews */ aio_unique_keywords: AioMetric; }; } type AioMetric = { /** Wartość bieżąca */ current: number; /** Wartość poprzednia (okres porównawczy) */ previous: number; /** Różnica current - previous */ diff: number; /** Zmiana procentowa */ percent: number; /** * Historia dzienna metryki. Kluczem jest **uniksowy timestamp jako string** * (północ UTC danego dnia), wartością — wartość metryki tego dnia. * W zwalidowanej odpowiedzi mapa liczyła 92 punkty dzienne. */ history: Record; } type AioSovMetric = AioMetric & { /** Średni Share of Voice konkurentów w AI Overviews */ competitorsAvgSov: number; /** Najwyższy Share of Voice wśród konkurentów w AI Overviews */ competitorsMaxSov: number; } export default AiOverviewsGetStatisticsResponse ``` ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data, unauthorized, unknown */ type: string; message: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > Kontroler wymusza metodę `GET` (`allowMethod('get')`) — żądanie **`POST`** zwraca **`405`**. Wskazanie projektu, do którego użytkownik nie ma dostępu, lub projektu nieistniejącego zwraca **`418`** z komunikatem `Unauthorized access`. ## Powiązane akcje - `getStatistics` — zbiorcze statystyki obecności w AI Overviews (ta strona) - `getKeywords` — frazy projektu z danymi o obecności w AI Overviews - `getDistribution` — rozkład fraz wyzwalających AI Overviews po pozycjach organicznych 1–50 - `getCompetitors` — konkurenci w blokach AI Overviews - `getOpportunities` — szanse na zdobycie obecności w AI Overviews - `getAioDetails` — szczegóły bloku AI Overviews dla frazy - `getAioSources` — źródła cytowane w blokach AI Overviews --- # AI Overviews: słowa kluczowe (`getKeywords`) **`POST /api/rank_tracker/reports/ai_overviews/getKeywords`** Zwraca stronicowaną listę fraz projektu Rank Tracker, które wyzwalają blok **AI Overviews** w wynikach Google — zarówno tych, w których monitorowana domena jest obecna (cytowana), jak i tych bez jej obecności. Ten raport — w odróżnieniu od AI Overviews w Analizie widoczności — **nie jest przestarzały**. --- ## Żądanie `POST` `/api/rank_tracker/reports/ai_overviews/getKeywords` Nagłówki: `Authorization: Bearer `, `Content-Type: application/json`. ### Struktura żądania **Podstawowy** ```jsonc filename="żądanie-podstawowe.jsonc" { "project_id": null } ``` **Rozszerzony** ```jsonc filename="żądanie-rozszerzone.jsonc" { "project_id": null, "limit": 2, "page": 1 } ``` **cURL** ```bash curl --location --request POST 'https://api.senuto.com/api/rank_tracker/reports/ai_overviews/getKeywords' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' \ --data-raw '{ "project_id": null, "limit": 2 }' ``` ### Parametry ```ts type GetAiOverviewsKeywordsRequest = { /** * **Wymagane**. ID projektu Rank Tracker (kontrola dostępu `ProjectAccessRules`). * Musi należeć do użytkownika — cudzy lub nieistniejący `project_id` zwraca `418` z komunikatem `Unauthorized access`. * Realny `project_id` pobierzesz z `POST /api/rank_tracker/management/projects/getMyActiveProjects`. */ project_id: number; /** * Rozmiar strony paginacji. Odbija się w `pagination.limit`. * @default 10 */ limit?: number; /** * Numer strony paginacji. * @default 1 */ page?: number; } export default GetAiOverviewsKeywordsRequest ``` > **Informacja:** > Endpoint przyjmuje także `filtering` i `order` — oba korzystają z rejestrów filtrów i sortowania dla raportów AI Overviews. Nieznany klucz filtra zwraca `418` z `invalid_filtering`. > **Ostrzeżenie:** > Projekt, który nie ma danych AI Overviews, zwraca `200` z **pustą tablicą** `data` — to nie błąd, tylko brak wyników dla tego projektu. Poniżej opisana jest koperta odpowiedzi. ## Odpowiedź Po pomyślnym żądaniu otrzymujesz standardową kopertę: `success`, `data` (tablica fraz wyzwalających AIO) oraz `pagination`. Dla projektu bez danych AIO tablica `data` jest pusta, a `pagination.count` wynosi `0`. **Skrócona** ```json filename="przykładowa-odpowiedź (skrócona)" { "success": true, "data": [], "pagination": { "page_count": 0, "current_page": 1, "has_next_page": false, "has_prev_page": false, "count": 0, "limit": 2 } } ``` **Pełna** ```json filename="przykładowa-odpowiedź (200)" { "success": true, "data": [], "pagination": { "page_count": 0, "current_page": 1, "has_next_page": false, "has_prev_page": false, "count": 0, "limit": 2 } } ``` ### Struktura odpowiedzi ```ts type GetAiOverviewsKeywordsResponse = { /** `true` przy powodzeniu; przy błędzie `false` i koperta z `error` */ success: boolean; /** * Frazy projektu wyzwalające AI Overviews. * Kształt wiersza zależy od danych AI Overviews projektu. */ data: unknown[]; /** Metadane paginacji */ pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; /** Wartość przekazanego `limit` */ limit: number; }; } export default GetAiOverviewsKeywordsResponse ``` ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data, unknown */ type: string; message: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > **`418`** jest zwracane przy błędach walidacji i braku dostępu: brak `project_id` → `invalid_data`, a cudzy lub nieistniejący `project_id` → `Unauthorized access` (`ProjectAccessRules`), nie `404`. ## Powiązane akcje - `getStatistics` — zbiorcze statystyki AI Overviews projektu - `getKeywords` — frazy wyzwalające AI Overviews (ta strona) - `getDistribution` — rozkład obecności w AI Overviews - `getCompetitors` — konkurenci cytowani w AI Overviews - `getOpportunities` — frazy z AIO, w których domena rankuje organicznie, ale nie jest cytowana - `getAioDetails` — pełny surowy blok AI Overview danej frazy (`project_id` + `keyword_id`) - `getAioSources` — lista źródeł cytowanych w bloku AIO danej frazy (`project_id` + `keyword_id`) --- # AI Overviews: rozkład pozycji (`getDistribution`) **`GET /api/rank_tracker/reports/ai_overviews/getDistribution`** Zwraca rozkład fraz projektu wyzwalających bloki **AI Overviews** Google po pozycjach organicznych. `data` to tablica **dokładnie 50 kubełków** (`pos` od 1 do 50) — dla każdej pozycji organicznej endpoint podaje, ile fraz projektu na tej pozycji wyzwala AI Overviews, z podziałem na frazy z obecnością domeny w bloku AIO (`with_presence`) i bez niej (`without_presence`). To **inny raport** niż wycofany raport AI Overviews w Analizie widoczności — ten endpoint **nie jest** oznaczony jako deprecated. Bez paginacji. | Pozycja | Frazy z AIO | Udział % | Z obecnością | Z obecnością % | Bez obecności | Bez obecności % | | --- | --- | --- | --- | --- | --- | --- | | 1 | 0 | 0 | 0 | 0 | 0 | 0 | | 2 | 0 | 0 | 0 | 0 | 0 | 0 | | 3 | 0 | 0 | 0 | 0 | 0 | 0 | _Rozkład fraz wyzwalających AI Overviews po pozycjach 1–50 (pokazano 3 z 50 kubełków; projekt bez obecności w AIO, stąd zera). Wszystkie adresowalne pola wiersza._ --- ## Żądanie `GET` `/api/rank_tracker/reports/ai_overviews/getDistribution` Nagłówki: `Authorization: Bearer `. Parametry przekazuj w **query stringu** (np. `?project_id=87944`). Realny `project_id` pobierzesz z `POST /api/rank_tracker/management/projects/getMyActiveProjects`. ### Struktura żądania **Podstawowy** ```jsonc filename="żądanie-podstawowe.jsonc" { "project_id": null } ``` **cURL** ```bash curl --location --request GET 'https://api.senuto.com/api/rank_tracker/reports/ai_overviews/getDistribution?project_id=87944' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' ``` ### Parametry ```ts type AiOverviewsGetDistributionRequest = { /** * **Wymagane**. ID projektu Rank Tracker. Użytkownik musi mieć dostęp do projektu * (reguły `ProjectAccessRules`); cudzy lub nieistniejący projekt → `418` `Unauthorized access`. * Realny `project_id` pobierzesz z `POST /api/rank_tracker/management/projects/getMyActiveProjects`. */ project_id: number; } export default AiOverviewsGetDistributionRequest ``` > **Ostrzeżenie:** > Ten endpoint używa metody **`GET`** — parametry należy przekazywać w **query stringu** (kontroler wymusza `allowMethod('get')`; żądanie `POST` zwraca `405`). Wymagany jest wyłącznie **`project_id`**; wskazanie cudzego lub nieistniejącego projektu skutkuje `418` (`Unauthorized access`) — dostęp weryfikują reguły `ProjectAccessRules`. ## Odpowiedź Po pomyślnym żądaniu otrzymujesz `data` będące tablicą **dokładnie 50 kubełków** — po jednym dla każdej pozycji organicznej od 1 do 50. Każdy kubełek podaje liczbę fraz projektu na danej pozycji, które wyzwalają AI Overviews (`total`), oraz podział na frazy z obecnością domeny w bloku AIO (`with_presence`) i bez tej obecności (`without_presence`) — wraz z udziałami procentowymi. Odpowiedź nie ma paginacji. W przykładach poniżej pokazano pierwsze kubełki oraz ostatni — w pełnej odpowiedzi jest ich 50. Projekt bez obecności w AI Overviews zwraca wszystkie wartości równe `0`; struktura pozostaje taka sama. **Skrócona** ```json filename="przykładowa-odpowiedź (skrócona)" { "success": true, "data": [ { "pos": 1, "total": 0, "total_percentage": 0, "with_presence": 0, "with_presence_percentage": 0, "without_presence": 0, "without_presence_percentage": 0 }, { "pos": 2, "total": 0, "total_percentage": 0, "with_presence": 0, "with_presence_percentage": 0, "without_presence": 0, "without_presence_percentage": 0 } ] } ``` **Pełna** ```json filename="przykładowa-odpowiedź (zwalidowana, 200; pokazano 3 z 50 kubełków)" { "success": true, "data": [ { "pos": 1, "total": 0, "total_percentage": 0, "with_presence": 0, "with_presence_percentage": 0, "without_presence": 0, "without_presence_percentage": 0 }, { "pos": 2, "total": 0, "total_percentage": 0, "with_presence": 0, "with_presence_percentage": 0, "without_presence": 0, "without_presence_percentage": 0 }, { "pos": 50, "total": 0, "total_percentage": 0, "with_presence": 0, "with_presence_percentage": 0, "without_presence": 0, "without_presence_percentage": 0 } ] } ``` ### Struktura odpowiedzi ```ts type AiOverviewsGetDistributionResponse = { /** `true` przy powodzeniu; przy błędzie `false` i koperta z `error` */ success: boolean; /** * Rozkład fraz wyzwalających AI Overviews po pozycjach organicznych. * Tablica zawiera **dokładnie 50 kubełków** — po jednym dla pozycji 1…50. */ data: AioDistributionBucket[]; } type AioDistributionBucket = { /** Pozycja organiczna kubełka (1–50) */ pos: number; /** Liczba fraz projektu na tej pozycji, które wyzwalają AI Overviews */ total: number; /** Udział procentowy kubełka w łącznej liczbie fraz wyzwalających AIO */ total_percentage: number; /** Liczba fraz z obecnością domeny projektu w bloku AI Overviews */ with_presence: number; /** Udział procentowy fraz z obecnością domeny w AIO */ with_presence_percentage: number; /** Liczba fraz bez obecności domeny projektu w bloku AI Overviews */ without_presence: number; /** Udział procentowy fraz bez obecności domeny w AIO */ without_presence_percentage: number; } export default AiOverviewsGetDistributionResponse ``` ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data, unauthorized, unknown */ type: string; message: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > Kontroler wymusza metodę `GET` (`allowMethod('get')`) — żądanie **`POST`** zwraca **`405`**. Wskazanie projektu, do którego użytkownik nie ma dostępu, lub projektu nieistniejącego zwraca **`418`** z komunikatem `Unauthorized access`. ## Powiązane akcje - `getStatistics` — zbiorcze statystyki obecności w AI Overviews - `getKeywords` — frazy projektu z danymi o obecności w AI Overviews - `getDistribution` — rozkład fraz wyzwalających AI Overviews po pozycjach organicznych 1–50 (ta strona) - `getCompetitors` — konkurenci w blokach AI Overviews - `getOpportunities` — szanse na zdobycie obecności w AI Overviews - `getAioDetails` — szczegóły bloku AI Overviews dla frazy - `getAioSources` — źródła cytowane w blokach AI Overviews --- # AI Overviews: konkurenci (`getCompetitors`) **`POST /api/rank_tracker/reports/ai_overviews/getCompetitors`** Porównuje obecność w blokach AI Overviews (AIO) domeny projektu Rank Tracker i jego konkurentów. Dla każdego konkurenta (lista pochodzi z `GET /api/rank_tracker/management/competitors/list`) zwracane są liczby fraz wspólnych i unikalnych oraz komplet dziewięciu metryk `aio_*` w ujęciu `current` / `previous` / `diff` / `percent`. Wynik jest stronicowany — paginacja liczona jest po konkurentach. Ten raport — w odróżnieniu od modułu AI Overviews w Analizie widoczności — **nie jest przestarzały**. | Domena | ID konkurenta | Frazy wspólne | Frazy unikalne (projekt) | Frazy unikalne (konkurent) | Widoczność AIO (bieżąco) | Widoczność AIO (poprz.) | Widoczność AIO (Δ) | Widoczność AIO (%) | Liczba wystąpień AIO (bieżąco) | Liczba wystąpień AIO (poprz.) | Liczba wystąpień AIO (Δ) | Liczba wystąpień AIO (%) | Śr. pozycja AIO (bieżąco) | Śr. pozycja AIO (poprz.) | Śr. pozycja AIO (Δ) | Śr. pozycja AIO (%) | Cytowania AIO (bieżąco) | Cytowania AIO (poprz.) | Cytowania AIO (Δ) | Cytowania AIO (%) | Frazy z AIO łącznie (bieżąco) | Frazy z AIO łącznie (poprz.) | Frazy z AIO łącznie (Δ) | Frazy z AIO łącznie (%) | SoV AIO (bieżąco) | SoV AIO (poprz.) | SoV AIO (Δ) | SoV AIO (%) | SoV AIO — śr. konkurentów | SoV AIO — maks. konkurentów | Potencjał AIO (bieżąco) | Potencjał AIO (poprz.) | Potencjał AIO (Δ) | Potencjał AIO (%) | Wykorz. potencjał AIO (bieżąco) | Wykorz. potencjał AIO (poprz.) | Wykorz. potencjał AIO (Δ) | Wykorz. potencjał AIO (%) | Unikalne frazy AIO (bieżąco) | Unikalne frazy AIO (poprz.) | Unikalne frazy AIO (Δ) | Unikalne frazy AIO (%) | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | fajnyzwierzak.pl | 55578 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | | psy.pl | 4741 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | _Konkurenci w AI Overviews (limit 2). Projekt bez obecności w AIO ma zera w metrykach. Wszystkie adresowalne pola wiersza (obiekt statistics ma stałe klucze aio_*)._ --- ## Żądanie `POST` `/api/rank_tracker/reports/ai_overviews/getCompetitors` Nagłówki: `Authorization: Bearer `, `Content-Type: application/json`. ### Struktura żądania **Podstawowy** ```jsonc filename="żądanie-podstawowe.jsonc" { "project_id": null } ``` **Rozszerzony** ```jsonc filename="żądanie-rozszerzone.jsonc" { "project_id": null, "limit": 2, "page": 1 } ``` **cURL** ```bash curl --location --request POST 'https://api.senuto.com/api/rank_tracker/reports/ai_overviews/getCompetitors' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' \ --data-raw '{ "project_id": null, "limit": 2 }' ``` ### Parametry ```ts type GetCompetitorsRequest = { /** * **Wymagane**. ID projektu Rank Tracker (`ProjectAccessRules`). * Projekt musi należeć do użytkownika lub być mu udostępniony — cudzy `project_id` zwraca `418` z `Unauthorized access`. * Realny `project_id` pobierzesz z `POST /api/rank_tracker/management/projects/getMyActiveProjects`. */ project_id: number; /** * Filtrowanie listy konkurentów. Nieznany `key` → `418` `invalid_filtering`. * Dozwolone klucze m.in.: `competitor_domain`, `aio_visibility`, `aio_count`, `aio_sov`, * `shared_keywords`, `unique_keywords`, `unique_for_competitor` oraz aliasy `statistics.aio_*.current`/`.diff`. * Operatory liczbowe: `gt` | `gte` | `lt` | `lte` | `eq`. */ filtering?: Array<{ filters: Array<{ key: string; match?: 'gt' | 'gte' | 'lt' | 'lte' | 'eq'; value: string | number | Array; complement?: boolean }>; conjunction?: 'and' | 'or'; }>; /** * Numer strony paginacji. Paginacja liczona jest po konkurentach projektu. * @default 1 */ page?: number; /** * Rozmiar strony paginacji (liczba konkurentów na stronę). Odbija się w `pagination.limit`. * @default 10 */ limit?: number; } export default GetCompetitorsRequest ``` > **Ostrzeżenie:** > **Pułapki typów i struktury:** pola `competitor_id`, `shared_keywords`, `unique_keywords` oraz `unique_for_competitor` są zwracane jako **stringi** (np. `"0"`), a nie liczby. Obiekt `statistics` zawiera te same dziewięć metryk `aio_*` co akcja `getStatistics`, ale każda metryka ma wyłącznie `{current, previous, diff, percent}` — **bez pola `history`**; dodatkowo `aio_sov` niesie jeszcze `competitorsAvgSov` i `competitorsMaxSov`. Paginacja liczona jest **po konkurentach** (w projekcie testowym `count: 3`). `project_id` jest wymagane (`ProjectAccessRules`) — cudzy lub nieistniejący projekt zwraca `418` z `Unauthorized access`. Projekt testowy `87944` (pies.pl) nie ma bieżącej obecności w AIO, stąd zera w metrykach — struktura odpowiedzi jest pewna, wartości są przykładowe. ## Odpowiedź Po pomyślnym żądaniu otrzymujesz `data` (tablicę konkurentów) oraz `pagination`. Każdy element `data` opisuje jednego konkurenta: jego identyfikator i domenę, liczby fraz wspólnych i unikalnych (jako stringi) oraz obiekt `statistics` z dziewięcioma metrykami `aio_*`. `count` w `pagination` to łączna liczba konkurentów w projekcie. **Skrócona** ```json filename="przykładowa-odpowiedź (skrócona)" { "success": true, "data": [ { "competitor_id": "55578", "competitor_domain": "fajnyzwierzak.pl", "shared_keywords": "0", "unique_keywords": "0", "unique_for_competitor": "0", "statistics": { "aio_visibility": { "current": 0, "previous": 0, "diff": 0, "percent": 0 }, "aio_sov": { "competitorsAvgSov": 0, "competitorsMaxSov": 0, "current": 0, "previous": 0, "diff": 0, "percent": 0 } } } ], "pagination": { "page_count": 2, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 3, "limit": 2 } } ``` **Pełna** ```json filename="przykładowa-odpowiedź (200)" { "success": true, "data": [ { "competitor_id": "55578", "competitor_domain": "fajnyzwierzak.pl", "shared_keywords": "0", "unique_keywords": "0", "unique_for_competitor": "0", "statistics": { "aio_visibility": { "current": 0, "previous": 0, "diff": 0, "percent": 0 }, "aio_count": { "current": 0, "previous": 0, "diff": 0, "percent": 0 }, "aio_avg_pos": { "current": 0, "previous": 0, "diff": 0, "percent": 0 }, "aio_citations": { "current": 0, "previous": 0, "diff": 0, "percent": 0 }, "aio_keywords_total": { "current": 0, "previous": 0, "diff": 0, "percent": 0 }, "aio_sov": { "competitorsAvgSov": 0, "competitorsMaxSov": 0, "current": 0, "previous": 0, "diff": 0, "percent": 0 }, "aio_potential": { "current": 0, "previous": 0, "diff": 0, "percent": 0 }, "aio_utilized_potential": { "current": 0, "previous": 0, "diff": 0, "percent": 0 }, "aio_unique_keywords": { "current": 0, "previous": 0, "diff": 0, "percent": 0 } } }, { "competitor_id": "4741", "competitor_domain": "psy.pl", "shared_keywords": "0", "unique_keywords": "0", "unique_for_competitor": "0", "statistics": { "aio_visibility": { "current": 0, "previous": 0, "diff": 0, "percent": 0 }, "aio_count": { "current": 0, "previous": 0, "diff": 0, "percent": 0 }, "aio_avg_pos": { "current": 0, "previous": 0, "diff": 0, "percent": 0 }, "aio_citations": { "current": 0, "previous": 0, "diff": 0, "percent": 0 }, "aio_keywords_total": { "current": 0, "previous": 0, "diff": 0, "percent": 0 }, "aio_sov": { "competitorsAvgSov": 0, "competitorsMaxSov": 0, "current": 0, "previous": 0, "diff": 0, "percent": 0 }, "aio_potential": { "current": 0, "previous": 0, "diff": 0, "percent": 0 }, "aio_utilized_potential": { "current": 0, "previous": 0, "diff": 0, "percent": 0 }, "aio_unique_keywords": { "current": 0, "previous": 0, "diff": 0, "percent": 0 } } } ], "pagination": { "page_count": 2, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 3, "limit": 2 } } ``` ### Struktura odpowiedzi ```ts type GetCompetitorsResponse = { /** `true` przy powodzeniu; przy błędzie `false` i koperta z `error` */ success: boolean; /** Konkurenci projektu wraz z metrykami AI Overviews */ data: AioCompetitor[]; /** Metadane paginacji — liczone po konkurentach (`count` = łączna liczba konkurentów) */ pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; limit: number | null; }; } type AioCompetitor = { /** ID konkurenta — **string**, nie liczba (np. "55578") */ competitor_id: string; /** Domena konkurenta */ competitor_domain: string; /** Liczba fraz wspólnych z domeną projektu — **string** (np. "0") */ shared_keywords: string; /** Liczba fraz unikalnych dla domeny projektu — **string** */ unique_keywords: string; /** Liczba fraz unikalnych dla konkurenta — **string** */ unique_for_competitor: string; /** Dziewięć metryk AI Overviews — te same co w `getStatistics`, ale bez pola `history` */ statistics: { /** Widoczność w AI Overviews */ aio_visibility: AioMetric; /** Liczba wystąpień w blokach AIO */ aio_count: AioMetric; /** Średnia pozycja w blokach AIO */ aio_avg_pos: AioMetric; /** Liczba cytowań w blokach AIO */ aio_citations: AioMetric; /** Łączna liczba fraz z blokiem AIO */ aio_keywords_total: AioMetric; /** Share of Voice — dodatkowo zawiera `competitorsAvgSov` i `competitorsMaxSov` */ aio_sov: AioMetric & { competitorsAvgSov: number; competitorsMaxSov: number }; /** Potencjał obecności w AIO */ aio_potential: AioMetric; /** Wykorzystany potencjał obecności w AIO */ aio_utilized_potential: AioMetric; /** Liczba unikalnych fraz z obecnością w AIO */ aio_unique_keywords: AioMetric; }; } type AioMetric = { /** Wartość bieżąca */ current: number; /** Wartość poprzednia */ previous: number; /** Różnica (current - previous) */ diff: number; /** Zmiana procentowa */ percent: number; } export default GetCompetitorsResponse ``` ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data, unknown */ type: string; message: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > **`418`** jest zwracane również przy błędach walidacji i dostępu. Brak `project_id` skutkuje `invalid_data`, a cudzy lub nieistniejący `project_id` zwraca `Unauthorized access` (`418`), a nie `404` — dostęp weryfikuje `ProjectAccessRules`. ## Powiązane akcje - `getStatistics` — zbiorcze metryki `aio_*` domeny projektu (z historią) - `getKeywords` — frazy projektu z obecnością w AI Overviews - `getDistribution` — rozkład obecności w blokach AIO - `getCompetitors` — porównanie z konkurentami (ta strona) - `getOpportunities` — frazy z szansą na obecność w AIO - `getAioDetails` — surowa treść bloku AIO dla pojedynczej frazy - `getAioSources` — źródła cytowane w blokach AIO --- # AI Overviews: szanse (`getOpportunities`) **`POST /api/rank_tracker/reports/ai_overviews/getOpportunities`** Zwraca stronicowaną listę fraz projektu, dla których istnieje blok **AI Overview**, a monitorowana domena **rankuje organicznie, ale nie jest cytowana w AIO** — czyli potencjalne szanse optymalizacyjne (semantyka potwierdzona adnotacją w kodzie źródłowym kontrolera). Ten raport — w odróżnieniu od AI Overviews w Analizie widoczności — **nie jest przestarzały**. --- ## Żądanie `POST` `/api/rank_tracker/reports/ai_overviews/getOpportunities` Nagłówki: `Authorization: Bearer `, `Content-Type: application/json`. ### Struktura żądania **Podstawowy** ```jsonc filename="żądanie-podstawowe.jsonc" { "project_id": null } ``` **Rozszerzony** ```jsonc filename="żądanie-rozszerzone.jsonc" { "project_id": null, "limit": 2, "page": 1 } ``` **cURL** ```bash curl --location --request POST 'https://api.senuto.com/api/rank_tracker/reports/ai_overviews/getOpportunities' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' \ --data-raw '{ "project_id": null, "limit": 2 }' ``` ### Parametry ```ts type GetAiOverviewsOpportunitiesRequest = { /** * **Wymagane**. ID projektu Rank Tracker (kontrola dostępu `ProjectAccessRules`). * Musi należeć do użytkownika — cudzy lub nieistniejący `project_id` zwraca `418` z komunikatem `Unauthorized access`. * Realny `project_id` pobierzesz z `POST /api/rank_tracker/management/projects/getMyActiveProjects`. */ project_id: number; /** * Rozmiar strony paginacji. Odbija się w `pagination.limit`. * @default 10 */ limit?: number; /** * Numer strony paginacji. * @default 1 */ page?: number; } export default GetAiOverviewsOpportunitiesRequest ``` > **Informacja:** > Endpoint przyjmuje także `filtering` i `order` — oba korzystają z rejestrów filtrów i sortowania dla raportów AI Overviews. Nieznany klucz filtra zwraca `418` z `invalid_filtering`. > **Ostrzeżenie:** > Projekt, który nie ma danych AI Overviews, zwraca `200` z **pustą tablicą** `data` — to nie błąd, tylko brak wyników dla tego projektu. Poniżej opisana jest koperta odpowiedzi. ## Odpowiedź Po pomyślnym żądaniu otrzymujesz standardową kopertę: `success`, `data` (tablica fraz-szans) oraz `pagination`. Dla projektu bez danych AIO tablica `data` jest pusta, a `pagination.count` wynosi `0`. **Skrócona** ```json filename="przykładowa-odpowiedź (skrócona)" { "success": true, "data": [], "pagination": { "page_count": 0, "current_page": 1, "has_next_page": false, "has_prev_page": false, "count": 0, "limit": 2 } } ``` **Pełna** ```json filename="przykładowa-odpowiedź (200)" { "success": true, "data": [], "pagination": { "page_count": 0, "current_page": 1, "has_next_page": false, "has_prev_page": false, "count": 0, "limit": 2 } } ``` ### Struktura odpowiedzi ```ts type GetAiOverviewsOpportunitiesResponse = { /** `true` przy powodzeniu; przy błędzie `false` i koperta z `error` */ success: boolean; /** * Frazy z istniejącym AI Overview, w których domena rankuje organicznie, ale nie jest cytowana. * Kształt wiersza zależy od danych AI Overviews projektu. */ data: unknown[]; /** Metadane paginacji */ pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; /** Wartość przekazanego `limit` */ limit: number; }; } export default GetAiOverviewsOpportunitiesResponse ``` ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data, unknown */ type: string; message: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > **`418`** jest zwracane przy błędach walidacji i braku dostępu: brak `project_id` → `invalid_data`, a cudzy lub nieistniejący `project_id` → `Unauthorized access` (`ProjectAccessRules`), nie `404`. ## Powiązane akcje - `getStatistics` — zbiorcze statystyki AI Overviews projektu - `getKeywords` — frazy wyzwalające AI Overviews - `getDistribution` — rozkład obecności w AI Overviews - `getCompetitors` — konkurenci cytowani w AI Overviews - `getOpportunities` — szanse optymalizacyjne AIO (ta strona) - `getAioDetails` — pełny surowy blok AI Overview danej frazy (`project_id` + `keyword_id`) - `getAioSources` — lista źródeł cytowanych w bloku AIO danej frazy (`project_id` + `keyword_id`) --- # AI Overviews: szczegóły AIO (`getAioDetails`) **`POST /api/rank_tracker/reports/ai_overviews/getAioDetails`** Zwraca surową treść bloku AI Overview (AIO) dla wskazanej frazy projektu Rank Tracker: pełny tekst bloku (`text`), status pobrania (`status`), pozycję bloku w SERP (`rank_absolute`), listę źródeł (`sources[]`) oraz linki osadzone w treści (`content_links[]` z polami `url`, `text`, `rank_inner`). Wynikiem jest **pojedynczy obiekt** — bez paginacji. Ten raport — w odróżnieniu od modułu AI Overviews w Analizie widoczności — **nie jest przestarzały**. --- ## Żądanie `POST` `/api/rank_tracker/reports/ai_overviews/getAioDetails` Nagłówki: `Authorization: Bearer `, `Content-Type: application/json`. ### Struktura żądania **Podstawowy** ```jsonc filename="żądanie-podstawowe.jsonc" { "project_id": null, "keyword_id": null } ``` **Rozszerzony** ```jsonc filename="żądanie-rozszerzone.jsonc" { "project_id": null, "keyword_id": null } ``` **cURL** ```bash curl --location --request POST 'https://api.senuto.com/api/rank_tracker/reports/ai_overviews/getAioDetails' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' \ --data-raw '{ "project_id": null, "keyword_id": null }' ``` ### Parametry ```ts type GetAioDetailsRequest = { /** * **Wymagane**. ID projektu Rank Tracker (`ProjectAccessRules`). * Projekt musi należeć do użytkownika lub być mu udostępniony — cudzy `project_id` zwraca `418` z `Unauthorized access`. * Realny `project_id` pobierzesz z `POST /api/rank_tracker/management/projects/getMyActiveProjects`. */ project_id: number; /** * **Wymagane** przez `AioDetailsValidator`. ID frazy w projekcie — fraza musi należeć do podanego `project_id` * (`KeywordAccessRules`). Brak `keyword_id` zwraca `418` z `invalid_data` i regułą `_required`. * ID frazy pobierzesz np. z `POST /api/rank_tracker/reports/keywords/getGroupKeywords`. */ keyword_id: number; } export default GetAioDetailsRequest ``` > **Ostrzeżenie:** > Walidator `AioDetailsValidator` wymaga **`project_id`** i **`keyword_id`** — brak `keyword_id` zwraca `418` z `invalid_data` i regułą `_required`. `keyword_id` musi należeć do podanego projektu (`KeywordAccessRules`), a cudzy `project_id` zwraca `418` z `Unauthorized access`. **Pułapki:** w zwalidowanej odpowiedzi `sources[]` jest puste, mimo że `content_links[]` są wypełnione — nie zakładaj, że oba pola są uzupełniane razem. Treść bloku AIO pochodzi wprost z SERP i **może być w innym języku niż projekt** — w przykładzie poniżej Google zwrócił blok po czesku dla frazy zawierającej słowo „jak". ## Odpowiedź Po pomyślnym żądaniu otrzymujesz w `data` **pojedynczy obiekt** (bez paginacji) z pełnym tekstem bloku AIO, statusem, pozycją bloku w SERP oraz listami źródeł i linków osadzonych w treści. Pamiętaj, że treść bloku odzwierciedla to, co faktycznie wyświetlił Google — może więc być w innym języku niż projekt. **Skrócona** ```json filename="przykładowa-odpowiedź (skrócona)" { "success": true, "data": { "text": "• Zvíře: Dlouhosrstý tur žijící ve velehorách Střední Asie (více na Wikipedii). …", "status": "success", "rank_absolute": 1, "sources": [], "content_links": [ { "url": "https://cs.wikipedia.org/wiki/Jak_divok%C3%BD", "text": "Wikipedii", "rank_inner": 1 } ] } } ``` **Pełna** ```json filename="przykładowa-odpowiedź (200)" { "success": true, "data": { "text": "• Zvíře: Dlouhosrstý tur žijící ve velehorách Střední Asie (více na Wikipedii). • Zájmeno / příslovce: Táže se na způsob nebo míru (např. jak se máš?). • OP JAK: Zkratka pro Operační program Jan Amos Komenský, který v Česku podporuje vzdělávání a výzkum (oficiální stránky na OPJAK.cz).", "status": "success", "rank_absolute": 1, "sources": [], "content_links": [ { "url": "https://cs.wikipedia.org/wiki/Jak_divok%C3%BD", "text": "Wikipedii", "rank_inner": 1 }, { "url": "https://opjak.cz/", "text": "OPJAK.cz", "rank_inner": 2 } ] } } ``` ### Struktura odpowiedzi ```ts type GetAioDetailsResponse = { /** `true` przy powodzeniu; przy błędzie `false` i koperta z `error` */ success: boolean; /** Szczegóły bloku AI Overview dla frazy — pojedynczy obiekt, bez paginacji */ data: AioDetails; } type AioDetails = { /** Pełny tekst bloku AI Overview — surowa treść z SERP (może być w innym języku niż projekt) */ text: string; /** Status pobrania bloku, np. "success" */ status: string; /** Pozycja absolutna bloku AIO w wynikach SERP */ rank_absolute: number; /** Źródła bloku AIO — w zwalidowanej odpowiedzi lista pusta, mimo wypełnionych `content_links` */ sources: unknown[]; /** Linki osadzone bezpośrednio w treści bloku */ content_links: AioContentLink[]; } type AioContentLink = { /** Adres URL linku osadzonego w treści */ url: string; /** Tekst kotwicy linku w treści bloku */ text: string; /** Kolejność linku w treści bloku */ rank_inner: number; } export default GetAioDetailsResponse ``` ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data, unknown */ type: string; message: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > **`418`** jest zwracane również przy błędach walidacji i dostępu. Brak `keyword_id` skutkuje `invalid_data` z regułą `_required` (walidator `AioDetailsValidator`). Cudzy lub nieistniejący `project_id` zwraca `Unauthorized access` (`418`), a nie `404`; `keyword_id` nienależący do projektu również nie przejdzie kontroli `KeywordAccessRules`. ## Powiązane akcje - `getStatistics` — zbiorcze metryki `aio_*` domeny projektu (z historią) - `getKeywords` — frazy projektu z obecnością w AI Overviews - `getDistribution` — rozkład obecności w blokach AIO - `getCompetitors` — porównanie obecności w AIO z konkurentami - `getOpportunities` — frazy z szansą na obecność w AIO - `getAioDetails` — surowa treść bloku AIO dla pojedynczej frazy (ta strona) - `getAioSources` — źródła cytowane w blokach AIO --- # AI Overviews: źródła (`getAioSources`) **`POST /api/rank_tracker/reports/ai_overviews/getAioSources`** Zwraca stronicowaną listę **źródeł cytowanych w bloku AI Overview** dla wskazanej frazy projektu Rank Tracker. W odróżnieniu od pozostałych akcji raportowych tego kontrolera wymaga — poza `project_id` — także **`keyword_id`** (kontrola dostępu `KeywordAccessRules`: fraza musi należeć do projektu). Pełny surowy blok AIO frazy (treść, nie tylko listę źródeł) zwraca uzupełniająca akcja `getAioDetails` (również `project_id` + `keyword_id`). Ten raport — w odróżnieniu od AI Overviews w Analizie widoczności — **nie jest przestarzały**. --- ## Żądanie `POST` `/api/rank_tracker/reports/ai_overviews/getAioSources` Nagłówki: `Authorization: Bearer `, `Content-Type: application/json`. ### Struktura żądania **Podstawowy** ```jsonc filename="żądanie-podstawowe.jsonc" { "project_id": null, "keyword_id": null } ``` **Rozszerzony** ```jsonc filename="żądanie-rozszerzone.jsonc" { "project_id": null, "keyword_id": null, "limit": 2, "page": 1 } ``` **cURL** ```bash curl --location --request POST 'https://api.senuto.com/api/rank_tracker/reports/ai_overviews/getAioSources' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' \ --data-raw '{ "project_id": null, "keyword_id": null, "limit": 2 }' ``` ### Parametry ```ts type GetAioSourcesRequest = { /** * **Wymagane**. ID projektu Rank Tracker (kontrola dostępu `ProjectAccessRules`). * Musi należeć do użytkownika — cudzy lub nieistniejący `project_id` zwraca `418` z komunikatem `Unauthorized access`. * Realny `project_id` pobierzesz z `POST /api/rank_tracker/management/projects/getMyActiveProjects`. */ project_id: number; /** * **Wymagane**. ID frazy w projekcie (kontrola dostępu `KeywordAccessRules` — fraza musi należeć * do podanego `project_id`). Brak `keyword_id` → `418` z `invalid_data`. * ID fraz pobierzesz np. z `getGroupKeywords` / `getProjectKeywords` w kontrolerze Keywords. */ keyword_id: number; /** * Rozmiar strony paginacji. Odbija się w `pagination.limit`. * @default 10 */ limit?: number; /** * Numer strony paginacji. * @default 1 */ page?: number; } export default GetAioSourcesRequest ``` > **Ostrzeżenie:** > Ten endpoint **nie obsługuje** `filtering` ani `order` — wbrew wcześniejszej wersji tej strony. Endpoint w ogóle nie odczytuje tych parametrów, a na prod nieznany klucz w `filtering` **nie** zwraca `418` (jest po cichu ignorowany, `200`). Filtrowanie/sortowanie zastosuj po stronie klienta. (Uwaga: inne raporty modułu AI Overviews — np. `getKeywords`, `getOpportunities`, `getCompetitors` — filtrowanie obsługują; ten konkretny endpoint nie.) > **Ostrzeżenie:** > Blok AI Overview może istnieć dla frazy, a lista źródeł i tak wrócić **pusta** — `data: []` przy `page_count: 1`. Traktuj pustą listę jako poprawną odpowiedź, nie błąd. ## Odpowiedź Po pomyślnym żądaniu otrzymujesz standardową kopertę: `success`, `data` (tablica źródeł cytowanych w bloku AIO) oraz `pagination`. Dla frazy bez cytowanych źródeł tablica `data` jest pusta przy `count: 0` — zwróć uwagę, że w tym przypadku `page_count` wyniosło `1` (inaczej niż `0` w `getKeywords`/`getOpportunities` przy braku danych). **Skrócona** ```json filename="przykładowa-odpowiedź (skrócona)" { "success": true, "data": [], "pagination": { "page_count": 1, "current_page": 1, "has_next_page": false, "has_prev_page": false, "count": 0, "limit": 2 } } ``` **Pełna** ```json filename="przykładowa-odpowiedź (200)" { "success": true, "data": [], "pagination": { "page_count": 1, "current_page": 1, "has_next_page": false, "has_prev_page": false, "count": 0, "limit": 2 } } ``` ### Struktura odpowiedzi ```ts type GetAioSourcesResponse = { /** `true` przy powodzeniu; przy błędzie `false` i koperta z `error` */ success: boolean; /** * Źródła cytowane w bloku AI Overview danej frazy. * Kształt wiersza zależy od źródeł cytowanych w bloku AI Overview danej frazy. */ data: unknown[]; /** Metadane paginacji */ pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; /** Wartość przekazanego `limit` */ limit: number; }; } export default GetAioSourcesResponse ``` ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data, unknown */ type: string; message: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > **`418`** jest zwracane przy błędach walidacji i braku dostępu: brak `keyword_id` → `invalid_data`; cudzy lub nieistniejący `project_id` → `Unauthorized access` (`ProjectAccessRules`); `keyword_id` nienależący do podanego projektu jest odrzucany przez `KeywordAccessRules` — nie `404`. ## Powiązane akcje - `getStatistics` — zbiorcze statystyki AI Overviews projektu - `getKeywords` — frazy wyzwalające AI Overviews - `getDistribution` — rozkład obecności w AI Overviews - `getCompetitors` — konkurenci cytowani w AI Overviews - `getOpportunities` — frazy z AIO, w których domena rankuje organicznie, ale nie jest cytowana - `getAioDetails` — pełny surowy blok AI Overview danej frazy (`project_id` + `keyword_id`) - `getAioSources` — źródła cytowane w bloku AIO frazy (ta strona) --- # Kanibalizacja: frazy (`getKeywords`) **`POST /api/rank_tracker/tools/cannibalization/getKeywords`** Przykładowe żądanie: ```json { "project_id": null, "date_min": "2026-06-20", "date_max": "2026-06-29", "limit": 10 } ``` Przykładowe żądanie (rozszerzone): ```json { "project_id": null, "date_min": "2026-06-20", "date_max": "2026-06-29", "limit": 10, "page": 1, "order": { "prop": "searches", "dir": "desc" }, "filtering": [] } ``` Przykładowa odpowiedź: ```json { "success": true, "data": [ { "keyword": "generator słów kluczowych", "searches": "320", "cpc": 6.4, "snippets": [ "ai_overview", "people_also_ask", "related_searches" ], "unique_urls_count": 2, "urls": [ "https://www.senuto.com/pl/blog/planer-slow-kluczowych/", "https://www.senuto.com/pl/baza-slow-kluczowych/" ] } ], "pagination": { "page_count": 1, "current_page": 1, "has_next_page": false, "has_prev_page": false, "count": 8, "limit": 10 } } ``` Zwraca frazy projektu Monitoringu, na które **rankuje więcej niż jeden URL** tej samej domeny (kanibalizacja) — wraz z liczbą wyszukiwań, CPC, cechami SERP oraz listą konkurujących URL-i. Analiza dotyczy wskazanego projektu (`project_id`) w zadanym zakresie dat. | Fraza | Wyszukiwania/mies. | CPC | Liczba URL-i | Konkurujące URL-e | Snippety SERP | | --- | --- | --- | --- | --- | --- | | generator słów kluczowych | 320 | 6.4 | 2 | ["https://www.senuto.com/pl/blog/planer-slow-kluczowych/","https://www.senuto.com/pl/baza-slow-kluczowych/"] | ["ai_overview","people_also_ask","related_searches"] | | senuto cennik | 320 | 3 | 2 | ["https://www.senuto.com/pl/cennik/","https://www.senuto.com/pl/blog/ai/"] | ["ai_overview","people_also_ask","related_searches"] | | wyszukiwarka słów kluczowych | 480 | 6.9 | 2 | ["https://www.senuto.com/pl/baza-slow-kluczowych/","https://www.senuto.com/pl/blog/wyszukiwanie-slow-kluczowych/"] | ["ai_overview","people_also_ask","related_searches","video_thumbs","videos_pack"] | _projekt dla senuto.com, 2026-06-20 → 2026-06-29. `searches` jest stringiem; `urls` to lista konkurujących adresów._ > **Ostrzeżenie:** > To **narzędzie** (`tools/*`) — każde wywołanie **zużywa jednostkę dziennego limitu narzędzi** (`tools_daily_limit`). Patrz [Limity zapytań](/rate-limits). --- ## Żądanie `POST` `/api/rank_tracker/tools/cannibalization/getKeywords` Nagłówki: `Authorization: Bearer `, `Content-Type: application/json`. ```jsonc filename="żądanie.jsonc" { "project_id": null, "date_min": "2026-06-20", "date_max": "2026-06-29", "limit": 10, "page": 1, "order": { "prop": "searches", "dir": "desc" }, "filtering": [] } ``` ### Parametry ```ts type RtCannibalizationGetKeywordsRequest = { /** **Wymagane**. ID projektu Monitoringu (`getProject`). Realny `project_id` pobierzesz z `POST /api/rank_tracker/management/projects/getMyActiveProjects`. */ project_id: number; /** Data początkowa zakresu (RRRR-MM-DD). */ date_min?: string; /** Data końcowa zakresu (RRRR-MM-DD). */ date_max?: string; /** Liczba wierszy na stronę. @default 10 */ limit?: number; /** Numer strony. @default 1 */ page?: number; /** Sortowanie — obiekt `{ prop, dir }` (np. `searches`). */ order?: { prop: string; dir: 'asc' | 'desc' }; /** Grupy filtrów. Pusta tablica = brak filtrowania. */ filtering?: unknown[]; } export default RtCannibalizationGetKeywordsRequest ``` ## Odpowiedź `data` to lista fraz z kanibalizacją; `pagination` jak w innych raportach. ```ts type RtCannibalizationGetKeywordsResponse = { success: boolean; data: Array<{ keyword: string; /** Liczba wyszukiwań/mies. — **string** */ searches: string; cpc: number; /** Cechy SERP */ snippets: string[]; /** Liczba unikalnych URL-i konkurujących o frazę */ unique_urls_count: number; /** Konkurujące adresy URL */ urls: string[]; }>; pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; limit: number }; } export default RtCannibalizationGetKeywordsResponse ``` ## Błędy Błędy tego endpointu przychodzą we [wspólnej kopercie ze statusem `418`](/types/errors). ## Powiązane akcje - `tools/cannibalization/getUrlsRanking` — ranking konkurujących URL-i dla pojedynczej frazy. --- # Kanibalizacja: URL-e frazy (`getUrlsRanking`) **`POST /api/rank_tracker/tools/cannibalization/getUrlsRanking`** Przykładowe żądanie: ```json { "project_id": null, "keyword": "generator słów kluczowych", "date_min": "2026-06-20", "date_max": "2026-06-29" } ``` Przykładowa odpowiedź: ```json { "success": true, "data": [ { "url": "https://www.senuto.com/pl/baza-slow-kluczowych/", "avg_pos": "40.20", "occurrences": 5 }, { "url": "https://www.senuto.com/pl/blog/planer-slow-kluczowych/", "avg_pos": "31.75", "occurrences": 4 } ], "pagination": { "page_count": 1, "current_page": 1, "has_next_page": false, "has_prev_page": false, "count": 2, "limit": 10 } } ``` Uszczegółowienie kanibalizacji dla **jednej frazy**: zwraca konkurujące URL-e wraz ze średnią pozycją (`avg_pos`) i liczbą pomiarów, w których dany URL wystąpił (`occurrences`) w zadanym zakresie dat. Frazę wskazujesz nazwą (`keyword`), a projekt przez `project_id`. | URL | Średnia pozycja | Liczba pomiarów | | --- | --- | --- | | https://www.senuto.com/pl/baza-slow-kluczowych/ | 40.20 | 5 | | https://www.senuto.com/pl/blog/planer-slow-kluczowych/ | 31.75 | 4 | _projekt Rank Trackera, keyword „generator słów kluczowych”, 2026-06-20 → 2026-06-29. `avg_pos` jest stringiem (2 miejsca po przecinku)._ --- ## Żądanie `POST` `/api/rank_tracker/tools/cannibalization/getUrlsRanking` Nagłówki: `Authorization: Bearer `, `Content-Type: application/json`. ```jsonc filename="żądanie.jsonc" { "project_id": null, "keyword": "generator słów kluczowych", "date_min": "2026-06-20", "date_max": "2026-06-29" } ``` ### Parametry ```ts type RtCannibalizationGetUrlsRankingRequest = { /** **Wymagane**. ID projektu Monitoringu. Realny `project_id` pobierzesz z `POST /api/rank_tracker/management/projects/getMyActiveProjects`. */ project_id: number; /** **Wymagane**. Fraza (dopasowanie po `Keywords.name`). */ keyword: string; /** Data początkowa (RRRR-MM-DD). */ date_min?: string; /** Data końcowa (RRRR-MM-DD). */ date_max?: string; /** Liczba wierszy na stronę. @default 10 */ limit?: number; /** Numer strony. @default 1 */ page?: number; } export default RtCannibalizationGetUrlsRankingRequest ``` > **Ostrzeżenie:** > `keyword` jest dopasowywane po dokładnej nazwie frazy — użyj wartości z `tools/cannibalization/getKeywords` (pole `keyword`). Nieistniejąca fraza da pustą listę. ## Odpowiedź `data` to lista konkurujących URL-i (`url`, `avg_pos` jako string, `occurrences`). ```ts type RtCannibalizationGetUrlsRankingResponse = { success: boolean; data: Array<{ url: string; /** Średnia pozycja — **string**, 2 miejsca po przecinku */ avg_pos: string; /** Liczba pomiarów, w których URL wystąpił */ occurrences: number; }>; pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; limit: number }; } export default RtCannibalizationGetUrlsRankingResponse ``` ## Błędy Błędy tego endpointu przychodzą we [wspólnej kopercie ze statusem `418`](/types/errors). ## Powiązane akcje - `tools/cannibalization/getKeywords` — lista fraz z kanibalizacją (źródło `keyword`). --- # Słownik: lokalizacje (`getLocalizations`) **`GET /api/rank_tracker/dictionary/project/getLocalizations`** Przykładowe żądanie: ```json { "country_id": 1, "name": "Wars" } ``` Przykładowa odpowiedź: ```json { "success": true, "data": [ { "id": 21991, "type": "City", "canonical": "Warsaw,Masovian Voivodeship,Poland", "country_id": 1, "name": "Warszawa, województwo mazowieckie, Polska", "_locale": "pl_PL" } ] } ``` Zwraca listę **lokalizacji** (miasta, dzielnice, regiony) pasujących do frazy `name` w danym kraju — do konfiguracji **lokalnego monitoringu pozycji** (śledzenie wyników dla konkretnej lokalizacji). Każdy wiersz zawiera `id` (do użycia przy tworzeniu projektu), `type`, kanoniczną nazwę oraz nazwę zlokalizowaną. | ID lokalizacji | Typ | Nazwa kanoniczna | ID kraju | Nazwa (lokalna) | Locale | | --- | --- | --- | --- | --- | --- | | 21991 | City | Warsaw,Masovian Voivodeship,Poland | 1 | Warszawa, województwo mazowieckie, Polska | pl_PL | _country_id 1, name „Wars”. Wszystkie pola wiersza._ --- ## Żądanie `GET` `/api/rank_tracker/dictionary/project/getLocalizations` Nagłówki: `Authorization: Bearer `. Parametry w query stringu. ```bash curl --location --request GET 'https://api.senuto.com/api/rank_tracker/dictionary/project/getLocalizations?country_id=1&name=Wars' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' ``` ### Parametry ```ts type DictionaryGetLocalizationsRequest = { /** ID kraju (bazy). Wymagany `country_id` **lub** `region_id` — bez żadnego z nich zwracana jest pusta lista. */ country_id?: number; /** Fragment nazwy lokalizacji do dopasowania (np. `Wars`). Opcjonalny. */ name?: string; /** ID regionu (zawężenie). Opcjonalny. */ region_id?: number; } export default DictionaryGetLocalizationsRequest ``` > **Ostrzeżenie:** > Warunek `where` budowany jest z `country_id`/`region_id` — jeśli **żadnego nie podasz**, zapytanie zwróci **pustą** listę. Podaj co najmniej `country_id`. ## Odpowiedź `data` to lista lokalizacji. ```ts type DictionaryGetLocalizationsResponse = { success: boolean; data: Array<{ id: number; /** np. City, Neighborhood, Borough, Region */ type: string; /** Nazwa kanoniczna (ang.), np. `Warsaw,Masovian Voivodeship,Poland` */ canonical: string; country_id: number; /** Nazwa zlokalizowana */ name: string; _locale: string; }>; } export default DictionaryGetLocalizationsResponse ``` ## Błędy Błędy tego endpointu przychodzą we [wspólnej kopercie ze statusem `418`](/types/errors). ## Powiązane akcje - [`countries/getList`](/countries) — słownik krajów; `id` stąd podajesz jako `country_id` w tym endpoincie - [`projects/getListWithExtendedData`](/modules/rank_tracker/rt-projects-getListWithExtendedData) — projekty wraz z ustawioną lokalizacją - [`projects/getProjectFits`](/modules/rank_tracker/rt-projects-getProjectFits) — tryby dopasowania projektu - [`positions/getData`](/modules/rank_tracker/rt-positions-getData) — pozycje fraz w projekcie, także dla monitoringu lokalnego --- # Porównanie dni (`getData`) **`POST /api/rank_tracker/tools/compare_days/getData`** Przykładowe żądanie: ```json { "project_id": null, "domain": "senuto.com", "compareDates": [ { "date": "2026-06-20" }, { "date": "2026-06-29" } ], "limit": 10 } ``` Przykładowa odpowiedź: ```json { "success": true, "data": [ { "kid": "4de724d3f86734765dd07036c21f4105", "cpc": 3, "keyword": "senuto cennik", "searches": "320", "snippets": [ "ai_overview", "people_also_ask", "related_searches" ], "2026-06-20": { "pos": 1, "url": "https://www.senuto.com/pl/cennik/" }, "2026-06-29": { "pos": 1, "url": "https://www.senuto.com/pl/cennik/", "diff": 0 } } ], "pagination": { "page_count": 1, "current_page": 1, "has_next_page": false, "has_prev_page": false, "count": 1, "limit": 10 } } ``` Porównuje **pozycje fraz** wskazanej domeny w projekcie między wybranymi **dniami pomiaru** (`compareDates`). Dla każdej frazy zwraca wiersz z metadanymi (`keyword`, `searches`, `cpc`, `snippets`) oraz — pod kluczem każdej daty — pozycję (`pos`), URL i różnicę (`diff`) względem poprzedniej daty. | Fraza | Wyszukiwania/mies. | CPC | Poz. 20.06 | Poz. 29.06 | Różnica | URL (29.06) | | --- | --- | --- | --- | --- | --- | --- | | senuto cennik | 320 | 3 | 1 | 1 | 0 | https://www.senuto.com/pl/cennik/ | _projekt dla senuto.com, dni 2026-06-20 i 2026-06-29. Kolumny dat pochodzą z compareDates (klucze-daty)._ > **Ostrzeżenie:** > To **narzędzie** (`tools/*`) — zużywa jednostkę dziennego limitu narzędzi (`tools_daily_limit`). Patrz [Limity zapytań](/rate-limits). --- ## Żądanie `POST` `/api/rank_tracker/tools/compare_days/getData` ```jsonc filename="żądanie.jsonc" { "project_id": null, "domain": "senuto.com", "compareDates": [{ "date": "2026-06-20" }, { "date": "2026-06-29" }], "limit": 10, "page": 1, "order": { "prop": "searches", "dir": "desc" }, "filtering": [] } ``` ### Parametry ```ts type RtCompareDaysRequest = { /** **Wymagane** (lub `group_id`). ID projektu Monitoringu. Realny `project_id` pobierzesz z `POST /api/rank_tracker/management/projects/getMyActiveProjects`. */ project_id?: number; /** Alternatywnie: ID grupy fraz zamiast całego projektu. */ group_id?: number; /** Domena, której pozycje porównujesz (w obrębie projektu). */ domain?: string; /** **Dni do porównania** — tablica obiektów `{ date: "RRRR-MM-DD" }`. */ compareDates: Array<{ date: string }>; /** Liczba wierszy na stronę. @default 10 */ limit?: number; /** Numer strony. @default 1 */ page?: number; /** Sortowanie `{ prop, dir }`. */ order?: { prop: string; dir: 'asc' | 'desc' }; /** Grupy filtrów. */ filtering?: unknown[]; } export default RtCompareDaysRequest ``` ## Odpowiedź Każdy wiersz to fraza; pod kluczem każdej daty (z `compareDates`) znajduje się `{ pos, url, diff }`. ```ts type RtCompareDaysResponse = { success: boolean; data: Array<{ kid: string; keyword: string; searches: string; cpc: number; snippets: string[]; /** Klucz = data z compareDates (RRRR-MM-DD) */ [date: string]: { pos: number; url: string; diff?: number } | string | number | string[]; }>; pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; limit: number }; } export default RtCompareDaysResponse ``` ## Błędy Błędy tego endpointu przychodzą we [wspólnej kopercie ze statusem `418`](/types/errors). ## Powiązane akcje - `tools/compare_serp/getData` — porównanie SERP dla pojedynczej frazy między dniami. --- # Porównanie SERP (`getData`) **`POST /api/rank_tracker/tools/compare_serp/getData`** Przykładowe żądanie: ```json { "project_id": null, "keyword": "generator słów kluczowych", "position": 10, "compareDates": [ { "date": "2026-06-20" }, { "date": "2026-06-29" } ] } ``` Przykładowa odpowiedź: ```json { "success": true, "data": [ { "position": 1, "2026-06-20": { "position": 1, "domain": "www.seoptimer.com", "url": "https://www.seoptimer.com/pl/keyword-generator" }, "2026-06-29": { "position": 1, "domain": "pl.semrush.com", "url": "https://pl.semrush.com/analytics/keywordmagic/", "diff": -1 } } ], "pagination": { "page_count": 1, "current_page": 1, "has_next_page": false, "has_prev_page": false, "count": 10, "limit": 10 } } ``` Porównuje **wyniki wyszukiwania (SERP)** dla pojedynczej frazy między wybranymi dniami — dla każdej pozycji TOP (do `position`) pokazuje, jaka domena/URL ją zajmowała w danym dniu oraz zmianę (`diff`). Pozwala zobaczyć, jak zmienił się układ SERP wokół monitorowanej frazy. | Pozycja | Domena 20.06 | URL 20.06 | Domena 29.06 | Różnica | | --- | --- | --- | --- | --- | | 1 | www.seoptimer.com | https://www.seoptimer.com/pl/keyword-generator | pl.semrush.com | -1 | _projekt Rank Trackera, keyword „generator słów kluczowych”, dni 2026-06-20 i 2026-06-29. Kolumny dat pochodzą z compareDates._ > **Ostrzeżenie:** > To **narzędzie** (`tools/*`) — zużywa jednostkę `tools_daily_limit`. `keyword` musi istnieć (dopasowanie po `Keywords.name`) — inaczej `418` (`invalid_data`). --- ## Żądanie `POST` `/api/rank_tracker/tools/compare_serp/getData` ```jsonc filename="żądanie.jsonc" { "project_id": null, "keyword": "generator słów kluczowych", "position": 10, "compareDates": [{ "date": "2026-06-20" }, { "date": "2026-06-29" }] } ``` ### Parametry ```ts type RtCompareSerpRequest = { /** **Wymagane**. ID projektu Monitoringu. Realny `project_id` pobierzesz z `POST /api/rank_tracker/management/projects/getMyActiveProjects`. */ project_id: number; /** **Wymagane**. Fraza (dopasowanie po `Keywords.name`). */ keyword: string; /** Maksymalna pozycja SERP do porównania (zakres 1..position). @default 10 */ position?: number; /** **Dni do porównania** — tablica `{ date: "RRRR-MM-DD" }`. */ compareDates: Array<{ date: string }>; } export default RtCompareSerpRequest ``` ## Odpowiedź Każdy wiersz to slot pozycji SERP; pod kluczem każdej daty `{ position, domain, url, diff }`. ```ts type RtCompareSerpResponse = { success: boolean; data: Array<{ /** Slot pozycji SERP */ position: number; /** Klucz = data z compareDates */ [date: string]: { position: number; domain: string; url: string; diff?: number } | number; }>; pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; limit: number }; } export default RtCompareSerpResponse ``` ## Błędy Błędy tego endpointu przychodzą we [wspólnej kopercie ze statusem `418`](/types/errors). ## Powiązane akcje - `tools/compare_days/getData` — porównanie pozycji fraz domeny między dniami. --- # Pozostałe: Landing pages · statystyki URL-i (`getUrlsStatistics`) **`POST /api/rank_tracker/reports/landing_pages/getUrlsStatistics`** Zwraca stronicowaną listę statystyk adresów URL (landing pages) projektu Rank Tracker w zadanym zakresie dat. Wymagane są `project_id` oraz zakres `date_min`–`date_max` w formacie `YYYY-MM-DD` (walidator `UrlsStatisticsValidator`). --- ## Żądanie `POST` `/api/rank_tracker/reports/landing_pages/getUrlsStatistics` Nagłówki: `Authorization: Bearer `, `Content-Type: application/json`. ### Struktura żądania **Podstawowy** ```jsonc filename="żądanie-podstawowe.jsonc" { "project_id": null, "date_min": "2026-06-20", "date_max": "2026-06-29" } ``` **Rozszerzony** ```jsonc filename="żądanie-rozszerzone.jsonc" { "project_id": null, "date_min": "2026-06-20", "date_max": "2026-06-29", "limit": 2, "page": 1 } ``` **cURL** ```bash curl --location --request POST 'https://api.senuto.com/api/rank_tracker/reports/landing_pages/getUrlsStatistics' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' \ --data-raw '{ "project_id": null, "date_min": "2026-06-20", "date_max": "2026-06-29", "limit": 2 }' ``` ### Parametry ```ts type GetUrlsStatisticsRequest = { /** * **Wymagane**. ID projektu Rank Tracker. Musi należeć do użytkownika, * inaczej zwracane jest `418` z `Unauthorized access`. * Realny `project_id` pobierzesz z `POST /api/rank_tracker/management/projects/getMyActiveProjects`. */ project_id: number; /** * **Wymagane**. Początek zakresu dat w formacie `YYYY-MM-DD`. * Brak pola → `418` z `{"date_min":{"_required":"This field is required"}}`. */ date_min: string; /** * **Wymagane**. Koniec zakresu dat w formacie `YYYY-MM-DD`. * **Uwaga (bug):** gdy `date_min > date_max`, komunikat walidacji `DateRangeRules` jest odwrócony. */ date_max: string; /** * Filtrowanie listy adresów URL. Rejestr filtrów tego endpointu jest **wąski** — * dozwolone klucze: `url` oraz alias `statistics.url.current`. * ⚠️ **Uwaga:** ten endpoint (backend MySQL) na **nieznany lub źle sformułowany** filtr zwraca * `HTTP 500` (nie `418`) — używaj wyłącznie kluczy z listy. */ filtering?: Array<{ filters: Array<{ key: string; match?: string; value: string | number; complement?: boolean }>; conjunction?: 'and' | 'or'; }>; /** * Rozmiar strony paginacji. Odbija się w `pagination.limit`. * @default 10 */ limit?: number; /** * Numer strony paginacji. * @default 1 */ page?: number; } export default GetUrlsStatisticsRequest ``` > **Ostrzeżenie:** > **Pułapki potwierdzone na produkcji:** > > - Brak `date_min`/`date_max` → `418` z `{"date_min":{"_required":"This field is required"},"date_max":{"_required":"This field is required"}}`. > - **Znany bug:** przy `date_min > date_max` komunikat walidacji `DateRangeRules` jest **odwrócony** — wskazuje niewłaściwe pole zakresu. > - `pagination.count` jest zwracane jako **string**, pozostałe pola paginacji są liczbami. > - **Ten sam wiersz podaje metryki dwoma typami:** na najwyższym poziomie `visibility`, `top3`, > `top10`, `top50`, `sum_searches` i `last_position` to **stringi**, a w obiekcie `statistics` > te same wartości są **liczbami** (poza `searches`, które zostaje stringiem). Rzutuj typy > przed obliczeniami. ## Odpowiedź Po pomyślnym żądaniu otrzymujesz `data` (tablicę statystyk URL-i) oraz `pagination`. Projekt, który w podanym zakresie dat nie ma pozycjonujących się adresów, zwraca pustą tablicę. **Skrócona** ```json filename="przykładowa-odpowiedź (skrócona)" { "success": true, "data": [ { "uid": "a0a93179e90261866c955748815a6b27", "url": "https://example.com/produkt/", "visibility": "7.52", "top3": "1", "top10": "1", "top50": "1", "sum_searches": "70", "best_keyword": "nazwa produktu", "last_position": "3", "page_path": "//example.com/produkt/" } ], "pagination": { "page_count": 1, "current_page": 1, "has_next_page": false, "has_prev_page": false, "count": "1", "limit": 3 } } ``` **Pełna** ```json filename="przykładowa-odpowiedź (200)" { "success": true, "data": [ { "uid": "a0a93179e90261866c955748815a6b27", "url": "https://example.com/produkt/", "visibility": "7.52", "top3": "1", "top10": "1", "top50": "1", "sum_searches": "70", "best_keyword": "nazwa produktu", "last_position": "3", "statistics": { "position": { "current": 3 }, "visibility": { "current": 7.52 }, "searches": { "current": "70" }, "url": { "current": "https://example.com/produkt/" }, "top3": { "current": 1 }, "top10": { "current": 1 }, "top50": { "current": 1 } }, "page_path": "//example.com/produkt/" } ], "pagination": { "page_count": 1, "current_page": 1, "has_next_page": false, "has_prev_page": false, "count": "1", "limit": 3 } } ``` ### Struktura odpowiedzi ```ts type GetUrlsStatisticsResponse = { /** `true` przy powodzeniu; przy błędzie `false` i koperta z `error` */ success: boolean; /** Statystyki adresów docelowych. */ data: Array<{ /** Identyfikator adresu w raporcie. */ uid: string; url: string; /** Ścieżka adresu bez schematu, w formie `//domena/sciezka`. */ page_path: string; /** **Uwaga na typy:** te pola wracają jako **stringi**, nie liczby. */ visibility: string; top3: string; top10: string; top50: string; sum_searches: string; last_position: string; /** Fraza, na której adres wypada najwyżej. */ best_keyword: string; /** Te same metryki w formie zagnieżdżonej — tu `position`, `visibility`, `top3`, * `top10` i `top50` są **liczbami**, a `searches` pozostaje stringiem. */ statistics: { position: { current: number }; visibility: { current: number }; searches: { current: string }; url: { current: string }; top3: { current: number }; top10: { current: number }; top50: { current: number }; }; }>; /** Metadane paginacji */ pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; /** **Uwaga:** string, nie liczba (np. `"0"`) */ count: string; limit: number; }; } export default GetUrlsStatisticsResponse ``` ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data, unknown */ type: string; message: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > **`418`** jest zwracane przy błędach walidacji (`invalid_data`) — nie tylko przy ograniczaniu liczby żądań. Brak dat → `418` z `{"date_min":{"_required":"This field is required"},"date_max":{"_required":"This field is required"}}`. Cudzy lub nieistniejący `project_id` zwraca `418` z `Unauthorized access`, a nie `404`. Pamiętaj o odwróconym komunikacie `DateRangeRules` przy `date_min > date_max`. ## Powiązane akcje - `getUrlsStatistics` — statystyki URL-i (landing pages) w zakresie dat (ta strona); jedyna zbadana akcja kontrolera `LandingPages` --- # Pozostałe: lista grup (`list`) **`GET /api/rank_tracker/management/groups/list`** Zwraca grupy fraz kluczowych zdefiniowane w projekcie Rank Tracker wraz z podstawowymi metadanymi każdej grupy (`id`, `name`, `is_dynamic`, `keywords_number`). Odpowiedź jest opakowana w kopertę z paginacją (`pagination`). | Nazwa | ID grupy | Dynamiczna | Liczba fraz | | --- | --- | --- | --- | | eee | 21423 | 0 | 0 | _limit 2 — grupy fraz zdefiniowane w projekcie Rank Tracker. Wszystkie adresowalne pola wiersza._ --- ## Żądanie `GET` `/api/rank_tracker/management/groups/list` Nagłówki: `Authorization: Bearer `. Parametry przekazuj w **query stringu** (np. `?project_id=87913&limit=2&page=1`) — przekazanie ich w body żądania zostanie zignorowane i zwróci `418`. ### Struktura żądania **Podstawowy** ```jsonc filename="żądanie-podstawowe.jsonc" { "project_id": null } ``` **Rozszerzony** ```jsonc filename="żądanie-rozszerzone.jsonc" { "project_id": null, "limit": 2, "page": 1 } ``` **cURL** ```bash curl --location --request GET 'https://api.senuto.com/api/rank_tracker/management/groups/list?project_id=87913&limit=2&page=1' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' ``` ### Parametry ```ts type GroupsListRequest = { /** * **Wymagane**. ID projektu Rank Tracker. Użytkownik musi mieć dostęp do projektu * (właściciel, admin lub udostępnienie ACL); w przeciwnym razie `418` `Unauthorized access`. * Realny `project_id` pobierzesz z `POST /api/rank_tracker/management/projects/getMyActiveProjects`. */ project_id: number; /** * Liczba grup na stronę (paginacja). Nieujemna liczba całkowita. * Gdy pominięte, zwracane są wszystkie grupy (`pagination.limit = null`). */ limit?: number; /** * Numer strony (paginacja). Nieujemna liczba całkowita. * @default 1 */ page?: number; } export default GroupsListRequest ``` > **Ostrzeżenie:** > Ten endpoint używa metody **`GET`** — parametry należy przekazywać w **query stringu** (kontroler odczytuje `getQuery()`, a nie `getData()`). Żądanie `POST` z parametrami w body zwraca `418` `invalid_data` (`params.project_id._required = "This field is required"`), ponieważ body jest ignorowane. Wymagany jest wyłącznie **`project_id`**; brak dostępu do projektu również skutkuje `418` (`Unauthorized access`). ## Odpowiedź Po pomyślnym żądaniu otrzymujesz `data` (płaską tablicę grup) oraz `pagination`. **Skrócona** ```json filename="przykładowa-odpowiedź (skrócona)" { "success": true, "data": [ { "id": 21423, "name": "eee", "is_dynamic": 0, "keywords_number": 0 } ], "pagination": { "page_count": 1, "current_page": 1, "has_next_page": false, "has_prev_page": false, "count": 1, "limit": 2 } } ``` **Pełna** ```json filename="przykładowa-odpowiedź (200)" { "success": true, "data": [ { "id": 21423, "name": "eee", "is_dynamic": 0, "keywords_number": 0 } ], "pagination": { "page_count": 1, "current_page": 1, "has_next_page": false, "has_prev_page": false, "count": 1, "limit": 2 } } ``` ### Struktura odpowiedzi ```ts type GroupsListResponse = { /** `true` przy powodzeniu; przy błędzie `false` i koperta z `error` */ success: boolean; /** Zwrócone grupy fraz kluczowych */ data: Group[]; /** Metadane paginacji */ pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; /** Zastosowany `limit`; `null`, gdy parametr `limit` został pominięty */ limit: number | null; }; } type Group = { /** ID grupy */ id: number; /** Nazwa grupy */ name: string; /** * Czy grupa jest dynamiczna. UWAGA: akcja `list` zwraca surową liczbę całkowitą 0/1 * (nie wartość boolean). Dla porównania akcja `get` mapuje to pole na boolean i dodaje pole `filtering`. */ is_dynamic: 0 | 1; /** * Liczba fraz kluczowych w grupie. Pochodzi z `SUM(...)` w SQL — może być zwracana * jako liczba lub string, zależnie od sterownika bazy danych. */ keywords_number: number | string; } export default GroupsListResponse ``` ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data, unauthorized, unknown */ type: string; message: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > **`418`** jest zwracane również w przypadku błędów walidacji — nie tylko przy ograniczaniu liczby żądań. Brak `project_id` (lub przekazanie parametrów w body zamiast w query stringu) → > `{"success":false,"data":{"error":{"type":"invalid_data","params":{"project_id":{"_required":"This field is required"}}}}}`. > Brak dostępu do wskazanego projektu → `418` z komunikatem `Unauthorized access`. ## Powiązane akcje - `list` — lista grup z paginacją (ta strona) - `get` — pojedyncza grupa po `group_id` (operacja odczytu, czyta `group_id` z query stringu; `is_dynamic` jako boolean + pole `filtering`) - `create` / `createDynamic` — utworzenie grupy statycznej / dynamicznej (`POST`, body przez `getData`) - `edit` / `editDynamic` — edycja grupy statycznej / dynamicznej (`POST`, body przez `getData`) - `delete` — usunięcie grupy (`POST`, body przez `getData`) --- # Pozostałe: konkurencja fraz (`getData`) **`POST /api/rank_tracker/tools/competition/getData`** Przykładowe żądanie: ```json { "project_id": null, "mode": "uncommon", "gte": 1, "lte": 50, "mainDomain": { "domain": "senuto.com", "gte": 1, "lte": 50 }, "compareDomains": [ { "domain": "pl.semrush.com", "gte": 1, "lte": 50 } ], "limit": 10 } ``` Przykładowa odpowiedź: ```json { "success": true, "data": [ { "keyword": "wyszukiwanie slow kluczowych", "searches": "480", "cpc": 6.88, "snippets": [ "ai_overview", "people_also_ask", "related_searches", "video_thumbs", "videos_pack" ], "senuto.com": null, "pl.semrush.com": 10 } ], "pagination": { "page_count": 1, "current_page": 1, "has_next_page": false, "has_prev_page": false, "count": 8, "limit": 10 } } ``` Porównuje frazy między **domeną główną** a **konkurentami** w projekcie, w wybranym zakresie pozycji. Tryb (`mode`) decyduje, co zwrócić: - `common` — frazy, na które rankują **wszystkie** porównywane domeny, - `uncommon` — frazy, na które rankuje **konkurent, a nie** domena główna (luki), - `uncommon_domain` — frazy unikalne dla **domeny głównej**. | Fraza | Wyszukiwania/mies. | CPC | Snippety SERP | | --- | --- | --- | --- | | wyszukiwanie slow kluczowych | 480 | 6.88 | ["ai_overview","people_also_ask","related_searches","video_thumbs","videos_pack"] | | wyszukiwarka słów kluczowych | 480 | 6.9 | ["ai_overview","people_also_ask","related_searches","video_thumbs","videos_pack"] | _projekt Rank Trackera, mode uncommon, senuto.com vs pl.semrush.com. Pozycje per domena są pod kluczami-domenami (np. „pl.semrush.com”: 10, „senuto.com”: null) — mają kropki w nazwie, więc nie są tu kolumnami; komplet w JSON._ > **Ostrzeżenie:** > To **narzędzie** (`tools/*`) — zużywa jednostkę `tools_daily_limit`. `mainDomain` i każdy wpis `compareDomains` to **obiekty** `{ domain, gte, lte }` (zakres pozycji); brak wymaganych pól → `418` (`invalid_data`). --- ## Żądanie `POST` `/api/rank_tracker/tools/competition/getData` ```jsonc filename="żądanie.jsonc" { "project_id": null, "mode": "uncommon", "gte": 1, "lte": 50, "mainDomain": { "domain": "senuto.com", "gte": 1, "lte": 50 }, "compareDomains": [{ "domain": "pl.semrush.com", "gte": 1, "lte": 50 }] } ``` ### Parametry ```ts type RtCompetitionRequest = { /** **Wymagane**. ID projektu Monitoringu. Realny `project_id` pobierzesz z `POST /api/rank_tracker/management/projects/getMyActiveProjects`. */ project_id: number; /** **Wymagane**. Tryb: `common` | `uncommon` | `uncommon_domain`. */ mode: 'common' | 'uncommon' | 'uncommon_domain'; /** **Wymagane**. Dolna granica zakresu pozycji (globalna). */ gte: number; /** **Wymagane**. Górna granica zakresu pozycji (globalna). */ lte: number; /** **Wymagane**. Domena główna: obiekt `{ domain, gte, lte }`. */ mainDomain: { domain: string; gte: number; lte: number }; /** **Wymagane** (niepusta). Konkurenci: tablica `{ domain, gte, lte }`. */ compareDomains: Array<{ domain: string; gte: number; lte: number }>; /** Liczba wierszy na stronę. @default 10 */ limit?: number; /** Numer strony. @default 1 */ page?: number; } export default RtCompetitionRequest ``` ## Odpowiedź Każdy wiersz to fraza; pozycja każdej porównywanej domeny znajduje się pod **kluczem = nazwa domeny** (wartość `null`, gdy domena nie rankuje). Klucze-domeny zawierają kropki, dlatego w tabeli powyżej pokazano tylko pola wspólne. ```ts type RtCompetitionResponse = { success: boolean; data: Array<{ keyword: string; searches: string; cpc: number; snippets: string[]; /** Klucz = nazwa domeny (np. "senuto.com"); wartość = pozycja lub null */ [domain: string]: number | null | string | string[]; }>; pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; limit: number }; } export default RtCompetitionResponse ``` ## Błędy Błędy tego endpointu przychodzą we [wspólnej kopercie ze statusem `418`](/types/errors). ## Powiązane akcje - `tools/cannibalization/getKeywords` — frazy z wieloma URL-ami tej samej domeny. --- --- title: Baza słów kluczowych sidebarTitle: Baza słów kluczowych asIndexPage: true ----------------- # Baza słów kluczowych Moduł **Bazy słów kluczowych** dostarcza dane o frazach niezależnie od domeny — liczbę wyszukiwań, koszt kliknięcia (CPC), trendy i cechy SERP. Poza raportami odpytywanymi na bieżąco udostępnia też narzędzia wsadowe: obliczanie statystyk dla własnej listy fraz oraz eksport wyników do CSV. Wspólne mechanizmy: [Filtrowanie](/types/filter) · [Paginacja](/types/pagination) · [Błędy i status `418`](/types/errors). ## Raporty fraz - [Wyszukiwarka](/modules/keywords_analysis/ka-keywords-getKeywords) - [Powiązane](/modules/keywords_analysis/ka-keywords-getRelated) - [Pytania](/modules/keywords_analysis/ka-keywords-getQuestions) - [Chmura tagów](/modules/keywords_analysis/ka-keywords-getTagsCloud) - [Części mowy](/modules/keywords_analysis/ka-keywords-getSpeechParts) - [Trendy](/modules/keywords_analysis/ka-keywords-getTrending) - [Domeny w wynikach](/modules/keywords_analysis/ka-keywords-getDomainsList) - [Statystyki zbioru](/modules/keywords_analysis/ka-keywords-getResultsStatistics) - [Sugestie fraz](/modules/keywords_analysis/ka-keywords-suggester-suggest) - [Statystyki frazy](/modules/keywords_analysis/ka-keyword-details-getStatistics) ## Statystyki fraz - [Utworzenie](/modules/keywords_analysis/ka-statistics-create) - [Status](/modules/keywords_analysis/ka-statistics-checkData) - [Wiersze](/modules/keywords_analysis/ka-statistics-getKeywords) ## Eksport - [Statystyki fraz (CSV)](/modules/keywords_analysis/ka-export-statistics-getKeywords) --- # Baza słów kluczowych: wyszukiwarka (`getKeywords`) **`POST /api/keywords_analysis/reports/keywords/getKeywords`** Przykładowe żądanie: ```json { "parameters": [ { "data_fetch_mode": "keyword", "value": [ "buty do biegania" ] } ], "match_mode": "wide", "country_id": 1, "limit": 10 } ``` Przykładowe żądanie (rozszerzone): ```json { "parameters": [ { "data_fetch_mode": "keyword", "value": [ "buty do biegania", "nike" ] } ], "match_mode": "medium", "country_id": 1, "filtering": [ { "filters": [ { "key": "searches", "match": "gte", "value": 100000 } ], "conjunction": "and" } ], "order": { "prop": "searches", "dir": "ASC" }, "limit": 20, "page": 1 } ``` Przykładowa odpowiedź: ```json { "success": true, "data": [ { "keyword": "nike airmax", "kid": "c9db40a54eacd9ab571c31b82d0b1ac5", "added": "2021-10-19", "searches": 135000, "cpc": 0.67, "cpc_min": 0.23, "cpc_max": 1.1, "words_count": 2, "variations": [], "variations_number": 0, "snippets": [ "image_thumbs", "pla", "top_bar" ], "trends": [ 90500, 110000, 201000, 165000, 135000, 110000, 110000, 135000, 135000, 110000, 90500, 74000 ] } ], "pagination": { "page_count": 4471, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 8941, "limit": 2 } } ``` Główna **wyszukiwarka bazy słów kluczowych** (Keyword Explorer): zwraca frazy pasujące do zapytania (`parameters`) wraz z metrykami — liczbą wyszukiwań, CPC (min/średnie/max), liczbą słów, trendem 12‑miesięcznym, cechami SERP (`snippets`) i wariacjami frazy. Zapytanie budujesz z jednej lub wielu grup `parameters` (fraza / URL / domena / katalog) oraz trybu dopasowania `match_mode`. | Fraza | KID | Wyszukiwania/mies. | CPC | CPC min | CPC max | Liczba słów | Wariacje | Trend (12 mies.) | Snippety SERP | Dodano | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | nike airmax | c9db40a54eacd9ab571c31b82d0b1ac5 | 135000 | 0.67 | 0.23 | 1.1 | 2 | 0 | [90500,110000,201000,165000,135000,110000,110000,135000,135000,110000,90500,74000] | ["image_thumbs","pla","top_bar"] | 2021-10-19 | | nike air maxes | dda94a2fbd99088b392c017ef4430e4d | 135000 | 0.67 | 0.23 | 1.1 | 3 | 0 | [90500,110000,201000,165000,135000,110000,110000,135000,135000,110000,90500,74000] | ["adwords","image_thumbs","map","pla","top_bar","yellow_pages"] | 2022-08-01 | _parameters keyword „buty do biegania”, match_mode wide, country_id 1. Pominięto zdublowane pola: trend_1..12 (= tablica trends) oraz obiekt statistics (= pola top-level)._ > **Ostrzeżenie:** > Uruchomienie zużywa jednostkę dziennego limitu zapytań Bazy słów kluczowych (`keywords_analysis_queries_per_day`) — patrz [Limity zapytań](/rate-limits). Przeglądanie kolejnych stron już pobranego wyniku nie zużywa kolejnej jednostki. --- ## Żądanie `POST` `/api/keywords_analysis/reports/keywords/getKeywords` ```jsonc filename="żądanie.jsonc" { "parameters": [ { "data_fetch_mode": "keyword", "value": ["buty do biegania"] } ], "match_mode": "wide", "country_id": 1, "limit": 10, "page": 1 } ``` ### Parametry ```ts type KeywordsGetKeywordsRequest = { /** * **Wymagane** (niepusta tablica). Grupy zapytania — każda określa źródło i wartości. */ parameters: Array<{ /** **Wymagane**. Typ źródła: `keyword` | `url` | `domain` | `catalog`. */ data_fetch_mode: 'keyword' | 'url' | 'domain' | 'catalog'; /** **Wymagane** (niepusta tablica). Wartości do dopasowania (frazy/URL-e/domeny). */ value: string[]; }>; /** **Wymagane**. Tryb dopasowania: `wide` | `medium` | `narrow`. */ match_mode: 'wide' | 'medium' | 'narrow'; /** ID kraju (bazy słów), np. `1` (PL 1.0), `200` (PL 2.0). */ country_id?: number; /** * Filtrowanie wyników. Tablica grup — grupy łączone są operatorem OR. * Nieznany `key` zwraca błąd `invalid_filtering` (HTTP 418). Patrz sekcja „Filtrowanie i sortowanie". */ filtering?: Array<{ /** Warunki w grupie. */ filters: Array<{ /** Pole do filtrowania, np. `searches` | `cpc` | `words_count` | `added` | `snippets`. */ key: string; /** Operator porównania (pola liczbowe/daty): `gt` | `gte` | `lt` | `lte` | `eq`. @default eq */ match?: 'gt' | 'gte' | 'lt' | 'lte' | 'eq'; /** Wartość (lub tablica wartości dla filtrów wielowartościowych). */ value: string | number | Array; /** `false` = negacja warunku (wyklucz pasujące). @default true */ complement?: boolean; }>; /** Łączenie warunków w grupie: `and` | `or`. @default and */ conjunction?: 'and' | 'or'; }>; /** * Sortowanie. **Uwaga:** wymagana forma `{ prop, dir }` — inne formy są ignorowane * (wpada domyślne `searches`/`DESC`). */ order?: { /** Pole: `searches` | `cpc` | `cpc_min` | `cpc_max` | `words_count` | `difficulty` | `keyword` | `added` | `trend_1`…`trend_12`. */ prop: string; /** Kierunek: `ASC` | `DESC`. @default DESC */ dir: 'ASC' | 'DESC'; }; /** Liczba wierszy na stronę. @default 10 */ limit?: number; /** Numer strony. @default 1 */ page?: number; } export default KeywordsGetKeywordsRequest ``` ## Filtrowanie i sortowanie Wyniki możesz zawężać opcjonalnym polem `filtering` oraz porządkować polem `order`. Oba są niezależne od `parameters`/`match_mode` (te definiują _co_ przeszukujemy; `filtering`/`order` — _jak zawężamy i porządkujemy_ wynik). Ogólny opis mechanizmu: [Filtrowanie (`filtering`)](/types/filter). ```jsonc filename="filtering + order.jsonc" { "parameters": [{ "data_fetch_mode": "keyword", "value": ["buty do biegania"] }], "match_mode": "wide", "country_id": 1, "filtering": [ { "filters": [ { "key": "searches", "match": "gte", "value": 100000 } ], "conjunction": "and" } ], "order": { "prop": "searches", "dir": "ASC" } } ``` **Filtry** (`filtering[].filters[]`) — dozwolone `key` (nieznany klucz → błąd `invalid_filtering`, HTTP 418): | `key` | Typ / operatory `match` | | -------------------------------------------------------------------------------------- | -------------------------------------- | | `searches`, `words_count` | liczbowy: `gt` `gte` `lt` `lte` `eq` | | `cpc` | walutowy: `gt` `gte` `lt` `lte` `eq` | | `added` | data: `gt` `gte` `lt` `lte` `eq` | | `snippets` | cechy SERP (tablica wartości) | | `speech_parts` | części mowy (tablica wartości) | | `trends_peaks` | szczyty trendu | | `domains`, `group`, `keywords` | dopasowanie tekstowe/wielowartościowe | | `statistics.cpc.current`, `statistics.searches.current`, `statistics.snippets.current` | aliasy pól `cpc`/`searches`/`snippets` | Grupy w `filtering` łączone są operatorem **OR**, a warunki wewnątrz grupy — polem `conjunction` (`and`/`or`, domyślnie `and`). `complement: false` neguje warunek (wyklucza pasujące wiersze). **Sortowanie** (`order`) — **wyłącznie** w formie `{ "prop": , "dir": "ASC" | "DESC" }`. Inne formy (np. `{ "searches": "ASC" }`) są ignorowane i wpada domyślne `searches`/`DESC`. Pola: `searches`, `cpc`, `cpc_min`, `cpc_max`, `words_count`, `difficulty`, `keyword`, `added`, `trend_1`…`trend_12`. > **Ostrzeżenie:** > Nieznany `key` w `filtering` zwraca `success: false` z `error.type = "invalid_filtering"` i statusem **HTTP 418** (nie 400) — jak wszystkie błędy walidacyjne tego API, patrz [Błędy](/types/errors). ## Odpowiedź `data` to lista fraz; `pagination` jak w innych raportach. ```ts type KeywordsGetKeywordsResponse = { success: boolean; data: Array<{ keyword: string; kid: string; /** Data dodania frazy do bazy (RRRR-MM-DD) */ added: string; /** Średnia miesięczna liczba wyszukiwań */ searches: number; cpc: number; cpc_min: number; cpc_max: number; words_count: number; /** Wariacje frazy i ich liczba */ variations: string[]; variations_number: number; /** Cechy SERP */ snippets: string[]; /** Trend 12‑miesięczny (tablica) */ trends: number[]; /** DUPLIKATY: trend_1..trend_12 (= elementy trends[]) oraz `statistics{}` (= pola top-level) */ trend_1?: number; /* … trend_12 */ statistics?: unknown; }>; pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; limit: number }; } export default KeywordsGetKeywordsResponse ``` ## Błędy Błędy tego endpointu przychodzą we [wspólnej kopercie ze statusem `418`](/types/errors). ## Powiązane akcje - `keywords/getRelated` — frazy powiązane. - `keywords/getQuestions` — frazy pytające. - `keyword_details/getStatistics` — szczegółowe statystyki pojedynczej frazy. --- # Baza słów kluczowych: histogramy zbioru (`getStatistics`) **`POST /api/keywords_analysis/reports/keywords/getStatistics`** Zwraca **rozkłady (histogramy) dla całego zbioru fraz** pasujących do zapytania — w trzech wymiarach: liczby wyszukiwań, trudności frazy i kosztu kliknięcia. Każdy wymiar to lista kubełków `{ start, end, value }`, gdzie `value` to liczba fraz w kubełku. Służy do oceny, jak wygląda cały zbiór, przed pobraniem pojedynczych wierszy przez [`getKeywords`](/modules/keywords_analysis/ka-keywords-getKeywords). --- ## Żądanie `POST` `/api/keywords_analysis/reports/keywords/getStatistics` Nagłówki: `Authorization: Bearer `, `Content-Type: application/json`. Parametry przekazuje się w **treści żądania**. **Żądanie** ```jsonc filename="żądanie.jsonc" { "parameters": [ { "data_fetch_mode": "keyword", "value": [ "buty do biegania" ] } ], "match_mode": "wide", "country_id": 1 } ``` **cURL** ```bash curl --location --request POST 'https://api.senuto.com/api/keywords_analysis/reports/keywords/getStatistics' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' \ --header 'Content-Type: application/json' \ --data '{"parameters": [{"data_fetch_mode": "keyword", "value": ["buty do biegania"]}], "match_mode": "wide", "country_id": 1}' ``` ### Parametry ```ts type KeywordsGetStatisticsRequest = { /** * **Wymagane**. Zapytanie do bazy: lista obiektów `{ data_fetch_mode, value }`. * Tryb `keyword` przyjmuje listę fraz w `value`. */ parameters: Array<{ data_fetch_mode: 'keyword' | string; value: string[] }>; /** **Wymagane**. Szerokość dopasowania frazy: `wide` | `medium` | `narrow`. */ match_mode: 'wide' | 'medium' | 'narrow'; /** **Wymagane**. Identyfikator kraju; nieznana wartość zwraca `418` z `Unknown country_id`. */ country_id: number; /** Filtry — tablica grup łączonych OR. Nieznany `key` zwraca `418` z `invalid_filtering`. */ filtering?: Array<{ filters: Array<{ key: string; match?: 'gt' | 'gte' | 'lt' | 'lte' | 'eq'; value: string | number | Array }>; conjunction?: 'and' | 'or'; }>; } export default KeywordsGetStatisticsRequest ``` > **Ostrzeżenie:** > Pole **`end` ostatniego kubełka to string `"*"`**, nie liczba — oznacza „bez górnej granicy". Parser oczekujący liczby wywróci się na ostatnim elemencie każdego histogramu. > **Ostrzeżenie:** > Wywołanie **zużywa jednostkę** dziennego limitu `keywords_analysis_queries_per_day`. Ta sama para `parameters` + `match_mode` policzona ponownie zużywa kolejną jednostkę. > **Ostrzeżenie:** > **`match_mode` jest wymagane** i przyjmuje wyłącznie `wide`, `medium` albo `narrow` — inna wartość zwraca `418`. ## Odpowiedź Przykład to **rzeczywista odpowiedź produkcyjna**, skrócona. ```json filename="przykładowa-odpowiedź" { "success": true, "data": { "searches": [ { "start": 0, "end": 0, "value": 90 }, { "start": 10, "end": 10, "value": 1120 }, { "start": 20, "end": 20, "value": 305 }, { "start": 3600, "end": "*", "value": 9 } ], "difficulty": [ { "start": 1, "end": 10, "value": 0 }, { "start": 11, "end": 20, "value": 0 } ], "cpc": [ { "start": 0, "end": 0.49, "value": 1376 }, { "start": 0.5, "end": 0.99, "value": 669 } ] } } ``` ### Struktura odpowiedzi ```ts type KeywordsGetStatisticsResponse = { success: boolean; /** Trzy histogramy; każdy to lista kubełków. */ data: { /** Rozkład liczby wyszukiwań. */ searches: Array<{ start: number; end: number | '*'; value: number }>; /** Rozkład trudności frazy (skala 1–100). */ difficulty: Array<{ start: number; end: number | '*'; value: number }>; /** Rozkład kosztu kliknięcia. */ cpc: Array<{ start: number; end: number | '*'; value: number }>; }; } export default KeywordsGetStatisticsResponse ``` ## Błędy Błędy tego endpointu przychodzą we [wspólnej kopercie ze statusem `418`](/types/errors). ## Powiązane akcje - [`getKeywords`](/modules/keywords_analysis/ka-keywords-getKeywords) — pojedyncze frazy z metrykami. - [`getGroups`](/modules/keywords_analysis/ka-keywords-getGroups) — ten sam zbiór pogrupowany tematycznie. - [`getResultsStatistics`](/modules/keywords_analysis/ka-keywords-getResultsStatistics) — sumaryczne statystyki zbioru. --- # Baza słów kluczowych: grupy zbioru (`getGroups`) **`POST /api/keywords_analysis/reports/keywords/getGroups`** Zwraca **zbiór fraz pogrupowany tematycznie** — każdy wiersz to grupa ze wspólnym fragmentem frazy, wraz z liczbą fraz i sumami oraz średnimi wyszukiwań i CPC. Pozwala zobaczyć strukturę dużego zbioru, zanim zejdziesz do pojedynczych fraz. --- ## Żądanie `POST` `/api/keywords_analysis/reports/keywords/getGroups` Nagłówki: `Authorization: Bearer `, `Content-Type: application/json`. Parametry przekazuje się w **treści żądania**. **Żądanie** ```jsonc filename="żądanie.jsonc" { "parameters": [ { "data_fetch_mode": "keyword", "value": [ "buty do biegania" ] } ], "match_mode": "wide", "country_id": 1, "limit": 3 } ``` **cURL** ```bash curl --location --request POST 'https://api.senuto.com/api/keywords_analysis/reports/keywords/getGroups' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' \ --header 'Content-Type: application/json' \ --data '{"parameters": [{"data_fetch_mode": "keyword", "value": ["buty do biegania"]}], "match_mode": "wide", "country_id": 1, "limit": 3}' ``` ### Parametry ```ts type KeywordsGetGroupsRequest = { /** * **Wymagane**. Zapytanie do bazy: lista obiektów `{ data_fetch_mode, value }`. * Tryb `keyword` przyjmuje listę fraz w `value`. */ parameters: Array<{ data_fetch_mode: 'keyword' | string; value: string[] }>; /** **Wymagane**. Szerokość dopasowania frazy: `wide` | `medium` | `narrow`. */ match_mode: 'wide' | 'medium' | 'narrow'; /** **Wymagane**. Identyfikator kraju; nieznana wartość zwraca `418` z `Unknown country_id`. */ country_id: number; /** Filtry — tablica grup łączonych OR. Nieznany `key` zwraca `418` z `invalid_filtering`. */ filtering?: Array<{ filters: Array<{ key: string; match?: 'gt' | 'gte' | 'lt' | 'lte' | 'eq'; value: string | number | Array }>; conjunction?: 'and' | 'or'; }>; /** Numer strony wyników. */ page?: number; /** Liczba wierszy na stronę. */ limit?: number; } export default KeywordsGetGroupsRequest ``` > **Ostrzeżenie:** > Liczba pobieranych wierszy podlega limitowi `keywords_analysis_rows_per_report` — przy zbyt dużym `limit` albo wysokim `page` żądanie zwróci `418` z informacją o przekroczeniu limitu. > **Ostrzeżenie:** > Paginacja jest **rzeczywista**: dla frazy `buty do biegania` w Polsce raport zwrócił `count: 23239` grup. To inny endpoint niż [`keyword_details/getGroups`](/modules/keywords_analysis/ka-keyword-details-getGroups), którego paginacja jest pozorna. ## Odpowiedź Przykład to **rzeczywista odpowiedź produkcyjna**, skrócona. ```json filename="przykładowa-odpowiedź" { "success": true, "data": [ { "group": "do biegania", "keywords_sum": 2902, "searches_sum": 461430, "searches_avg": 159, "cpc_sum": 1536.35, "cpc_avg": 0.53 }, { "group": "buty do", "keywords_sum": 2391, "searches_sum": 413430, "searches_avg": 172.91, "cpc_sum": 1244.23, "cpc_avg": 0.52 } ], "pagination": { "page_count": 7747, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 23239, "limit": 3 } } ``` ### Struktura odpowiedzi ```ts type KeywordsGetGroupsResponse = { success: boolean; data: Array<{ /** Nazwa grupy — wspólny fragment fraz. */ group: string; /** Liczba fraz w grupie. */ keywords_sum: number; /** Suma wyszukiwań fraz w grupie. */ searches_sum: number; /** Średnia liczba wyszukiwań w grupie. */ searches_avg: number; /** Suma CPC fraz w grupie. */ cpc_sum: number; /** Średni CPC w grupie. */ cpc_avg: number; }>; pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; limit: number; }; } export default KeywordsGetGroupsResponse ``` ## Błędy Błędy tego endpointu przychodzą we [wspólnej kopercie ze statusem `418`](/types/errors). ## Powiązane akcje - [`getKeywords`](/modules/keywords_analysis/ka-keywords-getKeywords) — frazy ze zbioru. - [`getGroupKeywords`](/modules/keywords_analysis/ka-keywords-getGroupKeywords) — frazy w obrębie grupy. - [`keyword_details/getGroups`](/modules/keywords_analysis/ka-keyword-details-getGroups) — grupy dla **jednej** frazy. --- # Baza słów kluczowych: frazy w grupie (`getGroupKeywords`) **`POST /api/keywords_analysis/reports/keywords/getGroupKeywords`** Zwraca **pojedyncze frazy wraz z metrykami** dla zbioru zawężonego jak w [`getGroups`](/modules/keywords_analysis/ka-keywords-getGroups). Kształt wiersza jest taki sam jak w [`getKeywords`](/modules/keywords_analysis/ka-keywords-getKeywords), a wyniki można sortować polem `order`. --- ## Żądanie `POST` `/api/keywords_analysis/reports/keywords/getGroupKeywords` Nagłówki: `Authorization: Bearer `, `Content-Type: application/json`. Parametry przekazuje się w **treści żądania**. **Żądanie** ```jsonc filename="żądanie.jsonc" { "parameters": [ { "data_fetch_mode": "keyword", "value": [ "buty do biegania" ] } ], "match_mode": "wide", "country_id": 1, "limit": 3 } ``` **cURL** ```bash curl --location --request POST 'https://api.senuto.com/api/keywords_analysis/reports/keywords/getGroupKeywords' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' \ --header 'Content-Type: application/json' \ --data '{"parameters": [{"data_fetch_mode": "keyword", "value": ["buty do biegania"]}], "match_mode": "wide", "country_id": 1, "limit": 3}' ``` ### Parametry ```ts type KeywordsGetGroupKeywordsRequest = { /** * **Wymagane**. Zapytanie do bazy: lista obiektów `{ data_fetch_mode, value }`. * Tryb `keyword` przyjmuje listę fraz w `value`. */ parameters: Array<{ data_fetch_mode: 'keyword' | string; value: string[] }>; /** **Wymagane**. Szerokość dopasowania frazy: `wide` | `medium` | `narrow`. */ match_mode: 'wide' | 'medium' | 'narrow'; /** **Wymagane**. Identyfikator kraju; nieznana wartość zwraca `418` z `Unknown country_id`. */ country_id: number; /** Filtry — tablica grup łączonych OR. Nieznany `key` zwraca `418` z `invalid_filtering`. */ filtering?: Array<{ filters: Array<{ key: string; match?: 'gt' | 'gte' | 'lt' | 'lte' | 'eq'; value: string | number | Array }>; conjunction?: 'and' | 'or'; }>; /** Sortowanie wyników. */ order?: { prop: string; dir: 'ASC' | 'DESC' }; /** Numer strony wyników. */ page?: number; /** Liczba wierszy na stronę. */ limit?: number; } export default KeywordsGetGroupKeywordsRequest ``` > **Ostrzeżenie:** > Metryki są **zdublowane trzykrotnie**: pola najwyższego poziomu, płaskie `trend_1` … `trend_12` oraz obiekt `statistics`. Wybierz jedno źródło. > **Ostrzeżenie:** > Pole **`snippets` powtarza wartości** — dla frazy `nike airmax` API zwróciło `image_thumbs`, `pla`, `top_bar` trzy razy pod rząd. Odfiltruj duplikaty po swojej stronie. > **Ostrzeżenie:** > Liczba wierszy podlega limitowi `keywords_analysis_rows_per_report`. ## Odpowiedź Przykład to **rzeczywista odpowiedź produkcyjna**, skrócona. ```json filename="przykładowa-odpowiedź" { "success": true, "data": [ { "added": "2021-10-19", "keyword": "nike airmax", "searches": 135000, "cpc": 0.67, "cpc_min": 0.23, "cpc_max": 1.1, "words_count": 2, "kid": "c9db40a54eacd9ab571c31b82d0b1ac5", "variations": [], "variations_number": 0, "snippets": [ "image_thumbs", "pla", "top_bar", "image_thumbs", "pla", "top_bar", "image_thumbs", "pla", "top_bar" ], "trends": [ 90500, 110000, 201000, 165000, 135000, 110000, 110000, 135000, 135000, 110000, 90500, 74000 ] } ], "pagination": { "page_count": 2982, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 8944, "limit": 3 } } ``` ### Struktura odpowiedzi ```ts type KeywordsGetGroupKeywordsResponse = { success: boolean; data: Array<{ keyword: string; /** Data dodania frazy do bazy, `YYYY-MM-DD`. */ added: string; searches: number; cpc: number; cpc_min: number | null; cpc_max: number | null; words_count: number; kid: string; variations: string[]; variations_number: number; /** Cechy SERP. Lista **może zawierać powtórzenia**. */ snippets: string[]; /** 12-elementowy trend wyszukiwań. */ trends: number[]; /** Te same wartości co `trends`, rozbite na pola `trend_1` … `trend_12`. */ trend_1: number; trend_12: number; /** Te same metryki w formie zagnieżdżonej — duplikat pól najwyższego poziomu. */ statistics: Record; }>; pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; limit: number; }; } export default KeywordsGetGroupKeywordsResponse ``` ## Błędy Błędy tego endpointu przychodzą we [wspólnej kopercie ze statusem `418`](/types/errors). ## Powiązane akcje - [`getKeywords`](/modules/keywords_analysis/ka-keywords-getKeywords) — ten sam kształt wiersza dla całego zbioru. - [`getGroups`](/modules/keywords_analysis/ka-keywords-getGroups) — lista grup. --- # Baza słów kluczowych: powiązane (`getRelated`) **`POST /api/keywords_analysis/reports/keywords/getRelated`** Przykładowe żądanie: ```json { "parameters": [ { "data_fetch_mode": "keyword", "value": [ "buty do biegania" ] } ], "match_mode": "wide", "country_id": 1, "limit": 10 } ``` Przykładowa odpowiedź: ```json { "success": true, "data": [ { "id": "68944706", "keyword": "buty do biegania", "searches": 60500, "common_factor": 18, "parent_keyword": "buty do biegania", "cpc": 0.8, "cpc_min": 0.38, "cpc_max": 1.21, "words_count": 3, "params": [ "adwords", "image_thumbs", "map", "news", "pla", "top_bar", "yellow_pages" ] } ], "pagination": { "page_count": 144, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 288, "limit": 2 } } ``` Zwraca frazy **powiązane semantycznie** z zapytaniem — wraz z metrykami (wyszukiwania, CPC, liczba słów, trend), cechami SERP (`params`) oraz `common_factor` (siła powiązania) i `parent_keyword` (fraza źródłowa). Request identyczny jak w [`keywords/getKeywords`](/modules/keywords_analysis/ka-keywords-getKeywords). | Fraza | ID | Wyszukiwania/mies. | Współczynnik wspólny | Fraza źródłowa | CPC | CPC min | CPC max | Liczba słów | Cechy SERP | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | buty do biegania | 68944706 | 60500 | 18 | buty do biegania | 0.8 | 0.38 | 1.21 | 3 | ["adwords","image_thumbs","map","news","pla","top_bar","yellow_pages"] | _parameters keyword „buty do biegania”, match_mode wide, country_id 1. Pominięto zdublowane trend_1..12 (= trend) i statistics{}._ > **Ostrzeżenie:** > Uruchomienie zużywa jednostkę `keywords_analysis_queries_per_day` — patrz [Limity zapytań](/rate-limits). --- ## Żądanie `POST` `/api/keywords_analysis/reports/keywords/getRelated` ```jsonc filename="żądanie.jsonc" { "parameters": [{ "data_fetch_mode": "keyword", "value": ["buty do biegania"] }], "match_mode": "wide", "country_id": 1, "limit": 10, "order": { "prop": "searches", "dir": "desc" }, "filtering": [] } ``` ### Parametry Identyczne jak w [`keywords/getKeywords`](/modules/keywords_analysis/ka-keywords-getKeywords): `parameters[{ data_fetch_mode, value[] }]`, `match_mode` (`wide`/`medium`/`narrow`), `country_id`, `limit`/`page`. Dodatkowo `order` (`{ prop, dir }`) i `filtering`. ## Odpowiedź ```ts type KeywordsGetRelatedResponse = { success: boolean; data: Array<{ id: string; keyword: string; searches: number; /** Siła powiązania z frazą źródłową */ common_factor: number; /** Fraza źródłowa (parent) */ parent_keyword: string; cpc: number; cpc_min: number; cpc_max: number; words_count: number; /** Cechy SERP */ params: string[]; /** DUPLIKATY: trend_1..trend_12 oraz `statistics{}` */ trend_1?: number; statistics?: unknown; }>; pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; limit: number }; } export default KeywordsGetRelatedResponse ``` ## Błędy Błędy tego endpointu przychodzą we [wspólnej kopercie ze statusem `418`](/types/errors). ## Powiązane akcje - `keywords/getKeywords` — główna wyszukiwarka. - `keywords/getQuestions` — frazy pytające. --- # Baza słów kluczowych: pytania (`getQuestions`) **`POST /api/keywords_analysis/reports/keywords/getQuestions`** Przykładowe żądanie: ```json { "parameters": [ { "data_fetch_mode": "keyword", "value": [ "buty do biegania" ] } ], "match_mode": "wide", "country_id": 1, "limit": 10 } ``` Przykładowa odpowiedź: ```json { "success": true, "data": [ { "keyword": "jakie buty na siłownie", "kid": "a831b77e08e37ba1515fd2972b261b73", "added": "2021-06-22", "searches": 1600, "cpc": 0.85, "cpc_min": 0.23, "cpc_max": 1.46, "words_count": 2, "variations": [ "jakie buty na siłownię", "jakie buty na silownie", "buty na siłownię jakie" ], "variations_number": 3, "snippets": [ "question", "adwords", "featured_answer", "people_also_ask", "pla", "top_bar", "video_thumbs" ] } ], "pagination": { "page_count": 293, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 585, "limit": 2 } } ``` Zwraca **frazy pytające** (pytania użytkowników) powiązane z zapytaniem — z tym samym zestawem metryk co [`keywords/getKeywords`](/modules/keywords_analysis/ka-keywords-getKeywords) (wyszukiwania, CPC, trend, wariacje, cechy SERP), ograniczony do fraz o charakterze pytania. Idealne do budowy sekcji FAQ / treści odpowiadających na pytania. | Pytanie | KID | Wyszukiwania/mies. | CPC | CPC min | CPC max | Liczba słów | Wariacje | Cechy SERP | Dodano | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | jakie buty na siłownie | a831b77e08e37ba1515fd2972b261b73 | 1600 | 0.85 | 0.23 | 1.46 | 2 | 3 | ["question","adwords","featured_answer","people_also_ask","pla","top_bar","video_thumbs"] | 2021-06-22 | _parameters keyword „buty do biegania”, match_mode wide, country_id 1. Pominięto zdublowane trend_1..12 i statistics{}._ > **Ostrzeżenie:** > Uruchomienie zużywa jednostkę `keywords_analysis_queries_per_day` — patrz [Limity zapytań](/rate-limits). --- ## Żądanie `POST` `/api/keywords_analysis/reports/keywords/getQuestions` ```jsonc filename="żądanie.jsonc" { "parameters": [{ "data_fetch_mode": "keyword", "value": ["buty do biegania"] }], "match_mode": "wide", "country_id": 1, "limit": 10, "order": { "prop": "searches", "dir": "desc" }, "filtering": [] } ``` ### Parametry Jak w [`keywords/getKeywords`](/modules/keywords_analysis/ka-keywords-getKeywords): `parameters[{ data_fetch_mode, value[] }]`, `match_mode`, `country_id`, `limit`/`page`, plus `order`/`filtering`. ## Odpowiedź ```ts type KeywordsGetQuestionsResponse = { success: boolean; data: Array<{ keyword: string; kid: string; added: string; searches: number; cpc: number; cpc_min: number; cpc_max: number; words_count: number; variations: string[]; variations_number: number; snippets: string[]; trends: number[]; /** DUPLIKATY: trend_1..trend_12 oraz `statistics{}` */ trend_1?: number; statistics?: unknown; }>; pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; limit: number }; } export default KeywordsGetQuestionsResponse ``` ## Błędy Błędy tego endpointu przychodzą we [wspólnej kopercie ze statusem `418`](/types/errors). ## Powiązane akcje - `keywords/getKeywords` — główna wyszukiwarka. - `keywords/getRelated` — frazy powiązane. --- # Baza słów kluczowych: chmura tagów (`getTagsCloud`) **`POST /api/keywords_analysis/reports/keywords/getTagsCloud`** Przykładowe żądanie: ```json { "parameters": [ { "data_fetch_mode": "keyword", "value": [ "buty do biegania" ] } ], "match_mode": "wide", "country_id": 1, "limit": 12 } ``` Przykładowa odpowiedź: ```json { "success": true, "data": { "do biegania": 2506, "buty do": 2080, "buty do biegania": 2066, "biegania damskie": 215, "biegania męskie": 153 } } ``` Zwraca **chmurę tagów**: najczęstsze n-gramy (składowe frazy) występujące w zbiorze słów kluczowych pasujących do zapytania, wraz z liczbą wystąpień. Przydatne do szybkiego zobaczenia dominujących motywów/intencji w temacie. **Najczęstsze n-gramy (liczba wystąpień)** | Wartość | wyst. | | --- | --- | | do biegania | 2506 | | buty do | 2080 | | buty do biegania | 2066 | | biegania damskie | 215 | | do biegania damskie | 213 | | biegania męskie | 153 | | do biegania męskie | 153 | | biegania po | 107 | | do biegania po | 107 | | biegania nike | 90 | | do biegania nike | 90 | | biegania w | 79 | _parameters keyword „buty do biegania”, match_mode wide, country_id 1. `data` to mapa n-gram → liczba; poniżej top wyników._ > **Ostrzeżenie:** > Uruchomienie zużywa jednostkę `keywords_analysis_queries_per_day` — patrz [Limity zapytań](/rate-limits). --- ## Żądanie `POST` `/api/keywords_analysis/reports/keywords/getTagsCloud` ```jsonc filename="żądanie.jsonc" { "parameters": [{ "data_fetch_mode": "keyword", "value": ["buty do biegania"] }], "match_mode": "wide", "country_id": 1, "limit": 12 } ``` ### Parametry Jak w [`keywords/getKeywords`](/modules/keywords_analysis/ka-keywords-getKeywords): `parameters[{ data_fetch_mode, value[] }]`, `match_mode`, `country_id`. Parametr `limit` określa **liczbę zwracanych tagów**. > **Ostrzeżenie:** > W przeciwieństwie do pozostałych raportów Bazy słów kluczowych ten endpoint **nie obsługuje `filtering` ani `order`**. Pole `filtering` jest wprawdzie przyjmowane bez błędu, ale **ignorowane** — nie zawęża wyniku, a nieznany `key` **nie** zwraca `418` (potwierdzone na prod: identyczny wynik z filtrem i bez). Aby operować na przefiltrowanym zbiorze fraz, użyj [`keywords/getKeywords`](/modules/keywords_analysis/ka-keywords-getKeywords) lub [`keywords/getSpeechParts`](/modules/keywords_analysis/ka-keywords-getSpeechParts). ## Odpowiedź `data` to **mapa** `n-gram → liczba wystąpień` (klucze są zmienne — to nie lista wierszy). ```ts type KeywordsGetTagsCloudResponse = { success: boolean; /** Mapa: n-gram (klucz) -> liczba wystąpień */ data: Record; } export default KeywordsGetTagsCloudResponse ``` ## Błędy Błędy tego endpointu przychodzą we [wspólnej kopercie ze statusem `418`](/types/errors). ## Powiązane akcje - `keywords/getKeywords` — główna wyszukiwarka fraz. - `keywords/getSpeechParts` — rozkład części mowy w zbiorze fraz. --- # Baza słów kluczowych: części mowy (`getSpeechParts`) **`POST /api/keywords_analysis/reports/keywords/getSpeechParts`** Przykładowe żądanie: ```json { "parameters": [ { "data_fetch_mode": "keyword", "value": [ "buty do biegania" ] } ], "match_mode": "wide", "country_id": 1, "type": "subjects", "limit": 10 } ``` Przykładowa odpowiedź: ```json { "success": true, "data": [ { "count": 4016, "name": "but" }, { "count": 1444, "name": "Nike" }, { "count": 598, "name": "adidas" }, { "count": 575, "name": "Max" }, { "count": 468, "name": "sklep" } ] } ``` Zwraca rozkład wybranej **części mowy** w zbiorze fraz pasujących do zapytania — dla każdego słowa (rzeczownika/przymiotnika/orzeczenia) liczbę wystąpień. Część mowy wybierasz parametrem `type`. Zbiór fraz możesz zawęzić opcjonalnym `filtering` (jak w [`keywords/getKeywords`](/modules/keywords_analysis/ka-keywords-getKeywords#filtrowanie-i-sortowanie)) — np. tylko frazy o `searches ≥ 100 000`. | Słowo | Liczba wystąpień | | --- | --- | | but | 4016 | | Nike | 1444 | | adidas | 598 | | Max | 575 | | sklep | 468 | _parameters keyword „buty do biegania”, match_mode wide, country_id 1, type „subjects”. Wszystkie pola wiersza._ > **Ostrzeżenie:** > Parametr **`type`** jest wymagany i walidowany — dozwolone: `subjects` (rzeczowniki), `adjectives` (przymiotniki), `predicates` (orzeczenia). Inna wartość → `418` (`invalid_data`). Uruchomienie zużywa jednostkę `keywords_analysis_queries_per_day`. --- ## Żądanie `POST` `/api/keywords_analysis/reports/keywords/getSpeechParts` ```jsonc filename="żądanie.jsonc" { "parameters": [{ "data_fetch_mode": "keyword", "value": ["buty do biegania"] }], "match_mode": "wide", "country_id": 1, "type": "subjects", "limit": 10 } ``` ### Parametry ```ts type KeywordsGetSpeechPartsRequest = { /** **Wymagane**. Grupy zapytania — jak w keywords/getKeywords. */ parameters: Array<{ data_fetch_mode: 'keyword' | 'url' | 'domain' | 'catalog'; value: string[] }>; /** **Wymagane**. Tryb dopasowania: `wide` | `medium` | `narrow`. */ match_mode: 'wide' | 'medium' | 'narrow'; /** **Wymagane**. Część mowy: `subjects` | `adjectives` | `predicates`. */ type: 'subjects' | 'adjectives' | 'predicates'; /** ID kraju (bazy słów). */ country_id?: number; /** * Filtrowanie zbioru fraz *przed* zliczeniem części mowy. Składnia i dozwolone klucze * jak w keywords/getKeywords (nieznany `key` → `418` `invalid_filtering`). Sortowania (`order`) brak. */ filtering?: Array<{ filters: Array<{ key: string; match?: 'gt' | 'gte' | 'lt' | 'lte' | 'eq'; value: string | number | Array; complement?: boolean }>; conjunction?: 'and' | 'or'; }>; /** Liczba wierszy na stronę. @default 10 */ limit?: number; /** Numer strony. @default 1 */ page?: number; } export default KeywordsGetSpeechPartsRequest ``` ## Odpowiedź `data` to lista słów z liczbą wystąpień. ```ts type KeywordsGetSpeechPartsResponse = { success: boolean; data: Array<{ name: string; count: number }>; } export default KeywordsGetSpeechPartsResponse ``` ## Błędy Błędy tego endpointu przychodzą we [wspólnej kopercie ze statusem `418`](/types/errors). ## Powiązane akcje - `keywords/getTagsCloud` — najczęstsze n-gramy w zbiorze fraz. - `keywords/getKeywords` — główna wyszukiwarka fraz. --- # Baza słów kluczowych: trendy (`getTrending`) **`POST /api/keywords_analysis/reports/keywords/getTrending`** Przykładowe żądanie: ```json { "country_id": 1, "date_min": "2026-06-01", "date_max": "2026-06-29", "limit": 20 } ``` Przykładowa odpowiedź: ```json { "success": true, "data": [], "pagination": { "page_count": 0, "current_page": 1, "has_next_page": false, "has_prev_page": false, "count": 0, "limit": 20 } } ``` Zwraca **frazy trendujące** (o rosnącej liczbie wyszukiwań) w wybranym kraju i okresie. W odróżnieniu od pozostałych raportów tego modułu **nie przyjmuje frazy wejściowej** — filtruje po kraju i zakresie dat. > **Ostrzeżenie:** > Zbiór trendujących fraz jest kuratorowany, więc dla części kont i krajów endpoint zwraca `200` z **pustą listą** (`data: []`) — także dla różnych zakresów dat. Kształt wiersza poniżej jest orientacyjny. Uruchomienie zużywa jednostkę `keywords_analysis_queries_per_day`. --- ## Żądanie `POST` `/api/keywords_analysis/reports/keywords/getTrending` ```jsonc filename="żądanie.jsonc" { "country_id": 1, "date_min": "2026-06-01", "date_max": "2026-06-29", "limit": 20, "page": 1, "order": { "prop": "searches", "dir": "desc" }, "filtering": [] } ``` ### Parametry ```ts type KeywordsGetTrendingRequest = { /** ID kraju (bazy słów), np. `1` (PL 1.0). */ country_id?: number; /** Data początkowa okresu (RRRR-MM-DD). */ date_min?: string; /** Data końcowa okresu (RRRR-MM-DD). */ date_max?: string; /** Liczba wierszy na stronę. @default 10 */ limit?: number; /** Numer strony. @default 1 */ page?: number; /** Sortowanie `{ prop, dir }`. */ order?: { prop: string; dir: 'asc' | 'desc' }; /** Grupy filtrów. */ filtering?: unknown[]; } export default KeywordsGetTrendingRequest ``` ## Odpowiedź `data` to lista fraz trendujących (może być pusta), plus `pagination`. Kształt wiersza jest zbliżony do pozostałych raportów bazy: fraza plus metryki wyszukiwań i trendu. ```ts type KeywordsGetTrendingResponse = { success: boolean; /** Lista fraz trendujących; może być pusta */ data: unknown[]; pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; limit: number }; } export default KeywordsGetTrendingResponse ``` ## Błędy Błędy tego endpointu przychodzą we [wspólnej kopercie ze statusem `418`](/types/errors). ## Powiązane akcje - `keywords/getKeywords` — główna wyszukiwarka fraz. --- # Baza słów kluczowych: domeny w wynikach (`getDomainsList`) **`POST /api/keywords_analysis/reports/keywords/getDomainsList`** Przykładowe żądanie: ```json { "parameters": [ { "data_fetch_mode": "keyword", "value": [ "buty do biegania" ] } ], "match_mode": "wide", "country_id": 1, "limit": 10 } ``` Przykładowa odpowiedź: ```json { "success": true, "data": [ { "count": 6604, "name": "allegro.pl" }, { "count": 5665, "name": "ceneo.pl" }, { "count": 3767, "name": "zalando.pl" } ], "pagination": { "page_count": 2386, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 7157, "limit": 3 } } ``` Zwraca **domeny rankujące** w zbiorze fraz pasujących do zapytania, wraz z liczbą fraz (`count`), na które dana domena występuje. Pozwala szybko zobaczyć, którzy gracze dominują w danym temacie. Request identyczny jak w [`keywords/getKeywords`](/modules/keywords_analysis/ka-keywords-getKeywords). | Domena | Liczba fraz | | --- | --- | | allegro.pl | 6604 | | ceneo.pl | 5665 | | zalando.pl | 3767 | _parameters keyword „buty do biegania”, match_mode wide, country_id 1. Wszystkie pola wiersza._ > **Ostrzeżenie:** > Uruchomienie zużywa jednostkę `keywords_analysis_queries_per_day` — patrz [Limity zapytań](/rate-limits). --- ## Żądanie `POST` `/api/keywords_analysis/reports/keywords/getDomainsList` ```jsonc filename="żądanie.jsonc" { "parameters": [{ "data_fetch_mode": "keyword", "value": ["buty do biegania"] }], "match_mode": "wide", "country_id": 1, "limit": 10 } ``` ### Parametry Jak w [`keywords/getKeywords`](/modules/keywords_analysis/ka-keywords-getKeywords): `parameters[{ data_fetch_mode, value[] }]`, `match_mode`, `country_id`, `limit`/`page`. Obsługuje też opcjonalne **`filtering`** — zawęża zbiór fraz _przed_ zliczeniem domen (składnia i dozwolone klucze jak w [sekcji „Filtrowanie i sortowanie" w `getKeywords`](/modules/keywords_analysis/ka-keywords-getKeywords#filtrowanie-i-sortowanie); nieznany `key` → `418` `invalid_filtering`). **Nie obsługuje `order`** — kolejność jest stała (malejąco po liczbie fraz). ```jsonc filename="z filtrem (frazy ≥ 100 000 wyszukiwań/mies.)" { "parameters": [{ "data_fetch_mode": "keyword", "value": ["buty do biegania"] }], "match_mode": "wide", "country_id": 1, "filtering": [{ "filters": [{ "key": "searches", "match": "gte", "value": 100000 }], "conjunction": "and" }] } ``` ## Odpowiedź ```ts type KeywordsGetDomainsListResponse = { success: boolean; data: Array<{ name: string; count: number }>; pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; limit: number }; } export default KeywordsGetDomainsListResponse ``` ## Błędy Błędy tego endpointu przychodzą we [wspólnej kopercie ze statusem `418`](/types/errors). ## Powiązane akcje - `keywords/getKeywords` — frazy w zbiorze. - `keywords/getResultsStatistics` — statystyki zbiorcze zbioru fraz. --- # Baza słów kluczowych: statystyki zbioru (`getResultsStatistics`) **`POST /api/keywords_analysis/reports/keywords/getResultsStatistics`** Przykładowe żądanie: ```json { "parameters": [ { "data_fetch_mode": "keyword", "value": [ "buty do biegania" ] } ], "match_mode": "wide", "country_id": 1 } ``` Przykładowa odpowiedź: ```json { "success": true, "data": { "searches": { "min": -1, "max": 135000, "sum": 3567428, "avg": 283.71 }, "cpc": { "min": 0, "max": 27.15, "sum": 4448.15, "avg": 0.35 }, "keywords": { "count": 12282 }, "trends": { "min": { "month": 12 }, "max": { "month": 3 } } } } ``` Zwraca **zagregowane statystyki** dla całego zbioru fraz pasujących do zapytania (bez listy fraz): zakres i sumę/średnią liczby wyszukiwań oraz CPC, całkowitą liczbę fraz i miesiące skrajne trendu. Przydatne jako podsumowanie nad wynikami wyszukiwarki. > **Ostrzeżenie:** > Uruchomienie zużywa jednostkę `keywords_analysis_queries_per_day` — patrz [Limity zapytań](/rate-limits). --- ## Żądanie `POST` `/api/keywords_analysis/reports/keywords/getResultsStatistics` ```jsonc filename="żądanie.jsonc" { "parameters": [{ "data_fetch_mode": "keyword", "value": ["buty do biegania"] }], "match_mode": "wide", "country_id": 1, "filtering": [] } ``` ### Parametry Jak w [`keywords/getKeywords`](/modules/keywords_analysis/ka-keywords-getKeywords): `parameters[{ data_fetch_mode, value[] }]`, `match_mode`, `country_id`, plus `filtering`. Endpoint zwraca podsumowanie, więc nie paginuje wyników. ## Odpowiedź `data` to obiekt statystyk zbioru. ```ts type KeywordsGetResultsStatisticsResponse = { success: boolean; data: { /** Statystyki liczby wyszukiwań w zbiorze */ searches: { min: number; max: number; sum: number; avg: number }; /** Statystyki CPC w zbiorze */ cpc: { min: number; max: number; sum: number; avg: number }; /** Łączna liczba fraz w zbiorze */ keywords: { count: number }; /** Miesiące skrajne trendu (1–12) */ trends: { min: { month: number }; max: { month: number } }; }; } export default KeywordsGetResultsStatisticsResponse ``` ## Błędy Błędy tego endpointu przychodzą we [wspólnej kopercie ze statusem `418`](/types/errors). ## Powiązane akcje - `keywords/getKeywords` — lista fraz w zbiorze. - `keywords/getDomainsList` — domeny rankujące w zbiorze. --- # Baza słów kluczowych: narzędzia · sugestie fraz (`suggest`) **`POST /api/keywords_analysis/tools/keywords_suggester/suggest`** Przykładowe żądanie: ```json { "country_id": 1, "keywords": [ "buty do biegania" ], "mode": "lr", "iterations": 2, "limit": 10 } ``` Przykładowa odpowiedź: ```json { "success": true, "data": [ { "parent_keyword": "buty do biegania", "suggested_keyword": "buty do biegania salomon", "first_letter": "b", "keywords_number": 3 } ], "pagination": { "page_count": 260, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 2597, "limit": 10 } } ``` Generuje **sugestie fraz** przez rozwijanie słów wejściowych (`keywords`) — technika podpowiedzi/autouzupełniania (dopisywanie słów przed/po frazie). Dla każdej sugestii zwraca frazę źródłową (`parent_keyword`), zaproponowaną frazę (`suggested_keyword`), pierwszą literę i liczbę słów. | Sugerowana fraza | Fraza źródłowa | Pierwsza litera | Liczba słów | | --- | --- | --- | --- | | buty do biegania salomon | buty do biegania | b | 3 | | buty do biegania skechers | buty do biegania | b | 3 | | buty do biegania saucony | buty do biegania | b | 3 | | buty do biegania salomon damskie | buty do biegania | b | 4 | _country_id 1, keywords „buty do biegania”, mode „lr”, iterations 2. Wszystkie pola wiersza._ > **Ostrzeżenie:** > To **narzędzie** (`tools/*`) — zużywa jednostkę dziennego limitu narzędzi Bazy słów (`keywords_analysis_suggester` / `tools_daily_limit`). Patrz [Limity zapytań](/rate-limits). --- ## Żądanie `POST` `/api/keywords_analysis/tools/keywords_suggester/suggest` ```jsonc filename="żądanie.jsonc" { "country_id": 1, "keywords": ["buty do biegania"], "mode": "lr", "iterations": 2, "limit": 10 } ``` ### Parametry ```ts type KeywordsSuggesterRequest = { /** **Wymagane**. ID kraju (bazy słów). */ country_id: number; /** **Wymagane**. Frazy wejściowe do rozwinięcia. */ keywords: string[]; /** Tryb rozwijania (kierunek dopisywania słów). @default 'lr' */ mode?: string; /** Głębokość rozwinięcia (liczba iteracji). @default 2 */ iterations?: number; /** Liczba wierszy na stronę. @default 10 */ limit?: number; /** Numer strony. @default 1 */ page?: number; /** Sortowanie `{ prop, dir }`. */ order?: { prop: string; dir: 'asc' | 'desc' }; } export default KeywordsSuggesterRequest ``` ## Odpowiedź ```ts type KeywordsSuggesterResponse = { success: boolean; data: Array<{ /** Fraza źródłowa, z której powstała sugestia */ parent_keyword: string; /** Zaproponowana fraza */ suggested_keyword: string; first_letter: string; /** Liczba słów w sugestii */ keywords_number: number; }>; pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; limit: number }; } export default KeywordsSuggesterResponse ``` ## Błędy Błędy tego endpointu przychodzą we [wspólnej kopercie ze statusem `418`](/types/errors). ## Powiązane akcje - `keywords/getKeywords` — pełna wyszukiwarka bazy z metrykami. - `keywords/getRelated` — frazy powiązane semantycznie. --- # Baza słów kluczowych: statystyki frazy (`getStatistics`) **`GET /api/keywords_analysis/reports/keyword_details/getStatistics`** Zwraca zbiorcze statystyki dla pojedynczej frazy kluczowej w wybranym kraju: liczbę wyszukiwań (`searches`), koszt kliknięcia (`cpc`), szacowaną wartość frazy (`rank_value`), listę cech SERP (`params`) oraz 12-miesięczny trend wyszukiwań (`trends`). To akcja zwracająca pojedynczy obiekt — bez paginacji. --- ## Żądanie `GET` `/api/keywords_analysis/reports/keyword_details/getStatistics` Nagłówki: `Authorization: Bearer `. Parametry przekazuje się w **query stringu** — nie w treści żądania. ### Struktura żądania **Podstawowy** ```jsonc filename="żądanie-podstawowe.jsonc" { "keyword": "hamak", "country_id": 1 } ``` **Rozszerzony** ```jsonc filename="żądanie-rozszerzone.jsonc" { "keyword": "hamak", "country_id": 1, "page": 1, "limit": 10 } ``` **cURL** ```bash curl --location --request GET 'https://api.senuto.com/api/keywords_analysis/reports/keyword_details/getStatistics?keyword=hamak&country_id=1' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' ``` ### Parametry ```ts type KeywordDetailsGetStatisticsRequest = { /** * **Wymagane**. Bazowa fraza kluczowa, dla której pobierane są statystyki (`searches`, `cpc`, `trends`, `rank_value`, `params`). Walidator: requirePresence + notEmptyString. */ keyword: string; /** * **Wymagane**. Identyfikator kraju. Liczba całkowita większa od 0, musi istnieć w tabeli `Countries` — nieznana wartość zwraca `418` z komunikatem `Unknown country_id`. Uwaga: `country_id=200` jest mapowane na `1` w kontrolerze (alias legacy). * @default 1 */ country_id: number; /** * Z `PaginationRules`. Opcjonalny i walidowany, lecz bez efektu dla tej akcji — `getStatistics` nie zwraca paginacji ani nie stosuje stronicowania. */ page?: number; /** * Z `PaginationRules`. Opcjonalny i bez efektu dla tej akcji (brak paginacji w odpowiedzi). */ limit?: number; } export default KeywordDetailsGetStatisticsRequest ``` > **Ostrzeżenie:** > Parametry muszą trafić do **query stringu**. Przekazanie ich w treści żądania (body) skutkuje `418`, ponieważ kontroler czyta wyłącznie `getQuery()` i waliduje query, ignorując body. > **Ostrzeżenie:** > To jest metoda **`GET`** — parametry przekazuje się w **query stringu**, nie w treści żądania. Wysłanie tego samego JSON-a w body (np. metodą `POST`) skutkuje `418` z `invalid_data`, ponieważ body jest ignorowane, a walidator widzi brak wymaganych pól. Zarówno **`keyword`**, jak i **`country_id`** są **wymagane**; nieznane `country_id` zwraca `418` z komunikatem `Unknown country_id`. Parametry `page` i `limit` są walidowane, lecz nie mają wpływu na tę akcję (brak paginacji w odpowiedzi). ## Odpowiedź Po pomyślnym żądaniu otrzymujesz kopertę `{ success, data }`, gdzie `data` to pojedynczy obiekt ze statystykami frazy. Brak pola `pagination` — to akcja pojedynczego obiektu. **Skrócona** ```json filename="przykładowa-odpowiedź (skrócona)" { "success": true, "data": { "searches": 22200, "cpc": 0.71, "rank_value": 5588.31 } } ``` **Pełna** ```json filename="przykładowa-odpowiedź (200)" { "success": true, "data": { "searches": 22200, "cpc": 0.71, "rank_value": 5588.31, "params": [ "image_thumbs", "map", "pla", "top_bar", "video_thumbs" ], "trends": [ 40500, 49500, 40500 ] } } ``` ### Struktura odpowiedzi ```ts type KeywordDetailsStatisticsResponse = { /** `true` przy powodzeniu; przy błędzie `false` i koperta z `error` */ success: boolean; /** Statystyki frazy (pojedynczy obiekt) */ data: { /** Średnia miesięczna liczba wyszukiwań frazy */ searches: number; /** Koszt kliknięcia (cost per click) */ cpc: number; /** Szacowana wartość frazy */ rank_value: number; /** Lista cech SERP, np. image_thumbs, map, pla, top_bar, video_thumbs */ params: string[]; /** 12-elementowa tablica miesięcznego trendu wyszukiwań */ trends: number[]; }; } export default KeywordDetailsStatisticsResponse ``` ## Błędy ```ts type ErrorResponse = { success: false; data: { error: { /** np. invalid_data, unknown */ type: string; message: string; /** pole -> reguła -> komunikat (dla invalid_data) */ params?: Record>; }; }; } export default ErrorResponse ``` > **Błąd:** > **`418`** jest zwracane przy błędach walidacji (`invalid_data`). Brak `keyword` → > `{"success":false,"data":{"error":{"type":"invalid_data","params":{"keyword":{"_required":"This field is required"}}}}}`. > Nieznane `country_id` → `418` z komunikatem `Unknown country_id`. Wysłanie parametrów w body zamiast w query stringu również zwraca `418` (`_required` dla `keyword` i `country_id`). ## Powiązane akcje - `getStatistics` — zbiorcze statystyki frazy (ta strona) - Pozostałe akcje raportu `keyword_details` przyjmują tę samą parę identyfikującą frazę: `keyword` + `country_id`. --- # Szczegóły frazy: liczba konkurentów (`getCompetitorsNumber`) **`GET /api/keywords_analysis/reports/keyword_details/getCompetitorsNumber`** Zwraca **liczbę domen konkurujących** o podaną frazę. Pojedyncza liczba — najtańszy sposób oceny, jak zatłoczona jest fraza, przed pobraniem pełnych raportów. Raport odpytujesz **wprost frazą i krajem** — bez wcześniejszego tworzenia zadania w Analizie SERP. Jedno żądanie zastępuje ścieżkę „utwórz zadanie → sprawdź status → pobierz wynik" i nie zużywa limitu zadań. --- ## Żądanie `GET` `/api/keywords_analysis/reports/keyword_details/getCompetitorsNumber` Nagłówki: `Authorization: Bearer `. Parametry przekazuje się w **query stringu**. **Żądanie** ```jsonc filename="żądanie.jsonc" { "keyword": "hamak", "country_id": 1 } ``` **cURL** ```bash curl --location --request GET 'https://api.senuto.com/api/keywords_analysis/reports/keyword_details/getCompetitorsNumber?keyword=hamak&country_id=1' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' ``` ### Parametry ```ts type KeywordDetailsGetCompetitorsNumberRequest = { /** **Wymagane**. Fraza kluczowa, dla której pobierany jest raport. Pusty string zwraca `418`. */ keyword: string; /** * **Wymagane**. Identyfikator kraju; musi istnieć w słowniku krajów — nieznana wartość zwraca * `418` z komunikatem `Unknown country_id`. Uwaga: `200` jest tu mapowane na `1`. * @default 1 */ country_id: number; /** Walidowane, ale bez efektu — ta akcja zwraca pojedynczy obiekt bez paginacji. */ page?: number; /** Walidowane, ale bez efektu — brak paginacji w odpowiedzi. */ limit?: number; } export default KeywordDetailsGetCompetitorsNumberRequest ``` > **Ostrzeżenie:** > To metoda **`GET`** — parametry przekazuje się w **query stringu**. Wysłanie ich w treści żądania kończy się `418`, bo endpoint czyta wyłącznie query. > **Ostrzeżenie:** > Zarówno **`keyword`**, jak i **`country_id`** są **wymagane**. Nieznane `country_id` zwraca `418` z `Unknown country_id`, a wartość **`200` jest mapowana na `1`** — dla Polski trafisz więc do bazy 1.0, nie 2.0. ## Odpowiedź Przykład poniżej to **rzeczywista odpowiedź produkcyjna** dla frazy `hamak` (`country_id: 1`), skrócona do jednego wiersza. ```json filename="przykładowa-odpowiedź" { "success": true, "data": { "competitors_number": 6290 } } ``` ### Struktura odpowiedzi ```ts type KeywordDetailsGetCompetitorsNumberResponse = { success: boolean; data: { /** Liczba domen konkurujących o frazę. */ competitors_number: number; }; } export default KeywordDetailsGetCompetitorsNumberResponse ``` ## Błędy Błędy tego endpointu przychodzą we [wspólnej kopercie ze statusem `418`](/types/errors). ## Powiązane akcje Wszystkie poniższe raporty przyjmują tę samą parę `keyword` + `country_id`: - [`getStatistics`](/modules/keywords_analysis/ka-keyword-details-getStatistics) — zbiorcze statystyki frazy. - [`getQuestions`](/modules/keywords_analysis/ka-keyword-details-getQuestions) — pytania o frazę. - [`getKeywordsPropositions`](/modules/keywords_analysis/ka-keyword-details-getKeywordsPropositions) — propozycje fraz. - [`getRelatedKeywords`](/modules/keywords_analysis/ka-keyword-details-getRelatedKeywords) — frazy powiązane. - [`getTopicLeaders`](/modules/keywords_analysis/ka-keyword-details-getTopicLeaders) — liderzy tematu. - [`getGroups`](/modules/keywords_analysis/ka-keyword-details-getGroups) — grupy frazy. --- # Szczegóły frazy: frazy powiązane (`getRelatedKeywords`) **`POST /api/keywords_analysis/reports/keyword_details/getRelatedKeywords`** Zwraca **frazy powiązane** z podaną — takie, które dzielą z nią adresy URL w TOP wyników. Siłę powiązania opisuje `common_factor`: liczba wspólnych URL-i. Raport odpytujesz **wprost frazą i krajem** — bez wcześniejszego tworzenia zadania w Analizie SERP. Jedno żądanie zastępuje ścieżkę „utwórz zadanie → sprawdź status → pobierz wynik" i nie zużywa limitu zadań. Dla frazy `hamak` w Polsce raport zwrócił **461** wierszy. --- ## Żądanie `POST` `/api/keywords_analysis/reports/keyword_details/getRelatedKeywords` Nagłówki: `Authorization: Bearer `, `Content-Type: application/json`. Parametry przekazuje się w **treści żądania**. **Żądanie** ```jsonc filename="żądanie.jsonc" { "keyword": "hamak", "country_id": 1, "limit": 10 } ``` **cURL** ```bash curl --location --request POST 'https://api.senuto.com/api/keywords_analysis/reports/keyword_details/getRelatedKeywords' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' \ --header 'Content-Type: application/json' \ --data '{"keyword":"hamak","country_id":1,"limit":10}' ``` ### Parametry ```ts type KeywordDetailsGetRelatedKeywordsRequest = { /** **Wymagane**. Fraza kluczowa, dla której pobierany jest raport. Pusty string zwraca `418`. */ keyword: string; /** * **Wymagane**. Identyfikator kraju; musi istnieć w słowniku krajów — nieznana wartość zwraca * `418` z komunikatem `Unknown country_id`. Uwaga: `200` jest tu mapowane na `1`. * @default 1 */ country_id: number; /** * **Ignorowane przez tę akcję.** Sprawdzone na produkcji: poprawny filtr nie zmienia liczby * wyników, a nieznany `key` zwraca `200` zamiast `418`. Filtruj po stronie klienta. */ filtering?: Array>; /** Numer strony wyników. */ page?: number; /** Liczba wierszy na stronę. */ limit?: number; } export default KeywordDetailsGetRelatedKeywordsRequest ``` > **Ostrzeżenie:** > Zarówno **`keyword`**, jak i **`country_id`** są **wymagane**. Nieznane `country_id` zwraca `418` z `Unknown country_id`, a wartość **`200` jest mapowana na `1`** — dla Polski trafisz więc do bazy 1.0, nie 2.0. > **Ostrzeżenie:** > Parametr **`filtering` nie działa na tej akcji.** Sprawdzone na produkcji: poprawny filtr nie zmienia liczby wyników, a nieznany klucz zwraca `200` zamiast `418`. Zawężaj wyniki po swojej stronie. > **Ostrzeżenie:** > Ta akcja **nie zwraca** tablicy `trends` — trend jest tylko w polach `trend_1` … `trend_12` oraz w `statistics.trends.history`. Cechy SERP nazywają się tu `params`, a nie `snippets` jak w pozostałych raportach rodziny. ## Odpowiedź Przykład poniżej to **rzeczywista odpowiedź produkcyjna** dla frazy `hamak` (`country_id: 1`), skrócona do jednego wiersza. ```json filename="przykładowa-odpowiedź" { "success": true, "data": [ { "id": "52043408", "keyword": "hamak", "searches": 22200, "common_factor": 18, "parent_keyword": "hamak", "cpc": 0.91, "cpc_min": 0.23, "cpc_max": 1.6, "words_count": 1, "trend_1": 40500, "trend_2": 27100, "trend_3": 12100, "trend_4": 6600, "trend_5": 8100, "trend_6": 8100, "trend_7": 9900, "trend_8": 9900, "trend_9": 22200, "trend_10": 27100, "trend_11": 49500, "trend_12": 40500, "params": [ "image_thumbs", "map", "pla", "top_bar", "video_thumbs" ], "statistics": { "snippets": { "current": [ "image_thumbs", "map", "pla", "top_bar", "video_thumbs" ] }, "searches": { "current": 22200 }, "cpc": { "current": 0.91 }, "cpc_min": { "current": 0.23 }, "cpc_max": { "current": 1.6 }, "trends": { "history": [ 40500, 27100, 12100, 6600, 8100, 8100, 9900, 9900, 22200, 27100, 49500, 40500 ] } } } ], "pagination": { "page_count": 231, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 461, "limit": 2 } } ``` ### Struktura odpowiedzi ```ts type KeywordDetailsGetRelatedKeywordsResponse = { success: boolean; data: Array<{ /** Identyfikator frazy (liczba w postaci stringa). */ id: string; keyword: string; /** Fraza źródłowa, dla której szukamy powiązań. */ parent_keyword: string; searches: number; /** Liczba wspólnych adresów URL w TOP z frazą źródłową — siła powiązania. */ common_factor: number; cpc: number; cpc_min: number | null; cpc_max: number | null; words_count: number; /** Cechy SERP frazy (tu pod nazwą `params`, nie `snippets`). */ params: string[]; /** Miesięczne wartości trendu, pola `trend_1` … `trend_12`. Duplikują tablicę `trends`. */ trend_1: number; trend_2: number; trend_3: number; trend_4: number; trend_5: number; trend_6: number; trend_7: number; trend_8: number; trend_9: number; trend_10: number; trend_11: number; trend_12: number; /** * Te same metryki co pola najwyższego poziomu, w formie zagnieżdżonej. Nic nowego nie wnosi * poza jednym: `statistics.snippets.current` jest listą **bez duplikatów**, podczas gdy * `params` może powtarzać te same wartości. */ statistics: { snippets: { current: string[] }; searches: { current: number }; cpc: { current: number }; cpc_min: { current: number } | []; cpc_max: { current: number } | []; trends: { history: number[] }; }; }>; /** Paginacja liczona z całego zbioru — `count` to liczba wszystkich wierszy. */ pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; limit: number; }; } export default KeywordDetailsGetRelatedKeywordsResponse ``` ## Błędy Błędy tego endpointu przychodzą we [wspólnej kopercie ze statusem `418`](/types/errors). ## Powiązane akcje Wszystkie poniższe raporty przyjmują tę samą parę `keyword` + `country_id`: - [`getStatistics`](/modules/keywords_analysis/ka-keyword-details-getStatistics) — zbiorcze statystyki frazy. - [`getQuestions`](/modules/keywords_analysis/ka-keyword-details-getQuestions) — pytania o frazę. - [`getKeywordsPropositions`](/modules/keywords_analysis/ka-keyword-details-getKeywordsPropositions) — propozycje fraz. - [`getTopicLeaders`](/modules/keywords_analysis/ka-keyword-details-getTopicLeaders) — liderzy tematu. - [`getGroups`](/modules/keywords_analysis/ka-keyword-details-getGroups) — grupy frazy. - [`getCompetitorsNumber`](/modules/keywords_analysis/ka-keyword-details-getCompetitorsNumber) — liczba konkurentów. --- # Szczegóły frazy: propozycje fraz (`getKeywordsPropositions`) **`POST /api/keywords_analysis/reports/keyword_details/getKeywordsPropositions`** Zwraca **propozycje fraz pokrewnych** do podanej — rozszerzenia i warianty do rozważenia przy budowie listy słów kluczowych. Kształt wiersza jest identyczny jak w pytaniach. Raport odpytujesz **wprost frazą i krajem** — bez wcześniejszego tworzenia zadania w Analizie SERP. Jedno żądanie zastępuje ścieżkę „utwórz zadanie → sprawdź status → pobierz wynik" i nie zużywa limitu zadań. Dla frazy `hamak` w Polsce raport zwrócił **5848** wierszy. --- ## Żądanie `POST` `/api/keywords_analysis/reports/keyword_details/getKeywordsPropositions` Nagłówki: `Authorization: Bearer `, `Content-Type: application/json`. Parametry przekazuje się w **treści żądania**. **Żądanie** ```jsonc filename="żądanie.jsonc" { "keyword": "hamak", "country_id": 1, "limit": 10 } ``` **cURL** ```bash curl --location --request POST 'https://api.senuto.com/api/keywords_analysis/reports/keyword_details/getKeywordsPropositions' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' \ --header 'Content-Type: application/json' \ --data '{"keyword":"hamak","country_id":1,"limit":10}' ``` ### Parametry ```ts type KeywordDetailsGetKeywordsPropositionsRequest = { /** **Wymagane**. Fraza kluczowa, dla której pobierany jest raport. Pusty string zwraca `418`. */ keyword: string; /** * **Wymagane**. Identyfikator kraju; musi istnieć w słowniku krajów — nieznana wartość zwraca * `418` z komunikatem `Unknown country_id`. Uwaga: `200` jest tu mapowane na `1`. * @default 1 */ country_id: number; /** * Filtry raportu — tablica grup łączonych operatorem OR. **Działa na tej akcji.** * Nieznany `key` zwraca `418` z `invalid_filtering`. Opis mechanizmu: /types/filter */ filtering?: Array<{ filters: Array<{ key: string; match?: 'gt' | 'gte' | 'lt' | 'lte' | 'eq'; value: string | number | Array }>; conjunction?: 'and' | 'or'; }>; /** Numer strony wyników. */ page?: number; /** Liczba wierszy na stronę. */ limit?: number; } export default KeywordDetailsGetKeywordsPropositionsRequest ``` > **Ostrzeżenie:** > Zarówno **`keyword`**, jak i **`country_id`** są **wymagane**. Nieznane `country_id` zwraca `418` z `Unknown country_id`, a wartość **`200` jest mapowana na `1`** — dla Polski trafisz więc do bazy 1.0, nie 2.0. > **Ostrzeżenie:** > Metryki są **zdublowane w trzech miejscach**: pola najwyższego poziomu (`searches`, `cpc`, `trends`), płaskie `trend_1` … `trend_12` oraz obiekt `statistics`. Do integracji wybierz jedno źródło — `statistics` różni się tylko tym, że jego lista cech SERP jest bez duplikatów. ## Odpowiedź Przykład poniżej to **rzeczywista odpowiedź produkcyjna** dla frazy `hamak` (`country_id: 1`), skrócona do jednego wiersza. ```json filename="przykładowa-odpowiedź" { "success": true, "data": [ { "searches": 74000, "cpc": 0.79, "cpc_min": 0.28, "cpc_max": 1.31, "words_count": 2, "trend_1": 135000, "trend_2": 90500, "trend_3": 60500, "trend_4": 22200, "trend_5": 12100, "trend_6": 9900, "trend_7": 8100, "trend_8": 12100, "trend_9": 27100, "trend_10": 110000, "trend_11": 135000, "trend_12": 201000, "added": "2021-06-22", "keyword": "huśtawka ogrodowa", "kid": "9c40f1c0405fe5ea7f00bdf8ec639a2f", "variations": [ "hustawka ogrodowa", "ogrodowa huśtawka" ], "variations_number": 2, "snippets": [ "adwords", "image_thumbs", "map", "pla", "top_bar", "yellow_pages" ], "trends": [ 135000, 90500, 60500, 22200, 12100, 9900, 8100, 12100, 27100, 110000, 135000, 201000 ], "statistics": { "snippets": { "current": [ "adwords", "image_thumbs", "map", "pla", "top_bar", "yellow_pages" ] }, "searches": { "current": 74000 }, "cpc": { "current": 0.79 }, "cpc_min": { "current": 0.28 }, "cpc_max": { "current": 1.31 }, "trends": { "history": [ 135000, 90500, 60500, 22200, 12100, 9900, 8100, 12100, 27100, 110000, 135000, 201000 ] } } } ], "pagination": { "page_count": 2924, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 5848, "limit": 2 } } ``` ### Struktura odpowiedzi ```ts type KeywordDetailsGetKeywordsPropositionsResponse = { success: boolean; data: Array<{ keyword: string; /** Data dodania frazy do bazy, format `YYYY-MM-DD`. */ added: string; /** Średnia miesięczna liczba wyszukiwań. */ searches: number; cpc: number; cpc_min: number | null; cpc_max: number | null; words_count: number; /** Identyfikator frazy w bazie. */ kid: string; /** Warianty zapisu tej samej frazy. */ variations: string[]; variations_number: number; /** Cechy SERP frazy. Lista może zawierać **powtórzenia** — odsiane w `statistics.snippets.current`. */ snippets: string[]; /** 12-elementowy trend wyszukiwań. */ trends: number[]; /** Miesięczne wartości trendu, pola `trend_1` … `trend_12`. Duplikują tablicę `trends`. */ trend_1: number; trend_2: number; trend_3: number; trend_4: number; trend_5: number; trend_6: number; trend_7: number; trend_8: number; trend_9: number; trend_10: number; trend_11: number; trend_12: number; /** * Te same metryki co pola najwyższego poziomu, w formie zagnieżdżonej. Nic nowego nie wnosi * poza jednym: `statistics.snippets.current` jest listą **bez duplikatów**, podczas gdy * `snippets` może powtarzać te same wartości. */ statistics: { snippets: { current: string[] }; searches: { current: number }; cpc: { current: number }; cpc_min: { current: number } | []; cpc_max: { current: number } | []; trends: { history: number[] }; }; }>; /** Paginacja liczona z całego zbioru — `count` to liczba wszystkich wierszy. */ pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; limit: number; }; } export default KeywordDetailsGetKeywordsPropositionsResponse ``` ## Błędy Błędy tego endpointu przychodzą we [wspólnej kopercie ze statusem `418`](/types/errors). ## Powiązane akcje Wszystkie poniższe raporty przyjmują tę samą parę `keyword` + `country_id`: - [`getStatistics`](/modules/keywords_analysis/ka-keyword-details-getStatistics) — zbiorcze statystyki frazy. - [`getQuestions`](/modules/keywords_analysis/ka-keyword-details-getQuestions) — pytania o frazę. - [`getRelatedKeywords`](/modules/keywords_analysis/ka-keyword-details-getRelatedKeywords) — frazy powiązane. - [`getTopicLeaders`](/modules/keywords_analysis/ka-keyword-details-getTopicLeaders) — liderzy tematu. - [`getGroups`](/modules/keywords_analysis/ka-keyword-details-getGroups) — grupy frazy. - [`getCompetitorsNumber`](/modules/keywords_analysis/ka-keyword-details-getCompetitorsNumber) — liczba konkurentów. --- # Szczegóły frazy: pytania o frazę (`getQuestions`) **`POST /api/keywords_analysis/reports/keyword_details/getQuestions`** Zwraca **pytania zawierające podaną frazę** — długie ogony w formie pytającej, przydatne przy planowaniu FAQ i sekcji „ludzie pytają też”. Każdy wiersz to osobna fraza pytająca z pełnym zestawem metryk. Raport odpytujesz **wprost frazą i krajem** — bez wcześniejszego tworzenia zadania w Analizie SERP. Jedno żądanie zastępuje ścieżkę „utwórz zadanie → sprawdź status → pobierz wynik" i nie zużywa limitu zadań. Dla frazy `hamak` w Polsce raport zwrócił **405** wierszy. --- ## Żądanie `POST` `/api/keywords_analysis/reports/keyword_details/getQuestions` Nagłówki: `Authorization: Bearer `, `Content-Type: application/json`. Parametry przekazuje się w **treści żądania**. **Żądanie** ```jsonc filename="żądanie.jsonc" { "keyword": "hamak", "country_id": 1, "limit": 10 } ``` **cURL** ```bash curl --location --request POST 'https://api.senuto.com/api/keywords_analysis/reports/keyword_details/getQuestions' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' \ --header 'Content-Type: application/json' \ --data '{"keyword":"hamak","country_id":1,"limit":10}' ``` ### Parametry ```ts type KeywordDetailsGetQuestionsRequest = { /** **Wymagane**. Fraza kluczowa, dla której pobierany jest raport. Pusty string zwraca `418`. */ keyword: string; /** * **Wymagane**. Identyfikator kraju; musi istnieć w słowniku krajów — nieznana wartość zwraca * `418` z komunikatem `Unknown country_id`. Uwaga: `200` jest tu mapowane na `1`. * @default 1 */ country_id: number; /** * Filtry raportu — tablica grup łączonych operatorem OR. **Działa na tej akcji.** * Nieznany `key` zwraca `418` z `invalid_filtering`. Opis mechanizmu: /types/filter */ filtering?: Array<{ filters: Array<{ key: string; match?: 'gt' | 'gte' | 'lt' | 'lte' | 'eq'; value: string | number | Array }>; conjunction?: 'and' | 'or'; }>; /** Numer strony wyników. */ page?: number; /** Liczba wierszy na stronę. */ limit?: number; } export default KeywordDetailsGetQuestionsRequest ``` > **Ostrzeżenie:** > Zarówno **`keyword`**, jak i **`country_id`** są **wymagane**. Nieznane `country_id` zwraca `418` z `Unknown country_id`, a wartość **`200` jest mapowana na `1`** — dla Polski trafisz więc do bazy 1.0, nie 2.0. > **Ostrzeżenie:** > Metryki są **zdublowane w trzech miejscach**: pola najwyższego poziomu (`searches`, `cpc`, `trends`), płaskie `trend_1` … `trend_12` oraz obiekt `statistics`. Do integracji wybierz jedno źródło — `statistics` różni się tylko tym, że jego lista cech SERP jest bez duplikatów. ## Odpowiedź Przykład poniżej to **rzeczywista odpowiedź produkcyjna** dla frazy `hamak` (`country_id: 1`), skrócona do jednego wiersza. ```json filename="przykładowa-odpowiedź" { "success": true, "data": [ { "added": "2021-03-11", "keyword": "jak zamontować hamak", "searches": 260, "cpc": 0, "words_count": 3, "trend_1": 20, "trend_2": 30, "trend_3": 70, "trend_4": 390, "trend_5": 720, "trend_6": 590, "trend_7": 590, "trend_8": 320, "trend_9": 50, "trend_10": 10, "trend_11": 10, "trend_12": 20, "kid": "47b5d3e1a14c5c24f14bd6ab34fd8e94", "variations": [ "hamak jak zamontować" ], "variations_number": 1, "snippets": [ "question", "question", "featured_answer", "image_thumbs", "people_also_ask", "video_thumbs", "featured_answer", "image_thumbs", "people_also_ask", "video_thumbs" ], "trends": [ 20, 30, 70, 390, 720, 590, 590, 320, 50, 10, 10, 20 ], "cpc_min": null, "cpc_max": null, "statistics": { "snippets": { "current": [ "question", "featured_answer", "image_thumbs", "people_also_ask", "video_thumbs" ] }, "searches": { "current": 260 }, "cpc": { "current": 0 }, "cpc_min": [], "cpc_max": [], "trends": { "history": [ 20, 30, 70, 390, 720, 590, 590, 320, 50, 10, 10, 20 ] } } } ], "pagination": { "page_count": 203, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 405, "limit": 2 } } ``` ### Struktura odpowiedzi ```ts type KeywordDetailsGetQuestionsResponse = { success: boolean; data: Array<{ keyword: string; /** Data dodania frazy do bazy, format `YYYY-MM-DD`. */ added: string; /** Średnia miesięczna liczba wyszukiwań. */ searches: number; cpc: number; cpc_min: number | null; cpc_max: number | null; words_count: number; /** Identyfikator frazy w bazie. */ kid: string; /** Warianty zapisu tej samej frazy. */ variations: string[]; variations_number: number; /** Cechy SERP frazy. Lista może zawierać **powtórzenia** — odsiane w `statistics.snippets.current`. */ snippets: string[]; /** 12-elementowy trend wyszukiwań. */ trends: number[]; /** Miesięczne wartości trendu, pola `trend_1` … `trend_12`. Duplikują tablicę `trends`. */ trend_1: number; trend_2: number; trend_3: number; trend_4: number; trend_5: number; trend_6: number; trend_7: number; trend_8: number; trend_9: number; trend_10: number; trend_11: number; trend_12: number; /** * Te same metryki co pola najwyższego poziomu, w formie zagnieżdżonej. Nic nowego nie wnosi * poza jednym: `statistics.snippets.current` jest listą **bez duplikatów**, podczas gdy * `snippets` może powtarzać te same wartości. */ statistics: { snippets: { current: string[] }; searches: { current: number }; cpc: { current: number }; cpc_min: { current: number } | []; cpc_max: { current: number } | []; trends: { history: number[] }; }; }>; /** Paginacja liczona z całego zbioru — `count` to liczba wszystkich wierszy. */ pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; limit: number; }; } export default KeywordDetailsGetQuestionsResponse ``` ## Błędy Błędy tego endpointu przychodzą we [wspólnej kopercie ze statusem `418`](/types/errors). ## Powiązane akcje Wszystkie poniższe raporty przyjmują tę samą parę `keyword` + `country_id`: - [`getStatistics`](/modules/keywords_analysis/ka-keyword-details-getStatistics) — zbiorcze statystyki frazy. - [`getKeywordsPropositions`](/modules/keywords_analysis/ka-keyword-details-getKeywordsPropositions) — propozycje fraz. - [`getRelatedKeywords`](/modules/keywords_analysis/ka-keyword-details-getRelatedKeywords) — frazy powiązane. - [`getTopicLeaders`](/modules/keywords_analysis/ka-keyword-details-getTopicLeaders) — liderzy tematu. - [`getGroups`](/modules/keywords_analysis/ka-keyword-details-getGroups) — grupy frazy. - [`getCompetitorsNumber`](/modules/keywords_analysis/ka-keyword-details-getCompetitorsNumber) — liczba konkurentów. --- # Szczegóły frazy: grupy frazy (`getGroups`) **`POST /api/keywords_analysis/reports/keyword_details/getGroups`** Zwraca **grupy tematyczne** fraz powiązanych z podanym słowem kluczowym wraz z liczbą fraz w grupie (`keywords_sum`). Pozwala zobaczyć strukturę tematu, zanim zejdziesz do pojedynczych fraz. Raport odpytujesz **wprost frazą i krajem** — bez wcześniejszego tworzenia zadania w Analizie SERP. Jedno żądanie zastępuje ścieżkę „utwórz zadanie → sprawdź status → pobierz wynik" i nie zużywa limitu zadań. --- ## Żądanie `POST` `/api/keywords_analysis/reports/keyword_details/getGroups` Nagłówki: `Authorization: Bearer `, `Content-Type: application/json`. Parametry przekazuje się w **treści żądania**. **Żądanie** ```jsonc filename="żądanie.jsonc" { "keyword": "hamak", "country_id": 1, "limit": 10 } ``` **cURL** ```bash curl --location --request POST 'https://api.senuto.com/api/keywords_analysis/reports/keyword_details/getGroups' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' \ --header 'Content-Type: application/json' \ --data '{"keyword":"hamak","country_id":1,"limit":10}' ``` ### Parametry ```ts type KeywordDetailsGetGroupsRequest = { /** **Wymagane**. Fraza kluczowa, dla której pobierany jest raport. Pusty string zwraca `418`. */ keyword: string; /** * **Wymagane**. Identyfikator kraju; musi istnieć w słowniku krajów — nieznana wartość zwraca * `418` z komunikatem `Unknown country_id`. Uwaga: `200` jest tu mapowane na `1`. * @default 1 */ country_id: number; /** * **Ignorowane przez tę akcję.** Sprawdzone na produkcji: poprawny filtr nie zmienia liczby * wyników, a nieznany `key` zwraca `200` zamiast `418`. Filtruj po stronie klienta. */ filtering?: Array>; /** Numer strony wyników. */ page?: number; /** Liczba wierszy na stronę. */ limit?: number; } export default KeywordDetailsGetGroupsRequest ``` > **Ostrzeżenie:** > Zarówno **`keyword`**, jak i **`country_id`** są **wymagane**. Nieznane `country_id` zwraca `418` z `Unknown country_id`, a wartość **`200` jest mapowana na `1`** — dla Polski trafisz więc do bazy 1.0, nie 2.0. > **Ostrzeżenie:** > Parametr **`filtering` nie działa na tej akcji.** Sprawdzone na produkcji: poprawny filtr nie zmienia liczby wyników, a nieznany klucz zwraca `200` zamiast `418`. Zawężaj wyniki po swojej stronie. > **Ostrzeżenie:** > **Paginacja jest pozorna.** Dla `limit` 1, 3, 10 i 100 API zwróciło odpowiednio 1, 3, 10 i 100 wierszy, a `count` zawsze równał się liczbie zwróconych wierszy przy `page_count: 1` i `has_next_page: false`. Nie stronicuj — pobierz całość jednym dużym `limit`. ## Odpowiedź Przykład poniżej to **rzeczywista odpowiedź produkcyjna** dla frazy `hamak` (`country_id: 1`), skrócona do jednego wiersza. ```json filename="przykładowa-odpowiedź" { "success": true, "data": [ { "group": "do hamaka", "keywords_sum": 234 } ], "pagination": { "page_count": 1, "current_page": 1, "has_next_page": false, "has_prev_page": false, "count": 2, "limit": 2 } } ``` ### Struktura odpowiedzi ```ts type KeywordDetailsGetGroupsResponse = { success: boolean; data: Array<{ /** Nazwa grupy — wspólny fragment fraz, np. `hamak ogrodowy`. */ group: string; /** Liczba fraz przypisanych do grupy. */ keywords_sum: number; }>; /** * **Paginacja pozorna.** `count` równa się liczbie zwróconych wierszy, `page_count` to zawsze * `1`, a `has_next_page` zawsze `false` — niezależnie od `limit`. Nie da się na tym * zbudować pętli stronicującej; pobierz całość jednym dużym `limit`. */ pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; limit: number; }; } export default KeywordDetailsGetGroupsResponse ``` ## Błędy Błędy tego endpointu przychodzą we [wspólnej kopercie ze statusem `418`](/types/errors). ## Powiązane akcje Wszystkie poniższe raporty przyjmują tę samą parę `keyword` + `country_id`: - [`getStatistics`](/modules/keywords_analysis/ka-keyword-details-getStatistics) — zbiorcze statystyki frazy. - [`getQuestions`](/modules/keywords_analysis/ka-keyword-details-getQuestions) — pytania o frazę. - [`getKeywordsPropositions`](/modules/keywords_analysis/ka-keyword-details-getKeywordsPropositions) — propozycje fraz. - [`getRelatedKeywords`](/modules/keywords_analysis/ka-keyword-details-getRelatedKeywords) — frazy powiązane. - [`getTopicLeaders`](/modules/keywords_analysis/ka-keyword-details-getTopicLeaders) — liderzy tematu. - [`getCompetitorsNumber`](/modules/keywords_analysis/ka-keyword-details-getCompetitorsNumber) — liczba konkurentów. --- # Szczegóły frazy: liderzy tematu (`getTopicLeaders`) **`POST /api/keywords_analysis/reports/keyword_details/getTopicLeaders`** Zwraca **adresy URL, które najczęściej rankują w TOP** dla fraz z tematu wokół podanego słowa kluczowego. `occurrences` mówi, dla ilu fraz z tematu dany URL się pojawia — czyli kto dominuje cały temat, a nie jedną frazę. Raport odpytujesz **wprost frazą i krajem** — bez wcześniejszego tworzenia zadania w Analizie SERP. Jedno żądanie zastępuje ścieżkę „utwórz zadanie → sprawdź status → pobierz wynik" i nie zużywa limitu zadań. Dla frazy `hamak` w Polsce raport zwrócił **23528** wierszy. --- ## Żądanie `POST` `/api/keywords_analysis/reports/keyword_details/getTopicLeaders` Nagłówki: `Authorization: Bearer `, `Content-Type: application/json`. Parametry przekazuje się w **treści żądania**. **Żądanie** ```jsonc filename="żądanie.jsonc" { "keyword": "hamak", "country_id": 1, "limit": 10 } ``` **cURL** ```bash curl --location --request POST 'https://api.senuto.com/api/keywords_analysis/reports/keyword_details/getTopicLeaders' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' \ --header 'Content-Type: application/json' \ --data '{"keyword":"hamak","country_id":1,"limit":10}' ``` ### Parametry ```ts type KeywordDetailsGetTopicLeadersRequest = { /** **Wymagane**. Fraza kluczowa, dla której pobierany jest raport. Pusty string zwraca `418`. */ keyword: string; /** * **Wymagane**. Identyfikator kraju; musi istnieć w słowniku krajów — nieznana wartość zwraca * `418` z komunikatem `Unknown country_id`. Uwaga: `200` jest tu mapowane na `1`. * @default 1 */ country_id: number; /** * Filtry raportu — tablica grup łączonych operatorem OR. **Działa na tej akcji.** * Nieznany `key` zwraca `418` z `invalid_filtering`. Opis mechanizmu: /types/filter */ filtering?: Array<{ filters: Array<{ key: string; match?: 'gt' | 'gte' | 'lt' | 'lte' | 'eq'; value: string | number | Array }>; conjunction?: 'and' | 'or'; }>; /** Numer strony wyników. */ page?: number; /** Liczba wierszy na stronę. */ limit?: number; } export default KeywordDetailsGetTopicLeadersRequest ``` > **Ostrzeżenie:** > Zarówno **`keyword`**, jak i **`country_id`** są **wymagane**. Nieznane `country_id` zwraca `418` z `Unknown country_id`, a wartość **`200` jest mapowana na `1`** — dla Polski trafisz więc do bazy 1.0, nie 2.0. > **Ostrzeżenie:** > Metryki są **zdublowane w trzech miejscach**: pola najwyższego poziomu (`searches`, `cpc`, `trends`), płaskie `trend_1` … `trend_12` oraz obiekt `statistics`. Do integracji wybierz jedno źródło — `statistics` różni się tylko tym, że jego lista cech SERP jest bez duplikatów. ## Odpowiedź Przykład poniżej to **rzeczywista odpowiedź produkcyjna** dla frazy `hamak` (`country_id: 1`), skrócona do jednego wiersza. ```json filename="przykładowa-odpowiedź" { "success": true, "data": [ { "url": "leroymerlin.pl/relaks-w-ogrodzie/hustawki-ogrodowe-hamaki,a47.html", "occurrences": 774 } ], "pagination": { "page_count": 11764, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 23528, "limit": 2 } } ``` ### Struktura odpowiedzi ```ts type KeywordDetailsGetTopicLeadersResponse = { success: boolean; data: Array<{ /** Adres URL — bez schematu w części wyników. */ url: string; /** Liczba fraz z tematu, dla których ten URL rankuje w TOP. */ occurrences: number; }>; /** Paginacja liczona z całego zbioru — `count` to liczba wszystkich wierszy. */ pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; limit: number; }; } export default KeywordDetailsGetTopicLeadersResponse ``` ## Błędy Błędy tego endpointu przychodzą we [wspólnej kopercie ze statusem `418`](/types/errors). ## Powiązane akcje Wszystkie poniższe raporty przyjmują tę samą parę `keyword` + `country_id`: - [`getStatistics`](/modules/keywords_analysis/ka-keyword-details-getStatistics) — zbiorcze statystyki frazy. - [`getQuestions`](/modules/keywords_analysis/ka-keyword-details-getQuestions) — pytania o frazę. - [`getKeywordsPropositions`](/modules/keywords_analysis/ka-keyword-details-getKeywordsPropositions) — propozycje fraz. - [`getRelatedKeywords`](/modules/keywords_analysis/ka-keyword-details-getRelatedKeywords) — frazy powiązane. - [`getGroups`](/modules/keywords_analysis/ka-keyword-details-getGroups) — grupy frazy. - [`getCompetitorsNumber`](/modules/keywords_analysis/ka-keyword-details-getCompetitorsNumber) — liczba konkurentów. --- # Statystyki fraz: utworzenie (`create`) > **Błąd:** > **Endpoint mutujący.** Zużywa jednostkę dziennego limitu narzędzi (`tools_daily_limit`) oraz limit `keywords_analysis_tool_keywords_statistics` (zależny od liczby fraz). Wynik zapisywany jest w cache pod `public_id` — [`getKeywords`](/modules/keywords_analysis/ka-statistics-getKeywords) odczytuje go **bez** ponownej konsumpcji. **`POST /api/keywords_analysis/tools/statistics/create`** Przykładowe żądanie: ```json { "keywords": [ "buty", "buty damskie", "sukienka", "kurtka zimowa" ], "country_id": 1 } ``` Przykładowa odpowiedź: ```json { "success": true, "data": { "has_data": true, "statistics": { "all_keywords": 8, "fetched_keywords": 8 }, "public_id": "b595c23e958c78a715474c35e80d3a18" } } ``` Pobiera **statystyki dla przesłanej listy fraz** (własnych): liczbę wyszukiwań, CPC, 12-miesięczny trend i typy snippetów SERP. Zwraca `public_id`, pod którym pełne wiersze są dostępne w cache — pobierzesz je przez [`getKeywords`](/modules/keywords_analysis/ka-statistics-getKeywords), bez ponownej konsumpcji limitu. --- ## Żądanie `POST` `/api/keywords_analysis/tools/statistics/create` ```jsonc filename="żądanie.jsonc" { "keywords": ["buty", "sukienka"], "country_id": 1 } ``` ### Parametry ```ts type KaStatisticsCreateRequest = { /** **Wymagane**. Lista fraz. */ keywords: string[]; /** **Wymagane**. ID kraju (1 = Polska). */ country_id: number; } export default KaStatisticsCreateRequest ``` ## Odpowiedź ```ts type KaStatisticsCreateResponse = { success: boolean; data: { has_data: boolean; statistics: { all_keywords: number; fetched_keywords: number }; /** Klucz do odczytu wierszy przez getKeywords. */ public_id: string; }; } export default KaStatisticsCreateResponse ``` ## Błędy Błędy tego endpointu przychodzą we [wspólnej kopercie ze statusem `418`](/types/errors). ## Powiązane akcje - [`statistics/checkData`](/modules/keywords_analysis/ka-statistics-checkData) — sprawdzenie gotowości wyniku. - [`statistics/getKeywords`](/modules/keywords_analysis/ka-statistics-getKeywords) — pobranie wierszy fraz. - [Limity zapytań](/rate-limits) — `tools_daily_limit`. --- # Statystyki fraz: status (`checkData`) **`POST /api/keywords_analysis/tools/statistics/checkData`** Przykładowe żądanie: ```json { "public_id": null } ``` Przykładowa odpowiedź: ```json { "success": true, "data": { "exists": true, "has_data": true, "statistics": { "all_keywords": 8, "fetched_keywords": 8 } } } ``` Sprawdza, czy wynik spod danego `public_id` (z [`statistics/create`](/modules/keywords_analysis/ka-statistics-create)) jest **wciąż dostępny** w cache. Gdy klucz wygasł — zwraca `{ exists: false }`. Odpowiedź zawiera statystyki zbiorcze, ale **nie** pełne wiersze (te pobierzesz przez `getKeywords`). Nie konsumuje limitu. --- ## Żądanie `POST` `/api/keywords_analysis/tools/statistics/checkData` ```jsonc filename="żądanie.jsonc" { "public_id": null } ``` ### Parametry ```ts type KaStatisticsCheckDataRequest = { /** **Wymagane**. Klucz z statistics/create. */ public_id: string; } export default KaStatisticsCheckDataRequest ``` ## Odpowiedź ```ts type KaStatisticsCheckDataResponse = { success: boolean; data: | { exists: false } | { exists: true; has_data: boolean; statistics: { all_keywords: number; fetched_keywords: number }; }; } export default KaStatisticsCheckDataResponse ``` ## Błędy Błędy tego endpointu przychodzą we [wspólnej kopercie ze statusem `418`](/types/errors). ## Powiązane akcje - [`statistics/create`](/modules/keywords_analysis/ka-statistics-create) — utworzenie wyniku. - [`statistics/getKeywords`](/modules/keywords_analysis/ka-statistics-getKeywords) — pobranie wierszy fraz. --- # Statystyki fraz: wiersze (`getKeywords`) **`POST /api/keywords_analysis/tools/statistics/getKeywords`** Przykładowe żądanie: ```json { "public_id": null, "limit": 10 } ``` Przykładowa odpowiedź: ```json { "success": true, "data": [ { "keyword": "sukienka", "searches": 368000, "cpc": 0.15, "words_count": 1, "trends": [ 368000, 301000, 368000, 450000, 550000, 368000, 301000, 301000, 246000, 246000, 301000, 368000 ], "serp_params": [ "adwords", "image_thumbs", "map", "news", "pla", "top_bar", "video_thumbs" ] } ], "pagination": { "page_count": 1, "current_page": 1, "has_next_page": false, "has_prev_page": false, "count": 8, "limit": 10 } } ``` Zwraca **pełne wiersze statystyk** dla fraz z [`statistics/create`](/modules/keywords_analysis/ka-statistics-create) (odczyt z cache po `public_id`): liczbę wyszukiwań, CPC, liczbę słów, 12-miesięczny trend oraz typy snippetów SERP (`serp_params`). Bez konsumpcji limitu. | Fraza | Wyszukiwania | CPC | Liczba słów | | --- | --- | --- | --- | | sukienka | 368000 | 0.15 | 1 | _frazy buty/sukienka/kurtka, PL. Wiersze zawierają też trend_1..12, trends[] i obiekt statistics (w JSON)._ > **Informacja:** > Obsługuje `filtering` oraz `order`. `serp_params` to typy elementów SERP obecne dla frazy. Odczyt z cache — nie konsumuje limitu. --- ## Żądanie `POST` `/api/keywords_analysis/tools/statistics/getKeywords` ```jsonc filename="żądanie.jsonc" { "public_id": null, "limit": 10, "page": 1 } ``` ### Parametry ```ts type KaStatisticsGetKeywordsRequest = { /** **Wymagane**. Klucz z statistics/create. */ public_id: string; /** Liczba wierszy na stronę. @default 10 */ limit?: number; /** Numer strony. @default 1 */ page?: number; filtering?: Record[]; order?: Record[]; } export default KaStatisticsGetKeywordsRequest ``` ## Odpowiedź ```ts type KaStatisticsGetKeywordsResponse = { success: boolean; data: Array<{ keyword: string; searches: number; cpc: number; words_count: number; /** 12-miesięczny trend liczby wyszukiwań. */ trends: number[]; /** Typy snippetów SERP obecne dla frazy. */ serp_params: string[]; }>; pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; limit: number; }; } export default KaStatisticsGetKeywordsResponse ``` ## Błędy Błędy tego endpointu przychodzą we [wspólnej kopercie ze statusem `418`](/types/errors). ## Powiązane akcje - [`statistics/create`](/modules/keywords_analysis/ka-statistics-create) — utworzenie wyniku. --- # Eksport: statystyki fraz (CSV) (`getKeywords`) > **Błąd:** > **Endpoint mutujący, zwraca PLIK CSV.** Zużywa jednostkę `tools_daily_limit`. Odpowiedzią jest plik do pobrania, a **nie** JSON. Eksportuje do pliku **CSV** wiersze statystyk fraz (odpowiednik [`statistics/getKeywords`](/modules/keywords_analysis/ka-statistics-getKeywords), wszystkie wiersze). Odczyt z cache po `public_id`. ## Żądanie `POST` `/api/keywords_analysis/tools/exports/statistics/getKeywords` ```jsonc filename="żądanie.jsonc" { "public_id": "" } ``` ### Parametry ```ts type KaExportStatisticsRequest = { /** **Wymagane**. Klucz z statistics/create. */ public_id: string; filtering?: Record[]; order?: Record[]; } export default KaExportStatisticsRequest ``` ## Kolumny pliku CSV | Kolumna | Pole źródłowe | | ----------------------------- | ----------------------------- | | Fraza | `keyword` | | Wyszukiwania | `statistics.searches.current` | | Liczba słów | `words_count` | | CPC | `statistics.cpc.current` | | Snippety / nazwy snippetów | `snippets` / `name_snippets` | | Sezonowość (styczeń–grudzień) | `trend_1` … `trend_12` | ## Błędy Błędy tego endpointu przychodzą we [wspólnej kopercie ze statusem `418`](/types/errors). ## Powiązane akcje - [`statistics/getKeywords`](/modules/keywords_analysis/ka-statistics-getKeywords) — wersja JSON (z paginacją). - [Limity zapytań](/rate-limits) — `tools_daily_limit`. --- # Analiza SERP Moduł **Analiza SERP** bada wyniki wyszukiwania (TOP) dla wybranej frazy: strony w rankingu, ich treść i nagłówki, statystyki konkurencji oraz powiązane frazy i pytania. Wymaga tokena Bearer, dostępu do API oraz modułu **Analiza SERP** w planie (`systemModuleKey: serp_analysis`). Wspólne mechanizmy: [Filtrowanie](/types/filter) · [Paginacja](/types/pagination) · [Błędy i status `418`](/types/errors). ## Model pracy: zadanie → raporty W przeciwieństwie do raportów odpytywanych wprost, Analiza SERP działa **asynchronicznie** przez obiekt **zadania** (`task`): 1. **Utwórz zadanie** — [`serp_analysis/create`](/modules/serp_analysis/serp-task-create) dla pary `keyword` + `country_id`. Zwraca `id` zadania i uruchamia crawl SERP. **Zużywa jednostkę** dziennego limitu (`serp_analysis_daily_limit`). 2. **Poczekaj aż gotowe** — odpytuj [`serp_analysis/check`](/modules/serp_analysis/serp-task-check) aż `progress.has_serp_data = true`. 3. **Pobieraj raporty** — wszystkie raporty (`reports/*`) przyjmują `task_id` gotowego zadania i zwracają dane. Zadanie niegotowe → błąd walidacji `Unfinished task`. > **Informacja:** > Ścieżki zarządzania zadaniem są w przestrzeni `tasks/management` (`/api/tasks/management/serp_analysis/...`), a raporty w `serp_analysis/reports/...`. --- # Zadanie: utworzenie (`create`) > **Błąd:** > **Endpoint mutujący.** Każde wywołanie **zużywa jednostkę** dziennego limitu Analizy SERP (`serp_analysis_daily_limit`) i uruchamia crawl wyników. Zachowaj `task_id` po swojej stronie — raporty gotowego zadania odpytasz później bez konsumpcji limitu. **`POST /api/tasks/management/serp_analysis/create`** Przykładowe żądanie: ```json { "keyword": "obroża dla psa świecąca", "country_id": 1 } ``` Przykładowa odpowiedź: ```json { "success": true, "data": { "id": 4472873, "status": "crawling", "country_id": 1, "is_read": 0, "type": "serp_analysis", "data": { "keyword": "obroża dla psa świecąca" }, "raw_request": { "keyword": "obroża dla psa świecąca", "country_id": 1 }, "created": 1789043179, "completed": null, "progress": { "has_serp_data": false, "has_keywords_analysis_data": true }, "refreshable": false } } ``` Tworzy **zadanie analizy SERP** dla pary `keyword` + `country_id` i uruchamia crawl wyników wyszukiwania. Zwraca `id`, którego użyjesz w [`check`](/modules/serp_analysis/serp-task-check) (odpytywanie statusu) oraz we wszystkich raportach (`reports/*`). Bezpośrednio po utworzeniu `status = "crawling"` i `progress.has_serp_data = false` — raporty SERP zwrócą dane dopiero, gdy `has_serp_data` będzie `true`. > **Informacja:** > Jeśli identyczne zadanie (ta sama fraza + kraj) już istnieje i jest świeże, backend może zwrócić istniejące zadanie **bez** ponownego naliczenia limitu. --- ## Żądanie `POST` `/api/tasks/management/serp_analysis/create` ```jsonc filename="żądanie.jsonc" { "keyword": "obroża dla psa świecąca", "country_id": 1 } ``` ### Parametry ```ts type SerpCreateRequest = { /** **Wymagane**. Fraza (min. 2 znaki). */ keyword: string; /** **Wymagane**. ID kraju (baza krajów Senuto; 1 = Polska). */ country_id: number; } export default SerpCreateRequest ``` ## Odpowiedź ```ts type SerpCreateResponse = { success: boolean; data: { /** ID zadania — używane w check i raportach jako task_id. */ id: number; /** np. "crawling" | "completed". */ status: string; country_id: number; is_read: number; type: string; data: { keyword: string }; raw_request: Record; /** Unix timestamp utworzenia. */ created: number; /** Unix timestamp zakończenia lub null. */ completed: number | null; progress: { has_serp_data: boolean; has_keywords_analysis_data: boolean; }; refreshable: boolean; }; } export default SerpCreateResponse ``` ## Błędy Błędy tego endpointu przychodzą we [wspólnej kopercie ze statusem `418`](/types/errors). ## Powiązane akcje - [`serp_analysis/check`](/modules/serp_analysis/serp-task-check) — odpytywanie statusu zadania. - [Limity zapytań](/rate-limits) — dzienny limit `serp_analysis_daily_limit`. --- # Zadanie: status (`check`) **`GET /api/tasks/management/serp_analysis/check`** Przykładowe żądanie: ```json { "task_id": null } ``` Przykładowa odpowiedź: ```json { "success": true, "data": { "id": 1579873, "user_id": 1234, "status": "completed", "country_id": 1, "type": "serp_analysis", "data": { "keyword": "pies" }, "created": 1651659965, "completed": 1743147214, "progress": { "has_serp_data": true, "has_keywords_analysis_data": true } } } ``` Zwraca **status i postęp** zadania SERP. Po utworzeniu zadania odpytuj ten endpoint (parametr `task_id` w query stringu) aż `progress.has_serp_data = true` — dopiero wtedy raporty SERP (`reports/*`) zwrócą dane; wcześniej odpowiadają błędem walidacji `Unfinished task`. > **Informacja:** > `has_serp_data` = crawl SERP zakończony (raporty URL-i, treści, tytułów gotowe). `has_keywords_analysis_data` = dane Bazy słów dla frazy gotowe (raporty powiązanych fraz, pytań, propozycji). --- ## Żądanie `GET` `/api/tasks/management/serp_analysis/check?task_id=1579873` ### Parametry (query string) ```ts type SerpCheckRequest = { /** **Wymagane**. ID zadania z create. */ task_id: number; } export default SerpCheckRequest ``` ## Odpowiedź ```ts type SerpCheckResponse = { success: boolean; data: { id: number; user_id: number; status: string; country_id: number; type: string; data: { keyword: string }; created: number | null; completed: number | null; serp_analysis_tasks: Array<{ id: number; task_id: number; keywords_analysis_data_request: { status: string; value: string; match: string; }; }>; progress: { has_serp_data: boolean; has_keywords_analysis_data: boolean; }; }; } export default SerpCheckResponse ``` ## Błędy Błędy tego endpointu przychodzą we [wspólnej kopercie ze statusem `418`](/types/errors). ## Powiązane akcje - [`serp_analysis/create`](/modules/serp_analysis/serp-task-create) — utworzenie zadania. --- # Fraza: statystyki (`getStatistics`) **`POST /api/serp_analysis/reports/keyword/getStatistics`** Przykładowe żądanie: ```json { "task_id": null } ``` Przykładowa odpowiedź: ```json { "success": true, "data": { "trends": [ 90500, 90500, 90500, 110000, 135000, 135000, 135000, 135000, 110000, 110000, 110000, 110000 ], "rank_value": 36044.92, "cpc": 0.92, "params": [ "image_thumbs", "video_thumbs", "wiki_right" ], "searches": 110000 } } ``` Zwraca **podstawowe statystyki frazy** zadania: średnią miesięczną liczbę wyszukiwań (`searches`), 12-miesięczny trend (`trends`), koszt kliknięcia (`cpc`), wskaźnik `rank_value` oraz obecne w SERP typy snippetów (`params`). **Trend wyszukiwań frazy „pies” (12 miesięcy)** | Wartość | wyszukiwań/mies. | | --- | --- | | mies. 1 | 90500 | | mies. 2 | 90500 | | mies. 3 | 90500 | | mies. 4 | 110000 | | mies. 5 | 135000 | | mies. 6 | 135000 | | mies. 7 | 135000 | | mies. 8 | 135000 | | mies. 9 | 110000 | | mies. 10 | 110000 | | mies. 11 | 110000 | | mies. 12 | 110000 | _task_id 1579873. Wartości z data.trends._ > **Informacja:** > `params` to lista typów elementów SERP obecnych dla frazy (np. `image_thumbs` = miniatury grafik, `wiki_right` = panel wiedzy). Wymaga `task_id` **gotowego** zadania (`progress.has_serp_data`). --- ## Żądanie `POST` `/api/serp_analysis/reports/keyword/getStatistics` ```jsonc filename="żądanie.jsonc" { "task_id": null } ``` ### Parametry ```ts type SerpKeywordStatisticsRequest = { /** **Wymagane**. ID gotowego zadania SERP. Realny `task_id` pobierzesz z `POST /api/tasks/management/serp_analysis/list` albo z odpowiedzi `create`. */ task_id: number; } export default SerpKeywordStatisticsRequest ``` ## Odpowiedź ```ts type SerpKeywordStatisticsResponse = { success: boolean; data: { /** 12-miesięczny trend liczby wyszukiwań. */ trends: number[]; rank_value: number; cpc: number; /** Typy snippetów SERP obecne dla frazy. */ params: string[]; searches: number; }; } export default SerpKeywordStatisticsResponse ``` ## Błędy Błędy tego endpointu przychodzą we [wspólnej kopercie ze statusem `418`](/types/errors). ## Powiązane akcje - [`keyword/getCompetitorsNumber`](/modules/serp_analysis/serp-keyword-getCompetitorsNumber) — liczba konkurentów w SERP. - [`keyword/getRelatedKeywords`](/modules/serp_analysis/serp-keyword-getRelatedKeywords) — frazy powiązane. --- # Fraza: liczba konkurentów (`getCompetitorsNumber`) **`POST /api/serp_analysis/reports/keyword/getCompetitorsNumber`** Przykładowe żądanie: ```json { "task_id": null } ``` Przykładowa odpowiedź: ```json { "success": true, "data": { "competitors_number": 228 } } ``` Zwraca **liczbę domen** konkurujących o badaną frazę w wynikach wyszukiwania. Pojedyncza wartość liczbowa — miara nasycenia konkurencją. --- ## Żądanie `POST` `/api/serp_analysis/reports/keyword/getCompetitorsNumber` ```jsonc filename="żądanie.jsonc" { "task_id": null } ``` ### Parametry ```ts type SerpCompetitorsNumberRequest = { /** **Wymagane**. ID gotowego zadania SERP. Realny `task_id` pobierzesz z `POST /api/tasks/management/serp_analysis/list` albo z odpowiedzi `create`. */ task_id: number; } export default SerpCompetitorsNumberRequest ``` ## Odpowiedź ```ts type SerpCompetitorsNumberResponse = { success: boolean; data: { competitors_number: number }; } export default SerpCompetitorsNumberResponse ``` ## Błędy Błędy tego endpointu przychodzą we [wspólnej kopercie ze statusem `418`](/types/errors). ## Powiązane akcje - [`keyword/getStatistics`](/modules/serp_analysis/serp-keyword-getStatistics) — statystyki frazy. - [`keyword/getTopicLeaders`](/modules/serp_analysis/serp-keyword-getTopicLeaders) — najczęstsze URL-e w temacie. --- # Fraza: powiązane (`getRelatedKeywords`) **`POST /api/serp_analysis/reports/keyword/getRelatedKeywords`** Przykładowe żądanie: ```json { "task_id": null, "limit": 10 } ``` Przykładowa odpowiedź: ```json { "success": true, "data": [ { "id": "89893385", "keyword": "pies", "searches": 110000, "common_factor": 18, "parent_keyword": "pies", "cpc": 0.92, "words_count": 1, "cpc_min": 0.08, "cpc_max": 1.76, "params": [ "image_thumbs", "video_thumbs", "wiki_right" ] } ], "pagination": { "page_count": 2, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 16, "limit": 10 } } ``` Zwraca **frazy powiązane** semantycznie z badaną frazą — na podstawie wspólnych wyników w SERP (`common_factor`). Dla każdej frazy: liczbę wyszukiwań, CPC (z zakresem `cpc_min`/`cpc_max`), liczbę słów, 12-miesięczny trend oraz typy snippetów (`params`). | Fraza | Wyszukiwania | Wsp. wspólny | Liczba słów | CPC | CPC min | CPC max | Fraza źródłowa | | --- | --- | --- | --- | --- | --- | --- | --- | | pies | 110000 | 18 | 1 | 0.92 | 0.08 | 1.76 | pies | | pies pies | 590 | 6 | 2 | 0 | 0 | 0 | pies | | piesie | 50 | 5 | 1 | 0 | | | pies | _task_id 1579873 „pies”. Wiersze zawierają też pola trend_1..12 oraz obiekt statistics (w JSON)._ > **Informacja:** > `common_factor` = liczba wspólnych adresów URL w TOP dla obu fraz (im wyższy, tym silniejsze powiązanie). Wymaga `task_id` gotowego zadania. --- ## Żądanie `POST` `/api/serp_analysis/reports/keyword/getRelatedKeywords` ```jsonc filename="żądanie.jsonc" { "task_id": null, "limit": 10, "page": 1 } ``` ### Parametry ```ts type SerpRelatedKeywordsRequest = { /** **Wymagane**. ID gotowego zadania SERP. Realny `task_id` pobierzesz z `POST /api/tasks/management/serp_analysis/list` albo z odpowiedzi `create`. */ task_id: number; /** Liczba wierszy na stronę. @default 10 */ limit?: number; /** Numer strony. @default 1 */ page?: number; } export default SerpRelatedKeywordsRequest ``` ## Odpowiedź ```ts type SerpRelatedKeywordsResponse = { success: boolean; data: Array<{ id: string; keyword: string; searches: number; /** Liczba wspólnych URL-i w TOP z frazą źródłową. */ common_factor: number; parent_keyword: string; cpc: number; words_count: number; cpc_min: number | null; cpc_max: number | null; params: string[]; /** trend_1..trend_12 — miesięczne wartości trendu. */ [trend: string]: unknown; }>; pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; limit: number; }; } export default SerpRelatedKeywordsResponse ``` ## Błędy Błędy tego endpointu przychodzą we [wspólnej kopercie ze statusem `418`](/types/errors). ## Powiązane akcje - [`keyword/getKeywordsPropositions`](/modules/serp_analysis/serp-keyword-getKeywordsPropositions) — propozycje fraz. - [`keyword/getQuestions`](/modules/serp_analysis/serp-keyword-getQuestions) — pytania powiązane. --- # Fraza: propozycje (`getKeywordsPropositions`) **`POST /api/serp_analysis/reports/keyword/getKeywordsPropositions`** Przykładowe żądanie: ```json { "task_id": null, "limit": 10, "filtering": [ { "filters": [ { "key": "keywords", "items": [ { "value": "pies", "match": "contain" } ] } ], "conjunction": "and" } ] } ``` Przykładowa odpowiedź: ```json { "success": true, "data": [ { "added": "2021-06-02", "keyword": "pies", "searches": 110000, "cpc": 0.92, "words_count": 1, "kid": "fb2fe71d592fad516f05549409da8e35", "cpc_min": 0.08, "cpc_max": 1.76, "variations_number": 60, "snippets": [ "image_thumbs", "video_thumbs", "wiki_right" ] } ], "pagination": { "page_count": 3071, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 30707, "limit": 10 } } ``` Zwraca **propozycje fraz** do rozbudowy treści wokół badanego tematu — frazy powiązane z metrykami (wyszukiwania, CPC, trend, warianty pisowni `variations`, typy snippetów). Przydatne przy planowaniu contentu pod pełne pokrycie tematyczne. | Fraza | Wyszukiwania | CPC | CPC min | CPC max | Liczba słów | Warianty | Dodano | | --- | --- | --- | --- | --- | --- | --- | --- | | pies | 110000 | 0.92 | 0.08 | 1.76 | 1 | 60 | 2021-06-02 | | pies sznaucer miniaturowy | 60500 | 0.13 | 0.06 | 0.19 | 3 | 1 | 2021-03-11 | | berneński pies pasterski | 60500 | 0.1 | 0.08 | 0.13 | 3 | 9 | 2021-06-22 | _task_id 1579873 „pies”, z filtrem na frazy zawierające „pies”. Wiersze zawierają też trend_1..12, trends[], variations[] i obiekt statistics (w JSON)._ > **Informacja:** > Obsługuje `filtering` (jak w Bazie słów). `variations` to warianty pisowni frazy. Wymaga `task_id` gotowego zadania. --- ## Żądanie `POST` `/api/serp_analysis/reports/keyword/getKeywordsPropositions` ```jsonc filename="żądanie.jsonc" { "task_id": null, "limit": 10, "page": 1, // bez filtra ranking schodzi na najpopularniejsze frazy z całego zbioru propozycji, // niekoniecznie związane z tematem — filtr trzyma listę przy badanej frazie "filtering": [{ "filters": [{ "key": "keywords", "items": [{ "value": "pies", "match": "contain" }] }], "conjunction": "and" }] } ``` ### Parametry ```ts type SerpKeywordsPropositionsRequest = { /** **Wymagane**. ID gotowego zadania SERP. Realny `task_id` pobierzesz z `POST /api/tasks/management/serp_analysis/list` albo z odpowiedzi `create`. */ task_id: number; /** Liczba wierszy na stronę. @default 10 */ limit?: number; /** Numer strony. @default 1 */ page?: number; /** Opcjonalne filtry (jak w Bazie słów). */ filtering?: Record[]; } export default SerpKeywordsPropositionsRequest ``` ## Odpowiedź ```ts type SerpKeywordsPropositionsResponse = { success: boolean; data: Array<{ added: string; keyword: string; searches: number; cpc: number; cpc_min: number | null; cpc_max: number | null; words_count: number; kid: string; variations: string[]; variations_number: number; snippets: string[]; trends: number[]; }>; pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; limit: number; }; } export default SerpKeywordsPropositionsResponse ``` ## Błędy Błędy tego endpointu przychodzą we [wspólnej kopercie ze statusem `418`](/types/errors). ## Powiązane akcje - [`keyword/getRelatedKeywords`](/modules/serp_analysis/serp-keyword-getRelatedKeywords) — frazy powiązane. - [`keyword/getQuestions`](/modules/serp_analysis/serp-keyword-getQuestions) — pytania powiązane. --- # Fraza: pytania (`getQuestions`) **`POST /api/serp_analysis/reports/keyword/getQuestions`** Przykładowe żądanie: ```json { "task_id": null, "limit": 10 } ``` Przykładowa odpowiedź: ```json { "success": true, "data": [ { "keyword": "półpasiec objawy", "searches": 33100, "cpc": 0.01, "cpc_min": 0.01, "cpc_max": 0.01, "words_count": 2, "added": "2021-06-22", "kid": "9903e4549ad179b86cec430b4c582910", "variations_number": 2, "snippets": [ "question", "featured_answer" ] } ], "pagination": { "page_count": 17289, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 51865, "limit": 10 } } ``` Zwraca **pytania** powiązane z tematem badanej frazy — frazy pytające z metrykami (wyszukiwania, CPC, trend, warianty, snippety). Podstawa pod sekcje FAQ i treści odpowiadające na intencje użytkowników. | Pytanie / fraza | Wyszukiwania | CPC | Liczba słów | Warianty | Dodano | | --- | --- | --- | --- | --- | --- | | półpasiec objawy | 33100 | 0.01 | 2 | 2 | 2021-06-22 | | jak narysowac kotka | 22200 | 0 | 2 | 1 | 2021-06-22 | | ile kolan ma pies | 18100 | 0 | 3 | 0 | 2023-10-02 | _task_id 1579873 „pies”. Wiersze zawierają też trend_1..12, trends[], variations[] i obiekt statistics (w JSON)._ > **Informacja:** > Zestaw pytań zależy od frazy — dla niektórych zadań lista bywa pusta. Obsługuje `filtering`. Wymaga `task_id` gotowego zadania. --- ## Żądanie `POST` `/api/serp_analysis/reports/keyword/getQuestions` ```jsonc filename="żądanie.jsonc" { "task_id": null, "limit": 10, "page": 1 } ``` ### Parametry ```ts type SerpQuestionsRequest = { /** **Wymagane**. ID gotowego zadania SERP. Realny `task_id` pobierzesz z `POST /api/tasks/management/serp_analysis/list` albo z odpowiedzi `create`. */ task_id: number; /** Liczba wierszy na stronę. @default 10 */ limit?: number; /** Numer strony. @default 1 */ page?: number; /** Opcjonalne filtry. */ filtering?: Record[]; } export default SerpQuestionsRequest ``` ## Odpowiedź ```ts type SerpQuestionsResponse = { success: boolean; data: Array<{ keyword: string; searches: number; cpc: number; cpc_min: number; cpc_max: number; words_count: number; added: string; kid: string; variations: string[]; variations_number: number; snippets: string[]; trends: number[]; }>; pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; limit: number; }; } export default SerpQuestionsResponse ``` ## Błędy Błędy tego endpointu przychodzą we [wspólnej kopercie ze statusem `418`](/types/errors). ## Powiązane akcje - [`keyword/getKeywordsPropositions`](/modules/serp_analysis/serp-keyword-getKeywordsPropositions) — propozycje fraz. - [`keyword/getGroups`](/modules/serp_analysis/serp-keyword-getGroups) — grupy tematyczne. --- # Fraza: liderzy tematu (`getTopicLeaders`) **`POST /api/serp_analysis/reports/keyword/getTopicLeaders`** Przykładowe żądanie: ```json { "task_id": null, "limit": 10 } ``` Przykładowa odpowiedź: ```json { "success": true, "data": [ { "url": "allegro.pl/kategoria/zywe-zwierzeta-psy-5344", "occurrences": 2752 }, { "url": "gratka.pl/zwierzeta/psy", "occurrences": 1038 } ], "pagination": { "page_count": 72489, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 724882, "limit": 10 } } ``` Zwraca **liderów tematu** — adresy URL najczęściej pojawiające się w TOP dla fraz z badanego tematu (`occurrences` = liczba fraz, dla których URL rankuje). Wskazuje strony o najsilniejszej pozycji tematycznej. | URL | Liczba wystąpień | | --- | --- | | allegro.pl/kategoria/zywe-zwierzeta-psy-5344 | 2752 | | gratka.pl/zwierzeta/psy | 1038 | | psy.pl/imiona-dla-psa/ | 922 | | medme.pl/artykuly/badanie-psa-normy-wolny-calkowity,67743.html | 834 | | polki.pl/dom/zwierzeta,imiona-dla-psow,10410993,artykul.html | 832 | _task_id 1579873 „pies”._ --- ## Żądanie `POST` `/api/serp_analysis/reports/keyword/getTopicLeaders` ```jsonc filename="żądanie.jsonc" { "task_id": null, "limit": 10, "page": 1 } ``` ### Parametry ```ts type SerpTopicLeadersRequest = { /** **Wymagane**. ID gotowego zadania SERP. */ task_id: number; /** * Filtrowanie zbioru fraz tematu. Ten sam mechanizm co w keywords/getKeywords * (komponent dziedziczy rejestr filtrów Bazy słów). Nieznany `key` → `418` `invalid_filtering`. * Dozwolone klucze m.in.: `searches`, `cpc`, `words_count`, `keywords`, `snippets`, * `trends_peaks`, `added`. Operatory liczbowe: `gt` | `gte` | `lt` | `lte` | `eq`. */ filtering?: Array<{ filters: Array<{ key: string; match?: 'gt' | 'gte' | 'lt' | 'lte' | 'eq'; value: string | number | Array; complement?: boolean }>; conjunction?: 'and' | 'or'; }>; /** Liczba wierszy na stronę. @default 10 */ limit?: number; /** Numer strony. @default 1 */ page?: number; } export default SerpTopicLeadersRequest ``` ## Odpowiedź ```ts type SerpTopicLeadersResponse = { success: boolean; data: Array<{ url: string; /** Liczba fraz z tematu, dla których URL rankuje w TOP. */ occurrences: number; }>; pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; limit: number; }; } export default SerpTopicLeadersResponse ``` ## Błędy Błędy tego endpointu przychodzą we [wspólnej kopercie ze statusem `418`](/types/errors). ## Powiązane akcje - [`urls/getList`](/modules/serp_analysis/serp-urls-getList) — pełna lista wyników SERP badanej frazy. - [`keyword/getGroups`](/modules/serp_analysis/serp-keyword-getGroups) — grupy tematyczne. --- # Fraza: grupy (`getGroups`) **`POST /api/serp_analysis/reports/keyword/getGroups`** Przykładowe żądanie: ```json { "task_id": null, "limit": 10 } ``` Przykładowa odpowiedź: ```json { "success": true, "data": [ { "group": "dla psa", "keywords_sum": 29661 }, { "group": "u psa", "keywords_sum": 10599 } ], "pagination": { "page_count": 1, "current_page": 1, "has_next_page": false, "has_prev_page": false, "count": 10, "limit": 10 } } ``` Zwraca **grupy tematyczne** fraz powiązanych z badaną frazą — klastry semantyczne z liczbą przypisanych fraz (`keywords_sum`). Ułatwia zaplanowanie struktury treści wokół podtematów. | Grupa | Liczba fraz | | --- | --- | | dla psa | 29661 | | u psa | 10599 | | dla psów | 7475 | | z psem | 4456 | | karma dla | 4019 | _task_id 1579873 „pies”._ --- ## Żądanie `POST` `/api/serp_analysis/reports/keyword/getGroups` ```jsonc filename="żądanie.jsonc" { "task_id": null, "limit": 10, "page": 1 } ``` ### Parametry ```ts type SerpGroupsRequest = { /** **Wymagane**. ID gotowego zadania SERP. Realny `task_id` pobierzesz z `POST /api/tasks/management/serp_analysis/list` albo z odpowiedzi `create`. */ task_id: number; /** Liczba wierszy na stronę. @default 10 */ limit?: number; /** Numer strony. @default 1 */ page?: number; /** Opcjonalne filtry. */ filtering?: Record[]; } export default SerpGroupsRequest ``` ## Odpowiedź ```ts type SerpGroupsResponse = { success: boolean; data: Array<{ group: string; keywords_sum: number; }>; pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; limit: number; }; } export default SerpGroupsResponse ``` ## Błędy Błędy tego endpointu przychodzą we [wspólnej kopercie ze statusem `418`](/types/errors). ## Powiązane akcje - [`keyword/getRelatedKeywords`](/modules/serp_analysis/serp-keyword-getRelatedKeywords) — frazy powiązane. - [`keyword/getTopicLeaders`](/modules/serp_analysis/serp-keyword-getTopicLeaders) — liderzy tematu. --- # Fraza: URL-e · lista wyników (`getList`) **`POST /api/serp_analysis/reports/urls/getList`** Przykładowe żądanie: ```json { "task_id": null, "limit": 10 } ``` Przykładowa odpowiedź: ```json { "success": true, "data": { "rows": [ { "position": 1, "domain": "pl.wikipedia.org", "main_domain": "wikipedia.org", "url": "https://pl.wikipedia.org/wiki/Pies_domowy", "uid": "8c704d3f9fff2c37bde6760a06988343", "content": { "title": { "value": "Pies domowy – Wikipedia, wolna encyklopedia", "length": 43 }, "description": { "value": "", "length": 0 }, "text2html_ratio": 0.2, "length": 39467, "headers": { "h1": { "value": [ "Pies domowy[edytuj wstęp]" ], "count": 1 }, "h2": { "value": [ "Wstęp", "Taksonomia", "Budowa i wygląd", "Wiek psa", "Zmysły psa", "Komunikacja niewerbalna psa", "Historia i udomowienie", "Użytkowość", "Choroby genetyczne psów", "Wpływ psa na człowieka", "Medycyna ludowa", "Miejsce chowu psa", "Rozmnażanie psów", "Psy na znaczkach pocztowych", "Zobacz też", "Przypisy", "Bibliografia", "Linki zewnętrzne" ], "count": 18 }, "h3": { "value": [ "Mózg psa", "Skóra i sierść psa", "Udomowienie", "Historia psa domowego na ziemiach polskich" ], "count": 4 } } }, "keywords": { "number": 0 } } ], "extra": [] }, "pagination": { "page_count": 2, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 20, "limit": 10 } } ``` Zwraca **listę adresów w TOP** dla badanej frazy wraz z analizą treści każdej strony: pozycję, domenę, tytuł i opis (z długościami), stosunek tekstu do HTML (`text2html_ratio`), długość treści oraz nagłówki `h1`/`h2`/`h3` (wartości i liczność). Podstawa analizy konkurencji na poziomie pojedynczych stron. | Pozycja | Domena | URL | Tytuł | Dł. tytułu | Dł. treści | text/HTML | H1 | H2 | | --- | --- | --- | --- | --- | --- | --- | --- | --- | | 1 | pl.wikipedia.org | https://pl.wikipedia.org/wiki/Pies_domowy | Pies domowy – Wikipedia, wolna encyklopedia | 43 | 39467 | 0.2 | 1 | 18 | | 2 | royalcanin.com | https://www.royalcanin.com/pl/dogs/breeds | | 0 | 0 | 0.1 | 0 | 0 | | 3 | filmweb.pl | https://www.filmweb.pl/film/Pies-2022-867994 | Pies (2022) - Filmweb | 21 | 569 | 0.07 | 1 | 1 | _task_id 1579873 „pies”. Wiersze zawierają pełne obiekty content.headers (h1/h2/h3) w JSON._ > **Informacja:** > Opcjonalne `compare_url_ids` (tablica ID własnych URL-i dodanych przez zarządzanie URL-ami) dokłada je do porównania. Obsługuje `filtering`. `extra` zawiera dane porównywanych URL-i użytkownika. Wymaga `task_id` gotowego zadania. --- ## Żądanie `POST` `/api/serp_analysis/reports/urls/getList` ```jsonc filename="żądanie.jsonc" { "task_id": null, "limit": 10, "page": 1 } ``` ### Parametry ```ts type SerpUrlsGetListRequest = { /** **Wymagane**. ID gotowego zadania SERP. Realny `task_id` pobierzesz z `POST /api/tasks/management/serp_analysis/list` albo z odpowiedzi `create`. */ task_id: number; /** Liczba wierszy na stronę. @default 10 */ limit?: number; /** Numer strony. @default 1 */ page?: number; /** Opcjonalne filtry. */ filtering?: Record[]; /** ID własnych URL-i do porównania. */ compare_url_ids?: number[]; } export default SerpUrlsGetListRequest ``` ## Odpowiedź ```ts type SerpUrlsGetListResponse = { success: boolean; data: { rows: Array<{ position: number; domain: string; main_domain: string; url: string; uid: string; content: { title: { value: string; length: number }; description: { value: string; length: number }; text2html_ratio: number; length: number; headers: { h1: { value: string[]; count: number }; h2: { value: string[]; count: number }; h3: { value: string[]; count: number }; }; }; keywords: { number: number }; }>; /** Dane porównywanych URL-i użytkownika (compare_url_ids). */ extra: unknown[]; }; pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; limit: number; }; } export default SerpUrlsGetListResponse ``` ## Błędy Błędy tego endpointu przychodzą we [wspólnej kopercie ze statusem `418`](/types/errors). ## Powiązane akcje - [`content/getStatistics`](/modules/serp_analysis/serp-content-getStatistics) — zbiorcze statystyki treści TOP. --- # Fraza: treść · statystyki (`getStatistics`) **`POST /api/serp_analysis/reports/content/getStatistics`** Przykładowe żądanie: ```json { "task_id": null } ``` Przykładowa odpowiedź: ```json { "success": true, "data": { "statistics": { "title": { "avg": 43.65, "min": 9, "domain_min": "josera.pl", "max": 97, "domain_max": "sklep.petsmile.pl" }, "description": { "avg": 126.4, "min": 92, "domain_min": "pieszcharakterem.pl", "max": 466, "domain_max": "onet.pl" }, "content": { "avg": 5826.95, "min": 217, "domain_min": "pieszcharakterem.pl", "max": 39467, "domain_max": "pl.wikipedia.org" }, "h1": { "avg": 1.7, "min": 1, "domain_min": "pl.wikipedia.org", "max": 17, "domain_max": "petbox.pl" } }, "extra_statistics": [] } } ``` Zwraca **zbiorcze statystyki treści** stron w TOP dla frazy: dla każdej metryki (długość tytułu, opisu, treści, `text2html_ratio` oraz liczba nagłówków `h1`/`h2`/`h3`) — wartość średnią (`avg`), minimum i maksimum wraz z domenami, które je osiągnęły. Pozwala zorientować się w „normie" treści wśród konkurencji. | Metryka | Średnia | Min | Domena (min) | Max | Domena (max) | | --- | --- | --- | --- | --- | --- | | title (dł.) | 43.65 | 9 | josera.pl | 97 | sklep.petsmile.pl | | description (dł.) | 126.4 | 92 | pieszcharakterem.pl | 466 | onet.pl | | content (dł.) | 5826.95 | 217 | pieszcharakterem.pl | 39467 | pl.wikipedia.org | | h1 (liczba) | 1.7 | 1 | pl.wikipedia.org | 17 | petbox.pl | | h2 (liczba) | 4.25 | 1 | psy24.pl | 18 | pl.wikipedia.org | | h3 (liczba) | 1.25 | 1 | wiadomosci.wp.pl | 20 | piesporadnik.pl | _task_id 1579873 „pies”. Wiersze zbudowane z data.statistics (każda metryka = jeden wiersz)._ > **Informacja:** > Klucze `statistics` to: `title`, `description`, `text2html_ratio`, `content`, `h1`, `h2`, `h3`. Obsługuje `filtering` oraz `compare_url_ids`. Wymaga `task_id` gotowego zadania. --- ## Żądanie `POST` `/api/serp_analysis/reports/content/getStatistics` ```jsonc filename="żądanie.jsonc" { "task_id": null } ``` ### Parametry ```ts type SerpContentStatisticsRequest = { /** **Wymagane**. ID gotowego zadania SERP. Realny `task_id` pobierzesz z `POST /api/tasks/management/serp_analysis/list` albo z odpowiedzi `create`. */ task_id: number; /** Opcjonalne filtry. */ filtering?: Record[]; /** ID własnych URL-i do porównania. */ compare_url_ids?: number[]; } export default SerpContentStatisticsRequest ``` ## Odpowiedź ```ts type MetricStat = { avg: number; min: number; domain_min: string; max: number; domain_max: string; }; type SerpContentStatisticsResponse = { success: boolean; data: { statistics: { title: MetricStat; description: MetricStat; text2html_ratio: MetricStat; content: MetricStat; h1: MetricStat; h2: MetricStat; h3: MetricStat; }; extra_statistics: unknown[]; }; } export default SerpContentStatisticsResponse ``` ## Błędy Błędy tego endpointu przychodzą we [wspólnej kopercie ze statusem `418`](/types/errors). ## Powiązane akcje - [`urls/getList`](/modules/serp_analysis/serp-urls-getList) — lista wyników z analizą treści. --- # TOP100: utworzenie (`create`) > **Błąd:** > **Endpoint mutujący.** Zużywa jednostkę dziennego limitu narzędzi (`tools_daily_limit`) oraz limit wejściowy `top100_crawl_input_limit` (zależny od liczby fraz) i uruchamia crawl wyników TOP100. **`POST /api/tasks/management/top100_crawl/create`** Przykładowe żądanie: ```json { "keywords": [ "buty" ], "country_id": 1 } ``` Przykładowa odpowiedź: ```json { "success": true, "data": { "id": 4430394, "status": "waiting", "country_id": 1, "type": "top100_crawl", "data": { "keywords": [ "buty" ], "country_id": 1 }, "raw_request": { "keywords": [ "buty" ], "country_id": 1 }, "created": 1783251245, "completed": null } } ``` Tworzy **zadanie crawla TOP100** dla listy fraz i zwraca `id`, którego użyjesz w [`check`](/modules/serp_analysis/serp-tool-top100-check) oraz w raportach narzędzia ([getKeywords](/modules/serp_analysis/serp-tool-top100-getKeywords), [getDomains](/modules/serp_analysis/serp-tool-top100-getDomains), [getData](/modules/serp_analysis/serp-tool-top100-getData)). Bezpośrednio po utworzeniu `status = "waiting"` — raporty zwrócą dane po zakończeniu crawla (`completed`). --- ## Żądanie `POST` `/api/tasks/management/top100_crawl/create` ```jsonc filename="żądanie.jsonc" { "keywords": ["buty"], "country_id": 1 } ``` ### Parametry ```ts type Top100CreateRequest = { /** **Wymagane**. Lista fraz do zbadania (TOP100). */ keywords: string[]; /** **Wymagane**. ID kraju (1 = Polska). */ country_id: number; } export default Top100CreateRequest ``` ## Odpowiedź ```ts type Top100CreateResponse = { success: boolean; data: { id: number; status: string; country_id: number; type: string; data: { keywords: string[]; country_id: number }; raw_request: Record; created: number; completed: number | null; }; } export default Top100CreateResponse ``` ## Błędy Błędy tego endpointu przychodzą we [wspólnej kopercie ze statusem `418`](/types/errors). ## Powiązane akcje - [`top100_crawl/check`](/modules/serp_analysis/serp-tool-top100-check) — status crawla. - [Limity zapytań](/rate-limits) — `tools_daily_limit`. --- # TOP100: status (`check`) **`GET /api/tasks/management/top100_crawl/check`** Przykładowe żądanie: ```json { "task_id": null } ``` Przykładowa odpowiedź: ```json { "success": true, "data": { "status": "completed", "progress": { "all": 1, "finished": 1, "unfinished": 0 } } } ``` Zwraca **status i postęp** zadania crawla TOP100. Odpytuj (parametr `task_id` w query stringu), aż `status = "completed"` (albo `progress.finished == progress.all`) — dopiero wtedy raporty narzędzia zwrócą dane. --- ## Żądanie `GET` `/api/tasks/management/top100_crawl/check?task_id=4430394` ### Parametry (query string) ```ts type Top100CheckRequest = { /** **Wymagane**. ID zadania z create. */ task_id: number; } export default Top100CheckRequest ``` ## Odpowiedź ```ts type Top100CheckResponse = { success: boolean; data: { status: string; progress: { all: number; finished: number; unfinished: number }; }; } export default Top100CheckResponse ``` ## Błędy Błędy tego endpointu przychodzą we [wspólnej kopercie ze statusem `418`](/types/errors). ## Powiązane akcje - [`top100_crawl/create`](/modules/serp_analysis/serp-tool-top100-create) — utworzenie zadania. - [`top100_crawl/getData`](/modules/serp_analysis/serp-tool-top100-getData) — wyniki crawla. --- # TOP100: frazy (`getKeywords`) **`POST /api/serp_analysis/tools/top100_crawl/getKeywords`** Przykładowe żądanie: ```json { "task_id": null, "limit": 10 } ``` Przykładowa odpowiedź: ```json { "success": true, "data": [ { "keyword": "buty", "searches": 110000, "cpc_min": 0.71, "cpc_max": 2.47, "cpc_avg": 1.59, "snippets": [ "image_thumbs", "people_also_ask", "popular_products", "related_searches" ], "domains_number": "82" } ], "pagination": { "page_count": 1, "current_page": 1, "has_next_page": false, "has_prev_page": false, "count": 1, "limit": 10 } } ``` Zwraca **frazy zadania TOP100** z metrykami (wyszukiwania, CPC z zakresem) oraz liczbą domen w wynikach (`domains_number`) i typami snippetów. Wymaga gotowego zadania [`top100_crawl`](/modules/serp_analysis/serp-tool-top100-create). | Fraza | Wyszukiwania | CPC (śr.) | CPC min | CPC max | Liczba domen | | --- | --- | --- | --- | --- | --- | | buty | 110000 | 1.59 | 0.71 | 2.47 | 82 | _fraza buty, PL._ --- ## Żądanie `POST` `/api/serp_analysis/tools/top100_crawl/getKeywords` ```jsonc filename="żądanie.jsonc" { "task_id": null, "limit": 10, "page": 1 } ``` ### Parametry ```ts type Top100GetKeywordsRequest = { /** **Wymagane**. ID gotowego zadania top100_crawl. Realny `task_id` pobierzesz z `POST /api/tasks/management/top100_crawl/list` albo z odpowiedzi `create`. */ task_id: number; limit?: number; page?: number; } export default Top100GetKeywordsRequest ``` ## Odpowiedź ```ts type Top100GetKeywordsResponse = { success: boolean; data: Array<{ keyword: string; searches: number; cpc_min: number; cpc_max: number; cpc_avg: number; snippets: string[]; /** Liczba domen w wynikach (string). */ domains_number: string; }>; pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; limit: number; }; } export default Top100GetKeywordsResponse ``` ## Błędy Błędy tego endpointu przychodzą we [wspólnej kopercie ze statusem `418`](/types/errors). ## Powiązane akcje - [`top100_crawl/getDomains`](/modules/serp_analysis/serp-tool-top100-getDomains) — domeny w TOP100 dla frazy. - [`top100_crawl/getData`](/modules/serp_analysis/serp-tool-top100-getData) — pełne dane (fraza × URL). --- # TOP100: domeny (`getDomains`) **`POST /api/serp_analysis/tools/top100_crawl/getDomains`** Przykładowe żądanie: ```json { "task_id": null, "keyword": "buty", "limit": 10 } ``` Przykładowa odpowiedź: ```json { "success": true, "data": [ { "pos": 1, "domain": "butosklep.pl", "main_domain": "butosklep.pl", "url": "https://butosklep.pl/obuwie-damskie" }, { "pos": 2, "domain": "www.filippo.pl", "main_domain": "filippo.pl", "url": "https://www.filippo.pl/pl/menu/damskie-245" } ], "pagination": { "page_count": 17, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 82, "limit": 10 } } ``` Zwraca **domeny/URL-e w TOP100** dla wybranej frazy zadania (`keyword`) wraz z pozycją. Wymaga gotowego zadania [`top100_crawl`](/modules/serp_analysis/serp-tool-top100-create). | Pozycja | Domena | Domena główna | URL | | --- | --- | --- | --- | | 1 | butosklep.pl | butosklep.pl | https://butosklep.pl/obuwie-damskie | | 2 | www.filippo.pl | filippo.pl | https://www.filippo.pl/pl/menu/damskie-245 | _fraza buty, PL._ > **Informacja:** > `keyword` jest wymagane (jedna z fraz zadania). Wymaga `task_id` gotowego zadania. --- ## Żądanie `POST` `/api/serp_analysis/tools/top100_crawl/getDomains` ```jsonc filename="żądanie.jsonc" { "task_id": null, "keyword": "buty", "limit": 10, "page": 1 } ``` ### Parametry ```ts type Top100GetDomainsRequest = { /** **Wymagane**. ID gotowego zadania top100_crawl. Realny `task_id` pobierzesz z `POST /api/tasks/management/top100_crawl/list` albo z odpowiedzi `create`. */ task_id: number; /** **Wymagane**. Fraza (jedna z fraz zadania). */ keyword: string; limit?: number; page?: number; } export default Top100GetDomainsRequest ``` ## Odpowiedź ```ts type Top100GetDomainsResponse = { success: boolean; data: Array<{ pos: number; domain: string; main_domain: string; url: string; }>; pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; limit: number; }; } export default Top100GetDomainsResponse ``` ## Błędy Błędy tego endpointu przychodzą we [wspólnej kopercie ze statusem `418`](/types/errors). ## Powiązane akcje - [`top100_crawl/getKeywords`](/modules/serp_analysis/serp-tool-top100-getKeywords) — frazy zadania. - [`top100_crawl/getData`](/modules/serp_analysis/serp-tool-top100-getData) — pełne dane (fraza × URL). --- # TOP100: pełne dane (`getData`) **`POST /api/serp_analysis/tools/top100_crawl/getData`** Przykładowe żądanie: ```json { "task_id": null, "limit": 10 } ``` Przykładowa odpowiedź: ```json { "success": true, "data": [ { "keyword": "buty", "url": "https://butosklep.pl/obuwie-damskie", "searches": 110000, "cpc_min": 0.71, "cpc_max": 2.47, "cpc_avg": 1.59, "pos": 1, "domain": "butosklep.pl", "main_domain": "butosklep.pl", "snippets": [ "image_thumbs", "people_also_ask", "popular_products", "related_searches" ] } ], "pagination": { "page_count": 17, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 82, "limit": 10 } } ``` Zwraca **pełne wiersze `fraza × URL`** z zadania TOP100: pozycję w wynikach, domenę, metryki frazy (wyszukiwania, CPC) oraz typy snippetów. To najbardziej szczegółowy raport narzędzia — łączy dane fraz i domen. Wymaga gotowego zadania [`top100_crawl`](/modules/serp_analysis/serp-tool-top100-create). | Pozycja | Fraza | Domena | URL | Wyszukiwania | CPC (śr.) | | --- | --- | --- | --- | --- | --- | | 1 | buty | butosklep.pl | https://butosklep.pl/obuwie-damskie | 110000 | 1.59 | | 2 | buty | www.filippo.pl | https://www.filippo.pl/pl/menu/damskie-245 | 110000 | 1.59 | _fraza buty, PL._ --- ## Żądanie `POST` `/api/serp_analysis/tools/top100_crawl/getData` ```jsonc filename="żądanie.jsonc" { "task_id": null, "limit": 10, "page": 1 } ``` ### Parametry ```ts type Top100GetDataRequest = { /** **Wymagane**. ID gotowego zadania top100_crawl. Realny `task_id` pobierzesz z `POST /api/tasks/management/top100_crawl/list` albo z odpowiedzi `create`. */ task_id: number; limit?: number; page?: number; } export default Top100GetDataRequest ``` ## Odpowiedź ```ts type Top100GetDataResponse = { success: boolean; data: Array<{ keyword: string; url: string; searches: number; cpc_min: number; cpc_max: number; cpc_avg: number; pos: number; domain: string; main_domain: string; snippets: string[]; }>; pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; limit: number; }; } export default Top100GetDataResponse ``` ## Błędy Błędy tego endpointu przychodzą we [wspólnej kopercie ze statusem `418`](/types/errors). ## Powiązane akcje - [Eksport TOP100 (CSV)](/modules/serp_analysis/serp-export-top100) — eksport tych danych do pliku. - [`top100_crawl/getKeywords`](/modules/serp_analysis/serp-tool-top100-getKeywords) — widok fraz. --- # URL crawler: utworzenie (`create`) > **Błąd:** > **Endpoint mutujący.** Zużywa jednostkę dziennego limitu narzędzi (`tools_daily_limit`) oraz limit wejściowy `urls_crawls_tool_input_limit` (zależny od liczby URL-i) i uruchamia crawl treści wskazanych adresów. **`POST /api/tasks/management/urls_crawl/create`** Przykładowe żądanie: ```json { "urls": [ "senuto.com" ], "country_id": 1 } ``` Przykładowa odpowiedź: ```json { "success": true, "data": { "id": 4430395, "status": "waiting", "country_id": 1, "type": "urls_crawl", "data": { "urls": [ "senuto.com" ] }, "raw_request": { "urls": [ "senuto.com" ], "country_id": 1 }, "created": 1783251369, "completed": null } } ``` Tworzy **zadanie crawla treści** wskazanych adresów URL i zwraca `id`, którego użyjesz w [`check`](/modules/serp_analysis/serp-tool-urlscrawl-check) oraz w [`getList`](/modules/serp_analysis/serp-tool-urlscrawl-getList). Crawler pobiera tytuł, opis, nagłówki, długość treści i linki każdej strony. --- ## Żądanie `POST` `/api/tasks/management/urls_crawl/create` ```jsonc filename="żądanie.jsonc" { "urls": ["senuto.com"], "country_id": 1 } ``` ### Parametry ```ts type UrlsCrawlCreateRequest = { /** **Wymagane**. Lista adresów URL do zcrawlowania. */ urls: string[]; /** **Wymagane**. ID kraju (1 = Polska). */ country_id: number; } export default UrlsCrawlCreateRequest ``` ## Odpowiedź ```ts type UrlsCrawlCreateResponse = { success: boolean; data: { id: number; status: string; country_id: number; type: string; data: { urls: string[] }; raw_request: Record; created: number; completed: number | null; }; } export default UrlsCrawlCreateResponse ``` ## Błędy Błędy tego endpointu przychodzą we [wspólnej kopercie ze statusem `418`](/types/errors). ## Powiązane akcje - [`urls_crawl/check`](/modules/serp_analysis/serp-tool-urlscrawl-check) — status crawla. - [`urls_crawl/getList`](/modules/serp_analysis/serp-tool-urlscrawl-getList) — wyniki crawla. --- # URL crawler: status (`check`) **`GET /api/tasks/management/urls_crawl/check`** Przykładowe żądanie: ```json { "task_id": null } ``` Przykładowa odpowiedź: ```json { "success": true, "data": { "id": 2429373, "status": "completed", "type": "urls_crawl", "progress": { "all": 2, "finished": 2, "unfinished": 0 } } } ``` Zwraca **status i postęp** zadania crawla URL-i. Odpytuj (parametr `task_id` w query stringu), aż `status = "completed"` — wtedy [`getList`](/modules/serp_analysis/serp-tool-urlscrawl-getList) zwróci dane. --- ## Żądanie `GET` `/api/tasks/management/urls_crawl/check?task_id=2429373` ### Parametry (query string) ```ts type UrlsCrawlCheckRequest = { /** **Wymagane**. ID zadania urls_crawl z create. */ task_id: number; } export default UrlsCrawlCheckRequest ``` ## Odpowiedź ```ts type UrlsCrawlCheckResponse = { success: boolean; data: { id: number; status: string; type: string; progress: { all: number; finished: number; unfinished: number }; }; } export default UrlsCrawlCheckResponse ``` ## Błędy Błędy tego endpointu przychodzą we [wspólnej kopercie ze statusem `418`](/types/errors). ## Powiązane akcje - [`urls_crawl/create`](/modules/serp_analysis/serp-tool-urlscrawl-create) — utworzenie zadania. - [`urls_crawl/getList`](/modules/serp_analysis/serp-tool-urlscrawl-getList) — wyniki crawla. --- # URL crawler: wyniki (`getList`) **`POST /api/serp_analysis/tools/urls_crawl/getList`** Przykładowe żądanie: ```json { "task_id": null, "limit": 10 } ``` Przykładowa odpowiedź: ```json { "success": true, "data": [ { "url": "pies.pl", "title": "Pies domowy – opis, występowanie i zdjęcia...", "title_length": 75, "description": "Trudno o bardziej różnorodny gatunek niż pies domowy...", "description_length": 377, "h1": [ "Pies domowy – opis, występowanie i zdjęcia..." ], "h1_count": 1, "h2": [ "Jak pies został przyjacielem człowieka?" ], "h2_count": 7, "content_length": 11611, "external_links": [], "external_links_count": 0, "internal_links": [], "internal_links_count": 0 } ], "pagination": { "page_count": 1, "current_page": 1, "has_next_page": false, "has_prev_page": false, "count": 2, "limit": 10 } } ``` Zwraca **zcrawlowaną treść** wskazanych adresów URL: tytuł i opis (z długościami), nagłówki `h1`/`h2` (wartości i liczność), długość treści oraz linki wewnętrzne i zewnętrzne (z liczbą). Wymaga gotowego zadania [`urls_crawl`](/modules/serp_analysis/serp-tool-urlscrawl-create). | URL | Tytuł | Dł. tytułu | Dł. opisu | H1 | H2 | Dł. treści | Linki wewn. | Linki zewn. | | --- | --- | --- | --- | --- | --- | --- | --- | --- | | pies.pl | Pies domowy – opis, występowanie i zdjęcia. Zwierzę pies domowy ciekawostki | 75 | 377 | 1 | 7 | 11611 | 0 | 0 | _Wiersze zawierają pełne tablice h1/h2/linków w JSON._ > **Informacja:** > `external_links` i `internal_links` to tablice obiektów `{ value: url }`. Wymaga `task_id` gotowego zadania. --- ## Żądanie `POST` `/api/serp_analysis/tools/urls_crawl/getList` ```jsonc filename="żądanie.jsonc" { "task_id": null, "limit": 10, "page": 1 } ``` ### Parametry ```ts type UrlsCrawlGetListRequest = { /** **Wymagane**. ID gotowego zadania urls_crawl. Realny `task_id` zwraca `create` — zapisz go po swojej stronie. */ task_id: number; limit?: number; page?: number; } export default UrlsCrawlGetListRequest ``` ## Odpowiedź ```ts type UrlsCrawlGetListResponse = { success: boolean; data: Array<{ url: string; title: string; title_length: number; description: string; description_length: number; h1: string[]; h1_count: number; h2: string[]; h2_count: number; content_length: number; external_links: Array<{ value: string }>; external_links_count: number; internal_links: Array<{ value: string }>; internal_links_count: number; }>; pagination: { page_count: number; current_page: number; has_next_page: boolean; has_prev_page: boolean; count: number; limit: number; }; } export default UrlsCrawlGetListResponse ``` ## Błędy Błędy tego endpointu przychodzą we [wspólnej kopercie ze statusem `418`](/types/errors). ## Powiązane akcje - [Eksport URL crawler (CSV)](/modules/serp_analysis/serp-export-urlscrawl) — eksport tych danych do pliku. - [`urls_crawl/create`](/modules/serp_analysis/serp-tool-urlscrawl-create) — utworzenie zadania. --- # Eksport: TOP100 (CSV) (`getData`) > **Błąd:** > **Endpoint mutujący, zwraca PLIK CSV.** Zużywa jednostkę `tools_daily_limit`. Odpowiedzią jest plik do pobrania (nagłówki `Content-Disposition`), a **nie** JSON — nie testuj go w playgroundzie API. Eksportuje do pliku **CSV** te same dane, które zwraca [`top100_crawl/getData`](/modules/serp_analysis/serp-tool-top100-getData) (wszystkie wiersze, bez paginacji). Wymaga gotowego zadania `top100_crawl`. ## Żądanie `POST` `/api/serp_analysis/tools/exports/top100_crawl/getData` ```jsonc filename="żądanie.jsonc" { "task_id": null } ``` ### Parametry ```ts type SerpExportTop100Request = { /** **Wymagane**. ID gotowego zadania top100_crawl. */ task_id: number; } export default SerpExportTop100Request ``` ## Kolumny pliku CSV | Kolumna | Pole źródłowe | | --------------- | ----------------------------------------- | | Fraza | `keyword` | | Pozycja | `pos` | | URL | `url` | | Domena | `domain` | | Wyszukiwania | `searches` | | CPC | `cpc_avg` | | Snippety | `snippets` (łączone znakami nowej linii) | | Nazwy snippetów | `name_snippets` (przetłumaczone etykiety) | ## Błędy Błędy tego endpointu przychodzą we [wspólnej kopercie ze statusem `418`](/types/errors). ## Powiązane akcje - [`top100_crawl/getData`](/modules/serp_analysis/serp-tool-top100-getData) — wersja JSON (z paginacją). - [Limity zapytań](/rate-limits) — `tools_daily_limit`. --- # Eksport: URL crawler (CSV) (`getList`) > **Błąd:** > **Endpoint mutujący, zwraca PLIK CSV.** Zużywa jednostkę `tools_daily_limit`. Odpowiedzią jest plik do pobrania, a **nie** JSON — nie testuj go w playgroundzie API. Eksportuje do pliku **CSV** te same dane, które zwraca [`urls_crawl/getList`](/modules/serp_analysis/serp-tool-urlscrawl-getList) (wszystkie wiersze, bez paginacji). Listy nagłówków i linków są w komórkach łączone znakami nowej linii. Wymaga gotowego zadania `urls_crawl`. ## Żądanie `POST` `/api/serp_analysis/tools/exports/urls_crawl/getList` ```jsonc filename="żądanie.jsonc" { "task_id": null } ``` ### Parametry ```ts type SerpExportUrlsCrawlRequest = { /** **Wymagane**. ID gotowego zadania urls_crawl. */ task_id: number; } export default SerpExportUrlsCrawlRequest ``` ## Kolumny pliku CSV | Kolumna | Pole źródłowe | | ------------------- | -------------------------- | | URL | `url` | | Tytuł | `title` | | Długość tytułu | `title_length` | | Opis | `description` | | Nagłówki H1 | `h1` (łączone) | | Liczba H1 | `h1_count` | | Nagłówki H2 | `h2` (łączone) | | Liczba H2 | `h2_count` | | Długość treści | `content_length` | | Linki zewnętrzne | `external_links` (łączone) | | Liczba linków zewn. | `external_links_count` | | Linki wewnętrzne | `internal_links` (łączone) | | Liczba linków wewn. | `internal_links_count` | ## Błędy Błędy tego endpointu przychodzą we [wspólnej kopercie ze statusem `418`](/types/errors). ## Powiązane akcje - [`urls_crawl/getList`](/modules/serp_analysis/serp-tool-urlscrawl-getList) — wersja JSON (z paginacją). - [Limity zapytań](/rate-limits) — `tools_daily_limit`. --- # Użytkownik Endpointy istotne przy **korzystaniu z API**: tożsamość konta, plan i dostęp do modułów. Wymagają tokena Bearer i służą do diagnostyki dostępu, zanim odpytasz właściwe moduły — np. sprawdzenia, do których modułów masz dostęp, by uniknąć `402`. Wspólne mechanizmy: [Błędy i status `418`](/types/errors) · [Limity zapytań](/rate-limits). ## Zakres - **Tożsamość, plan i dostęp do modułów:** [`getLoggedUser`](/modules/user/user-getLoggedUser) (`/api/users/getLoggedUser`) — profil konta, plan oraz mapa `access` z modułami, do których masz dostęp (pozwala uniknąć `402`). - **Stan limitów konta:** [`GET /api/users/getLimits`](/rate-limits#podgląd-limitów-przez-api) — `limit`, `usage` i `hours_to_reset` dla wszystkich pul naraz. > **Informacja:** > Uwierzytelnianie (pozyskanie tokena Bearer) opisuje strona [Autoryzacja](/authorization). --- # Profil konta (`getLoggedUser`) **`GET /api/users/getLoggedUser`** Zwraca **profil zalogowanego konta** — identyfikator, adres e-mail, język i walutę, aktualny plan wraz z datą wygaśnięcia oraz mapę dostępu do modułów (`access`). Przydatny na starcie integracji: pozwala jednym żądaniem ustalić, czym konto dysponuje, zanim wywołasz raporty — a zarazem jest najprostszym sprawdzeniem, czy token działa. --- ## Żądanie `GET` `/api/users/getLoggedUser` Nagłówki: `Authorization: Bearer `. Endpoint nie przyjmuje parametrów. **Żądanie** ```jsonc filename="żądanie.jsonc" {} ``` **cURL** ```bash curl --location --request GET 'https://api.senuto.com/api/users/getLoggedUser' \ --header 'Authorization: Bearer $YOUR_TOKEN_HERE' ``` ### Parametry Endpoint nie przyjmuje parametrów — działa w kontekście tokena z nagłówka `Authorization`. > **Ostrzeżenie:** > Odpowiedź zawiera **dane osobowe i rozliczeniowe konta** (adres e-mail, telefon, adres IP ostatniego logowania, identyfikatory planu). Traktuj ją jak dane wrażliwe — nie loguj całej odpowiedzi i nie przekazuj jej do systemów trzecich. > **Ostrzeżenie:** > Zestaw pól jest szerszy niż udokumentowany powyżej i **może się zmieniać** — czytaj po nazwach pól, nie po pozycji, i nie waliduj odpowiedzi zamkniętym schematem. ## Odpowiedź Przykład to **rzeczywista odpowiedź produkcyjna**, skrócona. ```json filename="przykładowa-odpowiedź" { "success": true, "data": { "id": 1234, "email": "user@example.com", "account_id": 1234, "parent_id": null, "country_id": 1, "lang": "pl-PL", "currency": "PLN", "status": 1, "plan_id": 49, "product_id": 49, "is_trial": false, "is_expired": false, "expiration": 1801909285, "sys_last_plan_length": "1y", "is_enterprise": false, "access": { "visibility_analysis_ranking": true, "serp_analysis": true } } } ``` ### Struktura odpowiedzi ```ts type GetLoggedUserResponse = { success: boolean; /** * Profil konta. Odpowiedź zawiera **cały rekord użytkownika** wraz z ustawieniami * i danymi rozliczeniowymi — poniżej pola przydatne przy integracji. Hasło nie jest * zwracane. Zestaw pól może się rozszerzać, więc nie zakładaj zamkniętej listy. */ data: { id: number; email: string; /** Konto nadrzędne (subkonta) — `null` dla konta głównego. */ parent_id: number | null; account_id: number; country_id: number; /** Język interfejsu, np. `pl-PL`. */ lang: string; currency: string; /** Status konta (1 = potwierdzone). */ status: number; plan_id: number | null; product_id: number | null; is_trial: boolean | null; is_expired: boolean; /** Znacznik czasu wygaśnięcia planu (Unix) albo `null`. */ expiration: number | null; /** Długość ostatniego planu, np. `12m`. */ sys_last_plan_length: string | null; is_enterprise: boolean; /** Mapa dostępu do modułów — klucz modułu → czy konto ma dostęp. */ access: Record; }; } export default GetLoggedUserResponse ``` ## Błędy Błędy tego endpointu przychodzą we [wspólnej kopercie ze statusem `418`](/types/errors). ## Powiązane akcje - [Limity zapytań](/rate-limits) — `GET /api/users/getLimits` zwraca stan wszystkich limitów konta. --- --- title: Typy sidebarTitle: Typy asIndexPage: true ----------------- # Typy Wspólne struktury i mechanizmy powtarzające się w wielu endpointach API. - [**Filter**](/types/filter) — parametr `filtering` zawężający zwracane wiersze (klucze zależą od endpointu). - [**Paginacja**](/types/pagination) — parametry `limit`/`page` oraz koperta `pagination` w odpowiedzi. - [**`fetch_mode`**](/types/fetch-mode) — zakres dopasowania domeny (host, subdomeny, katalog, URL) w raportach Analizy widoczności. - [**Błędy**](/types/errors) — koperta błędu, znaczenie statusu `418`, kategorie `type` i typowe pułapki. --- --- title: Filtry sidebarTitle: 🍒 Filtry asIndexPage: true ----------------- # `Filter` **`POST /api/visibility_analysis/reports/positions/getData`** Przykładowe żądanie: ```json { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "limit": 3, "filtering": [ { "filters": [ { "key": "statistics.position.current", "match": "lte", "value": 10 }, { "key": "keywords", "items": [ { "value": "buty", "match": "contain" } ] } ] } ] } ``` Przykładowe żądanie (rozszerzone): ```json { "domain": "zalando.pl", "fetch_mode": "topLevelDomain", "limit": 3, "filtering": [ { "filters": [ { "key": "statistics.visibility.current", "match": "gte", "value": 1000 }, { "key": "keywords", "items": [ { "value": "buty", "match": "startsWith" } ] } ] } ] } ``` Przykładowa odpowiedź: ```json { "success": true, "data": [ { "keyword_id": 270, "kid": "0000ec39a1698499de57f5d8979cb626", "domain": "zalando.pl", "keyword": "białe buty komunijne", "words_count": 3, "statistics": { "position": { "current": 7, "previous": 7, "diff": 0 }, "visibility": { "current": 1240, "previous": 1240, "diff": 0, "percent": 0 }, "url": { "current": "zalando.pl/dzieci-home/", "previous": "zalando.pl/dzieci-home/", "is_change": 0 }, "cpc": { "current": 0.71 }, "searches": { "current": 210 } } } ], "pagination": { "page_count": 1, "current_page": 1, "has_next_page": false, "has_prev_page": false, "count": 1, "limit": 3 } } ``` Wiele endpointów raportowych przyjmuje opcjonalny parametr **`filtering`**, który zawęża zwracane wiersze. To ten sam mechanizm, co pole **Filtry** nad tabelami w aplikacji Senuto. > **Informacja:** > Dostępne klucze filtrów różnią się per endpoint. Pełną listę znajdziesz na stronie danego endpointu, np. [Pozycje → Filtrowanie](/modules/visibility_analysis/positions#filtrowanie). ## Struktura `filtering` to **tablica grup**. Każda grupa ma klucz `filters` — listę warunków. Warunki w obrębie jednej grupy łączone są operatorem **AND**. ```jsonc filename="filtering.jsonc" "filtering": [ { "filters": [ // filtr wartości (liczbowy / logiczny): { key, match, value } { "key": "statistics.position.current", "match": "lte", "value": 10 }, // filtr tekstowy (frazy/URL): { key, items: [{ value, match }] } { "key": "keywords", "items": [{ "value": "buty", "match": "contain" }] } ] } ] ``` > **Uwaga:** > Aplikacja dodatkowo dołącza do każdego filtra pola `type` (np. `"string"`) oraz `filterSourceType` (np. `"customFilter"`). Są one **opcjonalne** — API działa również bez nich. ## Typy filtrów ### Filtr wartości — [`FilterNumeric`](/types/filter/FilterNumeric) Dla pól liczbowych i logicznych. Kształt: `{ key, match, value }`. | `match` | Znaczenie | | ------- | ------------------ | | `eq` | równe | | `gt` | większe niż | | `gte` | większe lub równe | | `lt` | mniejsze niż | | `lte` | mniejsze lub równe | ### Filtr tekstowy — [`FilterKeyword`](/types/filter/FilterKeyword) Dla fraz i pól tekstowych. Kształt: `{ key, items: [{ value, match }] }` (wiele `items` rozszerza dopasowanie). | `match` | Znaczenie | | -------------- | -------------- | | `contain` | zawiera frazę | | `containsWord` | zawiera słowo | | `startsWith` | zaczyna się od | | `endsWith` | kończy się na | | `notContain` | nie zawiera | ### Filtr przynależności — [`FilterInclusion`](/types/filter/FilterInclusion) Dla pól typu lista/zbiór (np. typy snippetów, intencje) — sprawdza przynależność wartości do dozwolonego zbioru. ## Piaskownica Przetestuj filtrowanie na realnym endpointcie pozycji (wklej swój token w pasku **API token**): > **Informacja:** > Jak filtry zawężają wynik — `positions/getData` dla `zalando.pl` filtr `statistics.position.current ≤ 3` zawęża `count` z 291 121 do 46 546; filtr `keywords contain "buty"` → 20 333; oba w jednej grupie (AND) → 7 952. ## Gdzie działa Filtrowanie wspierają m.in.: - [Analiza widoczności — Pozycje](/modules/visibility_analysis/positions#filtrowanie) (pełny zestaw kluczy), - [Analiza widoczności — Historia fraz](/modules/visibility_analysis/va-history-keywords#filtrowanie) (te same klucze), - [Monitoring — Pozycje (getData)](/modules/rank_tracker/rt-positions-getData#filtrowanie) (filtr `keywords`). Zestaw dostępnych kluczy jest specyficzny dla endpointu — sprawdzaj sekcję **Filtrowanie** na jego stronie. --- # `FilterInclusion` `FilterInclusion` służy do filtrowania danych zwracanych przez API. na przykład: ```jsonc filename=" " { "snippets_va.current in ['city' and 'brand']" } ``` --- # `FilterKeyword` `FilterKeyword` służy do filtrowania danych zwracanych przez API. na przykład: ```jsonc filename=" " { "keywords contains 'ciasto'", "keywords starts_with 'i'", "keywords ends_with 'e'" } ``` --- # `FilterNumeric` `FilterNumeric` służy do filtrowania danych zwracanych przez API. na przykład: ```jsonc filename=" " { "position >= 3" } ``` --- --- title: Paginacja sidebarTitle: Paginacja ----------------------- # Paginacja Endpointy zwracające listę (tablicę w polu `data`) stronicują wyniki. Rozmiar strony i numer strony sterujesz parametrami żądania **`limit`** i **`page`**, a metadane bieżącego wycinka znajdziesz w polu **`pagination`** odpowiedzi. > **Informacja:** > Koperta paginacji jest **spójna w całym API** i została potwierdzona na żywym produkcyjnym API (`api.senuto.com`) na kilkudziesięciu endpointach. Akcje zwracające pojedynczy obiekt (np. dashboardy, wykresy) **nie zawierają** pola `pagination`. ## Parametry żądania ```ts type PaginationParams = { /** * Rozmiar strony — liczba wierszy w `data`. Nieujemna liczba całkowita. * Maksimum zależy od endpointu (często `maxLimit = 100`). * @default 10 */ limit?: number; /** * Numer strony (liczony od 1). Nieujemna liczba całkowita. * @default 1 */ page?: number; } export default PaginationParams ``` > **Ostrzeżenie:** > `limit` i `page` są opcjonalne, ale walidowane — np. przy wartościach spoza dozwolonego zakresu endpoint zwróci `418`. W raportach Bazy słów kluczowych oba pola bywają przyjmowane, lecz **bez efektu** (akcja zwraca pojedynczy obiekt) — sprawdzaj stronę konkretnego endpointu. ## Pole `pagination` w odpowiedzi ```ts type Pagination = { /** Łączna liczba stron dla bieżących kryteriów */ page_count: number; /** Numer bieżącej strony */ current_page: number; /** Czy istnieje kolejna strona */ has_next_page: boolean; /** Czy istnieje poprzednia strona */ has_prev_page: boolean; /** Łączna liczba wierszy spełniających kryteria (przed stronicowaniem) */ count: number; /** Rozmiar strony użyty w tym żądaniu */ limit: number; } export default Pagination ``` > **Uwaga:** > **Pułapki typów.** W części endpointów `pagination.count` bywa zwracane jako **string** (np. `"94"`), a nie liczba — np. w Monitoringu (`rank_tracker`). Rzutuj wartość po stronie klienta. Sporadycznie `page_count` bywa niespójne przy `count: 0` (np. `1` zamiast `0`) — traktuj `has_next_page` jako źródło prawdy o kolejnej stronie. ## Przykład ```json filename="fragment odpowiedzi" { "success": true, "data": [ /* wiersze bieżącej strony */ ], "pagination": { "page_count": 97041, "current_page": 1, "has_next_page": true, "has_prev_page": false, "count": 291121, "limit": 10 } } ``` ## Iterowanie po wszystkich stronach Zwiększaj `page` aż `has_next_page` będzie `false`. Wzorzec: ```jsonc filename="pętla stronicowania (pseudokod)" // page = 1 // dopóki true: // wyślij żądanie z { ...params, page, limit } // przetwórz odpowiedź.data // jeśli odpowiedź.pagination.has_next_page == false → przerwij // page += 1 ``` > **Informacja:** > Aby ograniczyć liczbę żądań, ustaw `limit` na maksimum wspierane przez endpoint (często `100`). Zwróć uwagę na limity konta — patrz [Limity zapytań](/rate-limits). --- --- title: fetch\_mode sidebarTitle: fetch\_mode ------------------------- # `fetch_mode` — zakres dopasowania domeny Raporty Analizy widoczności (i część narzędzi) przyjmują parę **`domain` + `fetch_mode`**. `fetch_mode` mówi, **co dokładnie uznajemy za „tę domenę"**: sam host, całą domenę z subdomenami, katalog czy pojedynczy adres. Zły tryb to najczęstsza przyczyna odpowiedzi `200` z zerami — patrz [Błędy → puste dane](/types/errors#odpowiedź-200-ale-dane-są-puste-lub-zerowe). ```ts type FetchMode = { /** * Zakres dopasowania dla pola `domain`: * - `topLevelDomain` — dokładnie ten host, bez subdomen * - `subdomain` — cała domena wraz ze wszystkimi subdomenami * - `catalog` — wskazany katalog i wszystko poniżej niego * - `url` — dokładnie jeden adres */ fetch_mode: "topLevelDomain" | "subdomain" | "catalog" | "url"; } export default FetchMode ``` > **Błąd:** > Wartość **`domain` nie istnieje** — mimo że tak podpowiada intuicja. Podanie jej kończy się `418` > (`invalid_data`, `fetch_mode`). Odpowiednikiem „całej domeny" jest `subdomain`, a „samej domeny > głównej" — `topLevelDomain`. ## Czym różnią się tryby — na liczbach Ten sam raport (`dashboard/getDomainStatistics`, liczba fraz w TOP10, `country_id` domyślne), odpytany dla trzech wariantów tej samej domeny: | `domain` | `topLevelDomain` | `subdomain` | | ------------------ | ---------------- | ----------- | | `pl.wikipedia.org` | 2 592 633 | 4 907 326 | | `en.wikipedia.org` | 1 997 920 | 4 907 326 | | `wikipedia.org` | 418 | 4 907 326 | Czyta się to tak: - **`topLevelDomain` dopasowuje dokładnie ten host.** `pl.wikipedia.org` i `en.wikipedia.org` to dla tego trybu dwa różne byty, a `wikipedia.org` (bez subdomeny) prawie nic nie rankuje samodzielnie — stąd 418 fraz. - **`subdomain` agreguje całą domenę** wraz ze wszystkimi subdomenami — dlatego wszystkie trzy warianty zwracają tę samą wartość. **Nie zawęża wyniku do wskazanej subdomeny**: `blog.example.com` w tym trybie zwróci dane całego `example.com`. > **Ostrzeżenie:** > **Projekt na subdomenie.** Jeśli interesuje Cię wyłącznie `blog.example.com`, żaden z tych dwóch trybów > nie zrobi tego wprost: `topLevelDomain` policzy tylko frazy, na których rankuje sam host `blog.example.com`, > a `subdomain` policzy cały `example.com`. Dla „katalogu w obrębie domeny" użyj `catalog`. ## `catalog` i `url` | `domain` | `fetch_mode` | Frazy w TOP10 | | ------------------------------------ | ------------ | ------------- | | `senuto.com/pl/blog` | `catalog` | 244 | | `https://www.senuto.com/pl/blog/` | `catalog` | 244 | | `senuto.com/pl/blog/slowa-kluczowe/` | `url` | 13 | - **`catalog`** obejmuje wskazaną ścieżkę **i wszystko poniżej niej**. - **`url`** to dokładnie jeden adres — bez podstron. - Zapis jest tolerancyjny: schemat (`https://`), prefiks `www.` i końcowy ukośnik nie zmieniają wyniku. ## Prefiks `www` `www.example.com` i `example.com` to **ta sama domena** — prefiks jest normalizowany w każdym trybie. Sprawdzone na `zalando.pl`: identyczne wartości dla `zalando.pl` i `www.zalando.pl` zarówno w `topLevelDomain`, jak i `subdomain`. ## To samo w aplikacji — pole „Dopasowanie" W aplikacji Senuto ten sam wybór robisz listą **Dopasowanie** nad raportem. Mapowanie (sprawdzone w aplikacji 2026-08-12; panel wysyła te wartości w adresie, np. `…?domain=zalando.pl&fetch_mode=subdomain&country_id=200`): | Aplikacja — „Dopasowanie" | Opis w aplikacji | `fetch_mode` w API | | ------------------------------ | ------------------------------------------------ | ------------------ | | `*.domena.pl/*` **(domyślne)** | „Wprowadzona domena i wszystkie jej subdomeny" | `subdomain` | | `domena.pl/*` | „Tylko wprowadzona domena (bez subdomen)" | `topLevelDomain` | | `domena.pl/katalog/*` | „Tylko katalog domeny i należące do niego URL-e" | `catalog` | | `Adres URL` | „Tylko wprowadzony adres URL" | `url` | > **Ostrzeżenie:** > **Dlaczego API pokazuje inne liczby niż aplikacja.** To zwykle nie błąd, a różnica domyślnych ustawień: > aplikacja startuje z `*.domena.pl/*` (czyli `subdomain`) **i bazą Polska 2.0** (`country_id: 200`), a API > nie ma domyślnego `fetch_mode` (musisz go podać) i bez `country_id` liczy na bazie Polska 1.0. > Chcąc odtworzyć liczbę z aplikacji, wyślij `fetch_mode: "subdomain"` **i** `country_id: 200`. ## Wybór trybu — ściąga | Chcesz zobaczyć | `domain` | `fetch_mode` | | ------------------------------------------------------- | ----------------------- | ---------------- | | Widoczność serwisu razem z subdomenami | `example.com` | `subdomain` | | Widoczność samego `example.com`, bez subdomen | `example.com` | `topLevelDomain` | | Widoczność konkretnego hosta (np. sklepu na subdomenie) | `sklep.example.com` | `topLevelDomain` | | Widoczność sekcji serwisu (blog, kategoria) | `example.com/blog` | `catalog` | | Widoczność jednego artykułu | `example.com/blog/wpis` | `url` | > **Informacja:** > Definicja trybów żyje w `DataFetchMode` po stronie API — te same cztery wartości obowiązują we > wszystkich raportach, które przyjmują `fetch_mode`. Jeśli endpoint go wymaga, jest to zaznaczone > na jego stronie; pominięcie pola zwraca `418` z `params.fetch_mode._required`. --- --- title: Błędy sidebarTitle: Błędy ------------------- # Błędy Gdy żądanie się nie powiedzie, API zwraca kopertę z `success: false` i obiektem `error`. Struktura jest **spójna w całym API**. ## Koperta błędu ```ts type ErrorResponse = { /** Zawsze `false` dla odpowiedzi błędnej */ success: false; data: { error: { /** Kategoria błędu — np. invalid_data, invalid_filtering, unreachable_data, timeout, database, unknown */ type: string; /** Czytelny komunikat */ message: string; /** * Tylko dla `invalid_data`: mapa pole → reguła → komunikat. * Np. `{ "fetch_mode": { "_required": "This field is required" } }` */ params?: Record>; }; }; } export default ErrorResponse ``` ## Status HTTP `418` — najważniejsze > **Błąd:** > Status **`418`** to w tym API **generyczny błąd walidacji żądania**, a **nie** wyłącznie przekroczenie limitu zapytań. Zdecydowana większość `418` to `type: "invalid_data"` — brakujące lub nieprawidłowe pole. Sprawdzaj `data.error.type` i `data.error.params`, zanim uznasz błąd za rate limit. Typowe `418` (`invalid_data`) — brak wymaganego pola: ```json filename="brak fetch_mode" { "success": false, "data": { "error": { "type": "invalid_data", "message": "Given data is invalid, please check the documentation or contact Administrator", "params": { "fetch_mode": { "_required": "This field is required" } } } } } ``` ## Status HTTP `404` — trzy różne przyczyny `404` w tym API **nie** oznacza po prostu „zły adres". Rozróżnisz przypadki po tym, co przyszło w treści: | Treść odpowiedzi | Znaczenie | Co zrobić | | ------------------------------------------------ | --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | | `{"success": false, "message": ""}` | Trasa nie istnieje **albo** żądanie jest nieuwierzytelnione (wtedy poprzedza je `302` na `…/users/login`) | Sprawdź pisownię ścieżki (niżej), a potem token | | Strona **HTML** z nagłówkiem „Error / Not Found" | Trasa istnieje, ale wskazany zasób nie — albo akcja w ogóle nie dostała identyfikatora | Sprawdź, czy parametr idzie tam, gdzie trzeba (query string vs ciało) i czy zasób należy do Twojego konta | > **Błąd:** > **Nieważny lub brakujący token daje `302`, a po przekierowaniu `404` — nie `401`.** Żądanie bez > nagłówka `Authorization` oraz z tokenem po terminie ważności dostaje `302` z nagłówkiem `Location` > na `/api//users/login`, a pod tym adresem czeka `404 {"success": false, "message": ""}` — > dokładnie to samo, co po literówce w ścieżce. Klient podążający za przekierowaniami (`curl -L`, > `requests`, `axios`) pokaże `404`; Postman z wyłączonym podążaniem zatrzyma się na `302`. > Zanim zaczniesz debugować ścieżkę, pobierz świeży token (patrz [Autoryzacja](/authorization)). > Komunikat jest pusty celowo, produkcyjne API nie ujawnia szczegółów wyjątku. ### Konwencja adresów — najczęstsza przyczyna `404` Ścieżka ma postać `/api////`, przy czym **człony i akcja rządzą się różnymi regułami**: - **Moduł, sekcja i kontroler:** `snake_case` — `visibility_analysis`, `ai_overviews`, `serp_analysis`. - **Akcja:** `camelCase` — `getDomainStatistics`, `getKeywordHistory`. **Zamiana akcji na `snake_case` albo `kebab-case` daje `404`**, mimo że endpoint istnieje. - **Myślnik zamiast podkreślenia w module** (`visibility-analysis`) też kończy się `404`. - **Pominięty człon sekcji** (`/api/visibility_analysis/dashboard/…` zamiast `…/reports/dashboard/…`) — `404`. ```text filename="ta sama akcja, cztery adresy" /api/visibility_analysis/reports/dashboard/getDomainStatistics → 200 /api/visibility_analysis/reports/dashboard/get_domain_statistics → 404 akcja w snake_case /api/visibility-analysis/reports/dashboard/getDomainStatistics → 404 myślnik w module /api/visibility_analysis/dashboard/getDomainStatistics → 404 brak członu "reports" ``` > **Informacja:** > Sama **wielkość liter w nazwie akcji** akurat nie ma znaczenia (`getdomainstatistics` też odpowie `200`) — > decyduje brak separatorów. Nie opieraj się jednak na tym: trzymaj się pisowni podanej na stronie endpointu. ## Kategorie błędów (`type`) | `type` | Znaczenie | Częsta przyczyna | | -------------------------------------- | ---------------------------------- | ----------------------------------------------------------------------------------------------- | | `invalid_data` | Żądanie nie przeszło walidacji | Brak wymaganego pola, zła wartość (np. nieznany `fetch_mode`, `country_id`, `aggregation_type`) | | `invalid_filtering` | Nieprawidłowy parametr `filtering` | Klucz filtra nieobsługiwany przez ten endpoint | | `unauthorized` / `Unauthorized access` | Brak dostępu do zasobu | Cudzy lub nieistniejący `project_id` / `group_id` / `keyword_id` | | `unreachable_data` | Dane chwilowo niedostępne | Problem po stronie źródła danych | | `timeout` | Przekroczono czas przetwarzania | Bardzo szeroki zakres / duża domena | | `database` | Błąd warstwy danych | Bywa zwracany także jako `HTTP 500` z surową stroną HTML (patrz niżej) | ## Częste pułapki > **Ostrzeżenie:** > **Cudzy/nieistniejący zasób → `418 Unauthorized access`, nie `404`.** Podanie `project_id` lub `group_id` spoza konta zwraca błąd walidacji dostępu, a nie „nie znaleziono”. Konto administratora (`role_id = 1`) omija część kontroli dostępu. > **Ostrzeżenie:** > **Zła metoda HTTP.** Część akcji akceptuje wyłącznie `GET` (parametry w query stringu) — wysłanie ich metodą `POST` w ciele JSON zwraca `405` (Method Not Allowed) albo `418`, bo walidator widzi puste pola. Metoda jest podana na stronie każdego endpointu. > **Ostrzeżenie:** > **Konto bez dostępu do API → `403`, nie `418`.** Ważny token na koncie bez aktywnego planu albo bez > dodatku Dostęp do API daje `403 {"success": false, "message": ""}`. Treść jest pusta, więc ten przypadek > rozpoznajesz po samym statusie (patrz [Dostęp do API](/access)). > **Ostrzeżenie:** > **`HTTP 500` z surową stroną HTML.** Niektóre endpointy zwracają obecnie nieobsłużony wyjątek (`500`, treść to strona HTML, nie JSON) niezależnie od poprawności żądania — to znane usterki backendu, nie błąd Twojej integracji. Buduj klienta odpornie: gdy odpowiedź nie jest JSON-em, potraktuj ją jako błąd serwera. > **Uwaga:** > **Odwrócony komunikat zakresu dat.** W części raportów przy `date_min > date_max` komunikat błędu bywa odwrócony (mówi o `date_max`, choć problem dotyczy `date_min`). Waliduj kolejność dat po swojej stronie. ## Odpowiedź `200`, ale dane są puste lub zerowe To **nie jest błąd** — żądanie przeszło walidację, tylko dla podanych kryteriów nie ma danych. Kolejność sprawdzania, od najczęstszej przyczyny: 1. **Zły `fetch_mode`.** Host z subdomeną (`blog.example.com`) w trybie `topLevelDomain` liczy wyłącznie frazy, na których rankuje ten konkretny host — a tych zwykle nie ma. Zwalidowane: `blog.senuto.com` w `topLevelDomain` zwraca `0`, a w `subdomain` — 316 fraz w TOP10. Patrz [`fetch_mode`](/types/fetch-mode). 2. **Domena spoza indeksu.** Senuto liczy widoczność na własnym zbiorze fraz per rynek. Domena, która nie rankuje w TOP50 na żadną frazę z bazy, zwróci zera — tak samo jak domena nieistniejąca (sprawdzone: dla wymyślonej domeny raport dashboardu odpowiada `200` z samymi zerami, nie błędem). 3. **Zły rynek (`country_id`).** Bez tego pola raporty liczą na bazie domyślnej (Polska 1.0). Domena czeska sprawdzana na polskiej bazie da zera. Część danych istnieje **wyłącznie w bazie PL 2.0 (`country_id: 200`)** — np. raport intencji AI Overviews na `country_id: 1` zwraca pustą tablicę dla każdej domeny. 4. **Zakres dat sprzed pojawienia się domeny w bazie** — raporty historyczne zwrócą pustą serię dla okresu, w którym nie było jeszcze pomiarów. 5. **`filtering` odcina wszystko.** Filtr niepasujący do żadnego wiersza daje pustą tablicę, a nie błąd — sprawdź go, usuwając filtry po kolei. > **Informacja:** > Rozróżnienie, które warto zaszyć w kliencie: `data: []` (albo same zera) = **brak danych dla kryteriów**, > `success: false` = **odrzucone żądanie**. Pierwsze nie jest powodem do retry — powtórzone da to samo. ## `418` w trakcie długiego pobierania albo w pętli Typowy zgłaszany scenariusz: integracja (n8n, Make, własny skrypt) pobiera dane od kilku minut i nagle zaczyna dostawać `418`, choć te same żądania wcześniej działały. Prawdziwe przyczyny, w kolejności: - **Wyczerpany limit dobowy.** Najczęstsza. Komunikat wygląda tak: `You have reached daily limit of queries. Daily limit value is N`. Limity są **per konto**, nie per token, więc pętla w integracji rywalizuje o ten sam licznik co praca w aplikacji. Stan sprawdzisz przez [`GET /api/users/getLimits`](/rate-limits) — ten odczyt nic nie zużywa. - **Zmienna w pętli, która czasem jest pusta.** Iterując po liście domen czy fraz łatwo trafić na wiersz z pustą wartością — walidator zwróci `418` z `params` wskazującym pole. Zaloguj `data.error.params`, a znajdziesz sprawcę w jednym kroku. - **Zasób spoza konta.** `project_id` albo `group_id` innego użytkownika daje `418 Unauthorized access` (a nie `404`), co w pętli po projektach wygląda jak losowa awaria. > **Ostrzeżenie:** > Nie myl tego z **wygaśnięciem tokenu** — token po terminie ważności daje `302` na `…/users/login`, > a po przekierowaniu `404` z pustym `message`, nie `418`. Jeśli integracja działała miesiąc i nagle > „wszystkie endpointy zniknęły", to token (patrz [Status HTTP `404`](#status-http-404--trzy-różne-przyczyny)). Klienci HTTP i platformy no-code często **opakowują** treść błędu we własny komunikat (np. n8n pokazuje „Your request is invalid"). Zawsze zaglądaj do surowego ciała odpowiedzi — `data.error.type` i `data.error.params` mówią, co naprawdę odrzucono. ## `500` / `503` — awaria czy mój błąd? | Objaw | Najpewniej | Co zrobić | | ------------------------------------------------------------------------ | ----------------------------------------------------- | ---------------------------------------- | | `500` na **jednym** endpoincie, inne działają | znana usterka tego endpointu (patrz wyżej) | zgłoś do supportu, obejdź innym raportem | | `500`/`503` na **wszystkich** endpointach, także `GET /api/users/whoami` | awaria po stronie API | ponów z backoffem, potem zgłoś | | `418`/`404`/`302` na wszystkich | to **nie** awaria — patrz sekcje wyżej (limit, token) | sprawdź token i limity | Najprostszy test rozstrzygający: wywołaj `GET /api/users/whoami`. To najlżejszy endpoint w API — jeśli on odpowiada `200`, backend żyje i problem jest w konkretnym żądaniu. Bieżące informacje o awariach i pracach serwisowych publikujemy na **[komunikat.senuto.com](https://komunikat.senuto.com)** (strona tymczasowa) — zajrzyj tam, zanim zaczniesz debugować własny kod. Przy masowych `5xx` ponawiaj z **wykładniczym backoffem** (np. 1 s, 2 s, 4 s, 8 s… do kilku minut), a nie w pętli co sekundę. Gdy po kilkunastu minutach nic się nie zmienia, zgłoś do supportu i podaj: pełny URL żądania, metodę, czas z dokładnością do minuty (ze strefą), ciało żądania bez tokenu oraz surową odpowiedź z nagłówkami. Z tym zestawem sprawa idzie od razu do zespołu API zamiast wracać z prośbą o szczegóły. ## Obsługa po stronie klienta — wzorzec ```jsonc filename="obsługa błędu (pseudokod)" // odpowiedź = wyślij żądanie // jeśli odpowiedź nie jest JSON-em → błąd serwera (np. 500 HTML) // w przeciwnym razie: // jeśli odpowiedź.success == false: // switch (odpowiedź.data.error.type): // "invalid_data" → popraw pola z error.params (mapa pole → reguła) // "invalid_filtering"→ sprawdź klucze filtra dla tego endpointu // "unauthorized" → sprawdź dostęp do project_id / zasobu // "timeout" → zawęź zakres i ponów // inne → potraktuj jak błąd tymczasowy / zgłoś ```