--- title: "Konkurenci: macierz pozycji (`getMatrix`)" source: https://docs.senuto.com/modules/rank_tracker/rt-competitors-getMatrix api: POST /api/rank_tracker/reports/competitors/getMatrix --- # 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`