{"openapi":"3.0.0","info":{"title":"Tillio API v2","description":"Pełne API produktowe systemu Tillio - dla programistów i integracji (ERP, import, automatyzacje).\n\n**Zasady:**\n- Prawdziwe kody HTTP: 2xx sukces, 4xx błąd po stronie klienta (z typowanym opisem), 5xx po naszej.\n- Błędy walidacji zawsze w kontrakcie `{field, code, message}` - patrz schemat `Error`.\n- **Każda odpowiedź ma nagłówek `X-Request-Id`**, a koperta błędu dodatkowo pole\n  `requestId`. Podaj go w zgłoszeniu do nas - po nim znajdziemy pełną przyczynę\n  w logu instalacji. Komunikaty błędów są celowo neutralne: szczegóły techniczne\n  (zapytania SQL, ścieżki, komunikaty bibliotek) nie wychodzą poza serwer. Jeśli\n  wysyłasz własny `X-Request-Id` (do korelacji z Twoimi logami), użyjemy go.\n- **Który kod dla którego wejścia:** błąd w parametrach adresu (query string, ścieżka) = `400`\n  z kodami `query.*`; błąd w ciele żądania (JSON albo pola formularza multipart) = `422`\n  z kodami `body.*`. Ten sam parametr może więc mieć dwa kody zależnie od tego, którędy\n  przyjechał - np. `directoryId` w listingu DMS (query, 400) i przy uploadzie (formularz, 422).\n- Pola camelCase, po angielsku, stabilne - niezależne od wewnętrznego schematu bazy.\n- Sync przyrostowy przez `updatedAfter` na listach.\n- Pola niestandardowe (`customField`) zawsze w odpowiedzi; ich definicje w `GET /v2/<encja>/custom-fields`.\n- **Jedna nazwa na pojęcie, ta sama w GET, POST i PUT** (kanon od wersji API 2.0.0, bez aliasów):\n  - twórca rekordu / autor wpisu: `creatorUserId`,\n  - osoba odpowiedzialna (opiekun, właściciel, prowadzący - jedna osoba na rekord): `ownerUserId`,\n  - sprzedawca/handlowiec (rola obok właściciela): `salesUserId` (usługi),\n  - powiązanie rekordu z WIELOMA użytkownikami (wykonawcy zadań): `assignedUserIds`,\n  - pola słownikowe niosą nazwę słownika: `contractorStatusId`, `ticketStatusId`, `projectStatusId`,\n    `contractorTypeId`, `addressTypeId`, `noteTypeId`, `ticketSourceId`, `contractorSourceId`,\n    `pipelineStageId`, `ticketStageId`, `leadStageId` itd. - nazwa pola mówi, którym\n    słownikiem (`GET /v2/...`) mapować wartość, niezależnie od encji, na której pole występuje.\n  Wzorzec „pobierz rekord, zmień pole, odeślij\" przechodzi bez mapowania nazw.\n\n**Autoryzacja - trzy nagłówki na każdym endpoincie** (poza `/v2/health` i `/v2/docs`):\n`X-Tenant-Domain` (domena workspace), `X-Tenant-Id` (identyfikator/bucket tenanta),\n`X-Api-Key` (`nazwa:klucz` - klucz API wystawiony w aplikacji Tillio). Odmowa zawsze `401 auth.invalidCredentials`.\nPrzerwa serwisowa: `503 system.maintenance`. Konto zablokowane: `403 tenant.blocked`.\n\n**Rate limit:** 1000 requestów / 60 s na klucz API (cała strefa /v2). Stan w nagłówkach\n`X-RateLimit-Limit` / `X-RateLimit-Remaining` / `X-RateLimit-Reset`; przekroczenie =\n`429 rateLimit.exceeded` + `Retry-After` (sekundy do resetu okna). Nagłówki opisują\nlimit Twojego klucza. Niezależnie działa luźniejszy limit per adres IP (domyślnie\ntrzykrotność limitu klucza), obejmujący też żądania bez poprawnego klucza - chroni\nbramkę auth przed zgadywaniem kluczy. Do limitu IP liczą się także żądania na\nnieistniejące ścieżki (404) i złą metodą (405).\n\n**Limity treści żądania:** body JSON do 2 MB (`413 body.tooLarge`) i najwyżej 20 000\nobiektów/tablic w jednym żądaniu (`413 body.tooComplex`) - oba sprawdzane przed\ndekodowaniem, chronią pamięć serwera. Paczki batch (do 100 pozycji) mieszczą się\nz dużym zapasem; większe dane dziel na kilka żądań. Załączniki maila\n(`POST /v2/mail/send`): 50 MB na plik i 50 MB łącznie razem z załącznikami szablonu\n(`422 body.attachmentsTooLarge`).\n\n**Serwery multi-instalacyjne:** gdy na jednym serwerze działa kilka instalacji CRM,\nopcjonalny nagłówek `X-Instance-Name` wskazuje nazwę instancji, w której żyje Twój\ntenant (pole w sekcji Authorize; dotyczy garstki wdrożeń - jeśli nie dostałeś nazwy\ninstancji, pomiń).\n\n**Moduły instancji:** część API odpowiada modułom Tillio, które klient może mieć poza\nplanem albo wyłączone w Ustawieniach (Zgłoszenia, Leady, Szanse sprzedaży, Zadania,\nProjekty, DMS, Poczta). Zapis do encji z nieaktywnego modułu kończy się\n`403 module.notActive` z nazwą modułu - listę sprawdzisz z wyprzedzeniem\nw `GET /v2/modules`.\n\n## Wersja instancji - co jest dostępne\n\n`GET /v2/health` (publiczny, bez nagłówków) zwraca `version` w formacie semver -\nwersję API wdrożoną **na tej konkretnej instalacji**. Jeśli Twoja integracja obsługuje\nkilka instalacji naraz, mogą one stać na różnych wersjach.\n\nSkąd wiadomo, czego się po danej wersji spodziewać:\n- **Opis pola/endpointu** w tej dokumentacji podaje wersję przy opcjach dodanych później\n  (np. „dostępna od wersji API 1.5.1\").\n- **`GET /v2/openapi.json`** - kompletny kontrakt tej instancji. Jeśli wolisz sprawdzać\n  zamiast porównywać numery: obecność ścieżki, parametru czy pola w specyfikacji jest\n  rozstrzygająca, bo spec generuje się z kodu tej instalacji.\n\n## Czas, daty i zakresy\n\n**Wszystko dzieje się w GŁÓWNEJ strefie czasowej instancji** - tej z konfiguracji\nworkspace'u (np. `Europe/Warsaw`). Obowiązuje jednakowo przy filtrowaniu, zapisie\ni w odpowiedziach, po stronie API i bazy. Nie przeliczaj nic na UTC.\n\n⚠️ **To NIE jest strefa użytkownika.** Osoby pracujące w CRM mają własne ustawienie\nstrefy (widoczne w aplikacji), ale API nigdy się nim nie kieruje - także wtedy, gdy\nklucz API należy do użytkownika z inną strefą niż instancja. Dzięki temu ten sam\nrequest zwraca ten sam wynik niezależnie od tego, czyim kluczem jedzie.\n\n- **W odpowiedziach** daty to ISO 8601 z jawnym offsetem: `2026-08-20T00:46:05+02:00`.\n- **W zapytaniach** możesz podać czas lokalny bez offsetu (`2026-08-20T00:00:00`) -\n  zostanie zrozumiany jako czas instancji - albo z offsetem/`Z`\n  (`2026-08-19T22:00:00Z`), wtedy API sam go przeliczy. Oba warianty dają ten sam wynik.\n- **Przyjmowane zapisy dat na wejściu** (parametry zakresów i pola dat w body):\n  `2026-08-20T10:00:00+02:00`, `2026-08-20T08:00:00Z`, `2026-08-20T08:00:00.250Z`,\n  `2026-08-20T10:00:00`, `2026-08-20 10:00:00` i `2026-08-20` (północ). Każdy inny zapis,\n  pusta wartość i data spoza kalendarza (np. 31 lutego) to błąd `400 query.invalidDate`\n  (w body: `422 body.invalidValue`) - API nie zgaduje daty. Od wersji API 2.14.0; wcześniej\n  część takich wartości była po cichu zamieniana (pusty `updatedAfter` = „teraz\").\n- **Stronicowanie:** `page` i `limit` to liczby całkowite zapisane samymi cyframi -\n  `page=2abc` albo `page[]=2` to `400 query.invalidValue`.\n- Zaglądasz do bazy, żeby porównać wyniki? Kolumny dat to `TIMESTAMP`, więc surowe\n  zapytanie SQL pokaże czasy **w UTC**, jeśli sesja nie ma ustawionej strefy. To nie\n  jest rozbieżność w API - to różnica stref w Twoim kliencie SQL.\n\n**Zakresy dat** (tam, gdzie encja je wspiera): `createdAfter`/`createdBefore` po dacie\nutworzenia, `updatedAfter`/`updatedBefore` po dacie modyfikacji. Granice są\n**niesymetryczne i to jest celowe**:\n\n| Parametr | Warunek | Znaczenie |\n|---|---|---|\n| `createdAfter` / `updatedAfter` | `>` (ostro) | rekordy PO tej chwili, bez niej |\n| `createdBefore` / `updatedBefore` | `<=` (włącznie) | rekordy DO tej chwili, z nią |\n\nDzięki temu przy synchronizacji przyrostowej podajesz po prostu znacznik ostatniego\npobranego rekordu i nie dostajesz go po raz drugi.\n\n**Cała doba** (20 sierpnia) - dolna granica to koniec poprzedniej doby, bo `After`\njest ostre; inaczej wypadłyby rekordy utworzone dokładnie o północy:\n\n```\nGET /v2/notes?createdAfter=2026-08-19T23:59:59&createdBefore=2026-08-20T23:59:59&sort=createdAt&sortDir=desc\n```\n\n**Sync przyrostowy** (od ostatniego udanego pobrania), rosnąco, żeby przechodzić\nstrumień do przodu:\n\n```\nGET /v2/pipeline/items?updatedAfter=2026-08-20T12:00:00&sort=updatedAt&sortDir=asc&limit=1000\n```\n\nUwaga przy takim kursorze: daty mają rozdzielczość **jednej sekundy**, a import potrafi\nutworzyć kilkanaście rekordów w tej samej sekundzie. Jeśli strona skończy się w środku\ntakiej sekundy, kolejne zapytanie ją pominie. Bezpiecznie: cofnij kursor o sekundę\n(`updatedAfter = ostatni updatedAt - 1s`) i odfiltruj po `id` rekordy już przetworzone.\nPrzy sztywnych widełkach (np. raport dobowy) problem nie występuje.\n\n**Czego się spodziewać po encji:** nie każda ma datę modyfikacji - np. notatki mają\nwyłącznie `createdAt` (CRM nie zapisuje ich edycji), więc `updatedAfter` zwróci tam 400\nz listą wspieranych parametrów. Pola sortowania i filtry zakresowe są wypisane przy\nkażdym endpoincie.","version":"2.14.1"},"servers":[{"url":"/","description":"Bieżąca instancja"}],"paths":{"/v2/contractors/{id}/addresses":{"get":{"tags":["Contractors"],"summary":"Adresy kontrahenta","description":"Adresy fizyczne kontrahenta - ten sam kształt co `include=address` w GET /v2/contractors. `latitude`/`longitude` są `null`, dopóki adres nie został zgeokodowany.","operationId":"listContractorAddresses","parameters":[{"name":"id","in":"path","description":"Id kontrahenta","required":true,"schema":{"type":"integer","example":121}}],"responses":{"200":{"description":"Lista adresów","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Address"}}},"type":"object"}}}},"404":{"description":"Brak kontrahenta o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"post":{"tags":["Contractors"],"summary":"Nowy adres kontrahenta","description":"Nowy adres kontrahenta (dowolny aktywny typ z GET /v2/address/types). `duplicateCheck: true` sprawdza przed zapisem, czy kontrahent ma już adres tego typu o identycznych polach adresowych - trafienie zwraca istniejący adres z kodem 200 zamiast dokładać kopię (powtórka importu jest bezpieczna). Dostępne od wersji API 1.27.0.\n\n`addressLookup` weryfikuje adres w geokoderze (ten sam serwis, z którego korzysta\nCRM przy mapach), analogicznie do `taxIdLookup` przy NIP-ie:\n\n| wartość | zachowanie |\n|---|---|\n| `off` (domyślnie) | zapis bez sprawdzania |\n| `validateOnly` | sprawdź i powiedz w `lookup`, zapisz dane tak, jak przyszły |\n| `normalize` | zapisz adres w postaci z geokodera (ulica z numerem, kod, miasto, region, powiat, kraj) |\n\n`lookup.matchLevel` mówi, co geokoder rozpoznał: `exact` = ulica z numerem\ni miejscowość (prawdziwy adres), `approximate` = trafienie tylko w miejscowość\nalbo region - tak wyglądają nazwy sklepów i identyfikatory punktów wpisane\nw pole adresowe. `normalize` nadpisuje dane WYŁĄCZNIE przy `exact`.\n\n`failOnInvalidAddress: true` zamienia wszystko poniżej `exact` w twarde 422\n(`address.invalid`) - żeby śmieć nie wjechał do CRM i nie popsuł mapy.\nNiedostępny geokoder NIGDY nie blokuje zapisu (`lookup.status: unavailable`) -\ntak samo jak niedostępny GUS przy weryfikacji NIP-u.\n\nWynik weryfikacji (`lookup` w odpowiedzi) dostajesz zawsze. Współrzędne\ni `placeId` są ZAPISYWANE do rekordu adresu wyłącznie, gdy moduł Mapy jest\nAKTYWNY **w momencie wywołania API** - przy imporcie włącz Mapę PRZED\nstartem skryptu. Bez modułu system współrzędnych nigdzie nie używa,\na późniejsze włączenie modułu i tak przepuszcza wszystkie adresy przez\ngeokoder CRM.\n\nUwaga: przy AKTYWNYM module Mapy CRM samodzielnie uzupełnia `region` i `district`\nz geokodera przy każdym zapisie adresu - niezależnie od `addressLookup`. To\nzachowanie CRM, nie API.\n\nAdres fizyczny należy w CRM do FIRMY (wspólnej dla kontrahentów o tym samym\nNIP-ie), nie do pojedynczego kontrahenta - kontrahenci dzielący firmę widzą\nten sam zestaw adresów i zmiany zrobione przez któregokolwiek z nich.","operationId":"createContractorAddress","parameters":[{"name":"id","in":"path","description":"Id kontrahenta","required":true,"schema":{"type":"integer","example":121}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["addressTypeId"],"properties":{"addressTypeId":{"description":"Typ adresu z GET /v2/address/types","type":"integer","example":1},"street":{"description":"Ulica z numerem","type":"string","example":"Marszałkowska 10"},"street2":{"description":"Druga linia adresu (lokal, piętro)","type":"string","example":"lok. 3"},"postCode":{"type":"string","example":"00-590"},"city":{"type":"string","example":"Warszawa"},"region":{"description":"Województwo","type":"string","example":"mazowieckie"},"district":{"description":"Powiat","type":"string","example":"Warszawa"},"country":{"description":"Dwuliterowy kod kraju","type":"string","example":"PL"},"addressLookup":{"description":"Weryfikacja adresu w geokoderze (dostępna od wersji API 1.21.0)","type":"string","default":"off","enum":["off","validateOnly","normalize"]},"failOnInvalidAddress":{"description":"Odrzuć zapis, gdy geokoder nie rozpozna adresu z ulicą i numerem","type":"boolean","default":false},"duplicateCheck":{"description":"Nie dokładaj kopii: identyczny adres tego typu u kontrahenta = zwróć go z kodem 200 (info.created: false). Porównywane pola: street, street2, postCode, city, country - po ewentualnej normalizacji geokoderem. Dostępne od wersji API 1.27.0.","type":"boolean","default":false}},"type":"object"}}}},"responses":{"201":{"description":"Adres utworzony (200, gdy duplicateCheck zwrócił istniejący - patrz info.created)","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/Address"},"lookup":{"$ref":"#/components/schemas/AddressLookupResult"},"info":{"properties":{"created":{"description":"false = duplicateCheck trafił w istniejący adres","type":"boolean","example":true}},"type":"object"}},"type":"object"}}}},"422":{"description":"Błędy walidacji; `address.invalid` = geokoder nie potwierdził adresu przy failOnInvalidAddress","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień do edycji kontrahentów","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Brak kontrahenta o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/addresses/{id}":{"put":{"tags":["Contractors"],"summary":"Aktualizacja adresu","description":"Zmiana adresu. Aktualizacja CZĘŚCIOWA - pola nieprzysłane zostają bez zmian, pole przysłane jako `null` albo pusty tekst jest czyszczone.\n\n`addressLookup` weryfikuje adres w geokoderze (ten sam serwis, z którego korzysta\nCRM przy mapach), analogicznie do `taxIdLookup` przy NIP-ie:\n\n| wartość | zachowanie |\n|---|---|\n| `off` (domyślnie) | zapis bez sprawdzania |\n| `validateOnly` | sprawdź i powiedz w `lookup`, zapisz dane tak, jak przyszły |\n| `normalize` | zapisz adres w postaci z geokodera (ulica z numerem, kod, miasto, region, powiat, kraj) |\n\n`lookup.matchLevel` mówi, co geokoder rozpoznał: `exact` = ulica z numerem\ni miejscowość (prawdziwy adres), `approximate` = trafienie tylko w miejscowość\nalbo region - tak wyglądają nazwy sklepów i identyfikatory punktów wpisane\nw pole adresowe. `normalize` nadpisuje dane WYŁĄCZNIE przy `exact`.\n\n`failOnInvalidAddress: true` zamienia wszystko poniżej `exact` w twarde 422\n(`address.invalid`) - żeby śmieć nie wjechał do CRM i nie popsuł mapy.\nNiedostępny geokoder NIGDY nie blokuje zapisu (`lookup.status: unavailable`) -\ntak samo jak niedostępny GUS przy weryfikacji NIP-u.\n\nWynik weryfikacji (`lookup` w odpowiedzi) dostajesz zawsze. Współrzędne\ni `placeId` są ZAPISYWANE do rekordu adresu wyłącznie, gdy moduł Mapy jest\nAKTYWNY **w momencie wywołania API** - przy imporcie włącz Mapę PRZED\nstartem skryptu. Bez modułu system współrzędnych nigdzie nie używa,\na późniejsze włączenie modułu i tak przepuszcza wszystkie adresy przez\ngeokoder CRM.\n\nUwaga: przy AKTYWNYM module Mapy CRM samodzielnie uzupełnia `region` i `district`\nz geokodera przy każdym zapisie adresu - niezależnie od `addressLookup`. To\nzachowanie CRM, nie API.\n\nAdres fizyczny należy w CRM do FIRMY (wspólnej dla kontrahentów o tym samym\nNIP-ie), nie do pojedynczego kontrahenta - kontrahenci dzielący firmę widzą\nten sam zestaw adresów i zmiany zrobione przez któregokolwiek z nich.","operationId":"updateAddress","parameters":[{"name":"id","in":"path","description":"Id adresu","required":true,"schema":{"type":"integer","example":812}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"properties":{"addressTypeId":{"type":"integer","example":1},"street":{"type":"string","example":"Marszałkowska 12"},"street2":{"type":"string","nullable":true},"postCode":{"type":"string","example":"00-590"},"city":{"type":"string","example":"Warszawa"},"region":{"type":"string","example":"mazowieckie"},"district":{"type":"string","example":"Warszawa"},"country":{"type":"string","example":"PL"},"addressLookup":{"type":"string","default":"off","enum":["off","validateOnly","normalize"]},"failOnInvalidAddress":{"type":"boolean","default":false}},"type":"object"}}}},"responses":{"200":{"description":"Adres zaktualizowany","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/Address"},"lookup":{"$ref":"#/components/schemas/AddressLookupResult"}},"type":"object"}}}},"404":{"description":"Brak adresu o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"delete":{"tags":["Contractors"],"summary":"Usunięcie adresu","description":"Usuwa adres kontrahenta. Operacja nieodwracalna - CRM kasuje wiersz adresu wraz z powiązaniem kontrahent-adres.","operationId":"deleteAddress","parameters":[{"name":"id","in":"path","description":"Id adresu","required":true,"schema":{"type":"integer","example":812}}],"responses":{"200":{"description":"Adres usunięty","content":{"application/json":{"schema":{"properties":{"data":{"properties":{"id":{"type":"integer","example":812},"deleted":{"type":"boolean","example":true}},"type":"object"}},"type":"object"}}}},"404":{"description":"Brak adresu o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/calendars":{"get":{"tags":["Calendars"],"summary":"Lista kalendarzy","description":"Kalendarze instancji razem z dostępami użytkowników.\n\nTypowy scenariusz integracji (dopisywanie spotkań handlowcom): najpierw ustal,\ndo którego kalendarza wpisywać wydarzenia danej osoby. Główny kalendarz\nużytkownika to ten, w którym ma on wpis z `main: true`:\n\n```\nGET /v2/calendars?ownerUserId=12345\n```\n\nPole `users` niesie wszystkie dostępy: `userId`, `main` (czy to GŁÓWNY kalendarz\ntej osoby), `admin` (0 podgląd, 1 edycja, 2 administrator) i `accessTo`.\n\nRodzaj kalendarza mapuj słownikiem `GET /v2/calendar/types`. Kalendarz spięty ze\nskrzynką pocztową ma wypełnione `mailAccountId` (konto z `GET /v2/mail/accounts`)\n- stamtąd biorą się zaproszenia przychodzące mailem.\n\nSame WYDARZENIA nie są tutaj - trzymamy je w usłudze kalendarzowej, nie w bazie\nCRM. Lista wydarzeń: `GET /v2/calendars/{id}/events`.","operationId":"listCalendars","parameters":[{"name":"id","in":"query","description":"Filtr po id - dopasowanie dokładne.","required":false,"schema":{"type":"integer"}},{"name":"name","in":"query","description":"Filtr częściowy (zawiera).","required":false,"schema":{"type":"string","example":"Kowalski"}},{"name":"calendarTypeId","in":"query","description":"Rodzaj kalendarza - słownik GET /v2/calendar/types.","required":false,"schema":{"type":"integer","example":1}},{"name":"ownerUserId","in":"query","description":"Właściciel kalendarza (GET /v2/users).","required":false,"schema":{"type":"integer","example":12345}},{"name":"mailAccountId","in":"query","description":"Powiązane konto pocztowe (GET /v2/mail/accounts).","required":false,"schema":{"type":"integer","example":101}},{"name":"email","in":"query","description":"Adres kalendarza - dopasowanie dokładne.","required":false,"schema":{"type":"string"}},{"name":"oauthEmail","in":"query","description":"Konto zewnętrzne (Microsoft/Google) - dopasowanie dokładne.","required":false,"schema":{"type":"string"}},{"name":"oauthAuthorized","in":"query","description":"Czy synchronizacja z kontem zewnętrznym ma ważną autoryzację.","required":false,"schema":{"type":"boolean"}},{"name":"allowExternalEvents","in":"query","description":"Czy kalendarz przyjmuje zaproszenia z zewnątrz.","required":false,"schema":{"type":"boolean"}},{"name":"active","in":"query","description":"Kalendarz aktywny.","required":false,"schema":{"type":"boolean"}},{"name":"sort","in":"query","description":"Pole sortowania: id, name, calendarTypeId, ownerUserId.","required":false,"schema":{"type":"string","default":"id"}},{"name":"sortDir","in":"query","required":false,"schema":{"type":"string","default":"asc","enum":["asc","desc"]}},{"name":"page","in":"query","description":"Strona wyników, od 1.","required":false,"schema":{"type":"integer","default":1,"maximum":1000000,"minimum":1}},{"name":"limit","in":"query","description":"Rozmiar strony (max 500).","required":false,"schema":{"type":"integer","default":100,"maximum":500,"minimum":1}}],"responses":{"200":{"description":"Lista kalendarzy + metadane stronicowania","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Calendar"}},"pagination":{"$ref":"#/components/schemas/Pagination"}},"type":"object"}}}},"400":{"description":"Błąd walidacji zapytania - lista błędów w kontrakcie {field, code, message}","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/calendars/{id}":{"get":{"tags":["Calendars"],"summary":"Kalendarz po id","description":"Pojedynczy kalendarz po id, razem z dostępami użytkowników.","operationId":"getCalendar","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":400}}],"responses":{"200":{"description":"Kalendarz","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/Calendar"}},"type":"object"}}}},"404":{"description":"Brak kalendarza o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/calendars/{id}/events":{"get":{"tags":["Calendars"],"summary":"Wydarzenia kalendarza","description":"Wydarzenia z kalendarza w zadanym zakresie dat.\n\nWydarzeń NIE ma w bazie CRM - trzymamy je w usłudze kalendarzowej, a w CRM\nzostaje powiązanie z kontrahentem i typem. Dlatego lista wymaga zakresu dat\n(`dateFrom`, `dateTo`) i nie ma tu stronicowania ani filtrów po dowolnym polu;\nbez podanego zakresu zwracamy najbliższe 30 dni.\n\n```\nGET /v2/calendars/402/events?dateFrom=2026-09-01&dateTo=2026-09-30\n```\n\nOdczyt wykonuje się w kontekście WŁAŚCICIELA kalendarza (`ownerUserId`), bo to\njego konto jest znane usłudze kalendarzowej.","operationId":"listCalendarEvents","parameters":[{"name":"id","in":"path","description":"Id kalendarza (GET /v2/calendars).","required":true,"schema":{"type":"integer","example":402}},{"name":"dateFrom","in":"query","description":"Początek zakresu (data albo data z godziną). Domyślnie dziś.","required":false,"schema":{"type":"string","format":"date","example":"2026-09-01"}},{"name":"dateTo","in":"query","description":"Koniec zakresu. Domyślnie 30 dni od dziś.","required":false,"schema":{"type":"string","format":"date","example":"2026-09-30"}}],"responses":{"200":{"description":"Wydarzenia w zakresie dat (posortowane po dacie rozpoczęcia)","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/CalendarEvent"}}},"type":"object"}}}},"404":{"description":"Brak kalendarza o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Instalacja nie obsługuje wydarzeń kalendarza (brak modułu) - kod calendar.serviceUnavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"post":{"tags":["Calendars"],"summary":"Nowe wydarzenie w kalendarzu","description":"Dodaje wydarzenie do kalendarza - to ścieżka do automatycznego wpisywania spotkań\n(np. handlowcowi po umówieniu terminu).\n\nWymagane: `title`, `startAt`, `endAt` (ISO 8601 ze strefą, np. `2026-09-01T10:00:00+02:00`).\nUczestnicy z `attendees` dostają zaproszenia; `contractorId` wiąże spotkanie z\nkartoteką kontrahenta, `taskId` z zadaniem.\n\nWydarzenie zakłada się w kontekście WŁAŚCICIELA kalendarza i to on figuruje jako\norganizator. Jeśli nie chcesz wysyłać powiadomień, ustaw `sendNotifications: false`.","operationId":"createCalendarEvent","parameters":[{"name":"id","in":"path","description":"Id kalendarza (GET /v2/calendars).","required":true,"schema":{"type":"integer","example":402}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["title","startAt","endAt"],"properties":{"title":{"type":"string","example":"Spotkanie handlowe - ACME"},"description":{"type":"string","example":"Omówienie oferty na 2027 rok.","nullable":true},"location":{"type":"string","example":"Warszawa, ul. Marszałkowska 10","nullable":true},"startAt":{"description":"Początek (ISO 8601 ze strefą).","type":"string","format":"date-time","example":"2026-09-01T10:00:00+02:00"},"endAt":{"description":"Koniec (ISO 8601 ze strefą).","type":"string","format":"date-time","example":"2026-09-01T11:00:00+02:00"},"allDay":{"description":"Wydarzenie całodniowe - godziny są wtedy pomijane.","type":"boolean","example":false},"attendees":{"description":"Uczestnicy - dostają zaproszenie na podany adres.","type":"array","items":{"required":["email"],"properties":{"name":{"type":"string","example":"Jan Kowalski"},"email":{"type":"string","example":"jan.kowalski@acme.pl"}},"type":"object"}},"contractorId":{"description":"Kontrahent, którego dotyczy spotkanie (GET /v2/contractors) - wydarzenie pojawi się na jego kartotece.","type":"integer","example":12345,"nullable":true},"contactId":{"description":"Kontakt, którego dotyczy spotkanie (GET /v2/contacts) - wydarzenie pojawi się na jego kartotece. Wymaga nowszej wersji CRM: starsza instalacja odpowiada 501 feature.notSupportedByCrmVersion. Dostępne od wersji API 2.8.0.","type":"integer","example":3921,"nullable":true},"taskId":{"description":"Zadanie powiązane ze spotkaniem (GET /v2/tasks).","type":"integer","example":987,"nullable":true},"ticketId":{"description":"Zgłoszenie powiązane ze spotkaniem (GET /v2/tickets).","type":"integer","example":42,"nullable":true},"eventTypeId":{"description":"Typ wydarzenia z konfiguracji kalendarza.","type":"integer","example":3,"nullable":true},"sendNotifications":{"description":"Czy rozesłać zaproszenia i powiadomienia. Domyślnie true.","type":"boolean","example":true}},"type":"object"}}}},"responses":{"201":{"description":"Wydarzenie utworzone","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/CalendarEvent"},"created":{"type":"boolean","example":true}},"type":"object"}}}},"403":{"description":"Brak uprawnień do dodawania wydarzeń w tym kalendarzu","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Brak kalendarza o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Błąd walidacji - lista błędów w kontrakcie {field, code, message}","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"Usługa kalendarza nie przyjęła wydarzenia (przyczyna w logu pod requestId)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Instalacja nie obsługuje wydarzeń kalendarza (brak modułu) - kod calendar.serviceUnavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/contacts":{"get":{"tags":["Contacts"],"summary":"Lista kontaktów z filtrowaniem po kontrahencie, mailu i dowolnym polu","description":"Lista osób kontaktowych - jedna lista dla całej instancji, z filtrami i paginacją.\n\n**Typowe zastosowania:**\n- osoby kontaktowe kontrahenta (główny use-case): `?contractorId=121`,\n- dopasowanie po mailu przy imporcie (szukanie istniejącego kontaktu): `?email=jan@acme.pl`\n  (dokładne; dopasowuje którykolwiek z adresów kontaktu, nie tylko główny),\n- szukanie po nazwisku: `?name=kowal` (częściowe, po imieniu i nazwisku razem),\n- synchronizacja przyrostowa: `?updatedAfter=<data ostatniego syncu>`.\n\nKontakt może być powiązany z wieloma kontrahentami - filtr `contractorId`\nzwraca każdy kontakt powiązany ze wskazanym kontrahentem (nie tylko te,\ndla których jest on głównym). Filtry po polach niestandardowych:\n`customField[<klucz>]=<wartość>` (definicje w GET /v2/contact/custom-fields).","operationId":"listContacts","parameters":[{"name":"contractorId","in":"query","description":"Kontakty powiązane ze wskazanym kontrahentem (dokładne). Główny use-case listy.","required":false,"schema":{"type":"integer","example":121}},{"name":"email","in":"query","description":"Dopasowanie dokładne do któregokolwiek adresu e-mail kontaktu (główny lub dodatkowy).","required":false,"schema":{"type":"string","example":"jan.kowalski@acme.pl"}},{"name":"name","in":"query","description":"Filtr częściowy (zawiera) po imieniu i nazwisku razem. Analogicznie częściowe: firstName, lastName, position, phone, phoneAlternative, note.","required":false,"schema":{"type":"string","example":"kowal"}},{"name":"phone","in":"query","description":"Filtr częściowy (zawiera) po numerze głównym, dosłownie po zapisie w bazie. Do dopasowania DOKŁADNEGO po wszystkich polach numeru (kontakty i kontrahenci naraz, niezależnie od zapisu z plusem/bez) służy GET /v2/lookup/phone.","required":false,"schema":{"type":"string","example":"601234"}},{"name":"phoneAlternative","in":"query","description":"Filtr częściowy (zawiera) po numerze alternatywnym. Do dopasowania dokładnego służy GET /v2/lookup/phone.","required":false,"schema":{"type":"string"}},{"name":"contactStatusId","in":"query","description":"Status kontaktu: 1 = aktywny, 0 = nieaktywny (dokładne). Analogicznie dokładne: id, ownerUserId, externalId.","required":false,"schema":{"type":"integer","example":1}},{"name":"updatedAfter","in":"query","description":"Tylko rekordy utworzone/zmienione PO tej chwili (ISO 8601). Fundament syncu przyrostowego.","required":false,"schema":{"type":"string","format":"date-time","example":"2026-08-01T00:00:00Z"}},{"name":"updatedBefore","in":"query","description":"Górna granica zakresu zmian (ISO 8601, włącznie).","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"createdAfter","in":"query","description":"Tylko rekordy utworzone po tej chwili (ISO 8601).","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"createdBefore","in":"query","description":"Tylko rekordy utworzone do tej chwili (ISO 8601, włącznie).","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"customField","in":"query","description":"Filtry pól niestandardowych: `customField[<klucz>]=<wartość>` (dokładne; klucze i typy w GET /v2/contact/custom-fields). Pusta wartość = pole nieustawione.","required":false,"style":"deepObject","explode":true,"schema":{"type":"object","additionalProperties":{"type":"string"}}},{"name":"id","in":"query","required":false,"description":"Filtr po polu id - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"firstName","in":"query","required":false,"description":"Filtr po polu firstName - dopasowanie częściowe (zawiera).","schema":{"type":"string"}},{"name":"lastName","in":"query","required":false,"description":"Filtr po polu lastName - dopasowanie częściowe (zawiera).","schema":{"type":"string"}},{"name":"position","in":"query","required":false,"description":"Filtr po polu position - dopasowanie częściowe (zawiera).","schema":{"type":"string"}},{"name":"note","in":"query","required":false,"description":"Filtr po polu note - dopasowanie częściowe (zawiera).","schema":{"type":"string"}},{"name":"ownerUserId","in":"query","required":false,"description":"Filtr po polu ownerUserId - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"externalId","in":"query","required":false,"description":"Filtr po polu externalId - dopasowanie dokładne.","schema":{"type":"string"}},{"name":"sort","in":"query","description":"Pole sortowania: id, firstName, lastName, name, createdAt, updatedAt, lastActivityAt.","required":false,"schema":{"type":"string","default":"updatedAt"}},{"name":"sortDir","in":"query","required":false,"schema":{"type":"string","default":"asc","enum":["asc","desc"]}},{"name":"page","in":"query","description":"Strona wyników, od 1.","required":false,"schema":{"type":"integer","default":1,"maximum":1000000,"minimum":1}},{"name":"limit","in":"query","description":"Rozmiar strony (max 1000).","required":false,"schema":{"type":"integer","default":100,"maximum":1000,"minimum":1}}],"responses":{"200":{"description":"Lista kontaktów + metadane stronicowania","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Contact"}},"pagination":{"$ref":"#/components/schemas/Pagination"}},"type":"object"}}}},"400":{"description":"Błąd walidacji zapytania - lista błędów w kontrakcie {field, code, message}","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"post":{"tags":["Contacts"],"summary":"Nowy kontakt (create-or-attach po email/phone)","description":"**Create-or-attach:** przed zapisem API samo sprawdza, czy taki kontakt już\nistnieje - domyślnie po `email` (którykolwiek adres kontaktu) i `phone`\n(oba numery). ZNALEZIONY kontakt NIE jest duplikowany - API **podpina do\nniego dane**: pola tekstowe uzupełniają wyłącznie puste (nie nadpisują),\ntelefon trafia w wolny numer (główny → alternatywny → warning gdy oba\nzajęte), email jest dokładany jako kolejny adres, `contractorId` dopina\npowiązanie i ustawia kontrahenta głównego (istniejące zostają), `customField` jest nadpisywane (klucze\nintegracji mają być aktualne). Odpowiedź: **HTTP 200** +\n`info.duplicate = {matchedBy, contactId}`.\n\nTablicą `duplicateCheck` (enum: email, phone, name, externalId oraz\n`custom:<klucz>` - pola typu INT/STR/VARCHAR z GET /v2/contact/custom-fields)\nustawiasz pola i ich kolejność (priorytet). `allowDuplicates: true`\nwyłącza sprawdzanie (zawsze insert).\n\nNowy kontakt: wymagane tylko `firstName`; autor/opiekun = użytkownik\nklucza API, status = aktywny. `contractorId` od razu wiąże z kontrahentem\n(jako głównym), `contractorIds` wiąże z kilkoma naraz (pierwszy = główny).\n\n**Kontrahent główny przy podpięciu do istniejącego kontaktu** (od wersji API\n2.10.0): `contractorId` dopina powiązanie i czyni ten kontrahent GŁÓWNYM\n(dotychczasowe powiązania zostają, schodzą niżej); `contractorIds` ZASTĘPUJE\ncałą listę powiązań (pierwszy = główny) - kontrahent usunięty z listy traci\nteż powiązanie kontaktu ze swoimi szansami sprzedaży i zgłoszeniami\n(zachowanie CRM). Wcześniej `contractorId` dokładał powiązanie na koniec\nlisty, bez zmiany głównego.","operationId":"createContact","requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["firstName"],"properties":{"createdAt":{"description":"IMPORT DANYCH: data utworzenia rekordu (ISO 8601, strefa instancji). Pozwala zachować chronologię przy wciąganiu historii sprzed lat - bez niej rekord dostaje datę importu. Tylko przy tworzeniu: przy aktualizacji zwracamy 422 (pomyłkę w imporcie poprawia się kasując rekord i importując ponownie). Data z przyszłości = 422. Dostępne od wersji API 1.16.0.","type":"string","format":"date-time","example":"2023-05-17T09:30:00"},"creatorUserId":{"description":"IMPORT DANYCH: użytkownik, który ma figurować jako twórca rekordu (GET /v2/users). Brak = użytkownik przypisany do klucza API. Zapis wykonuje się w kontekście tej osoby, więc obowiązują JEJ uprawnienia. Dostępne od wersji API 1.16.0.","type":"integer","example":42},"firstName":{"type":"string","example":"Jan"},"lastName":{"type":"string","example":"Kowalski"},"position":{"type":"string","example":"Dyrektor handlowy"},"email":{"description":"Normalizowany; przy podpięciu dokładany jako kolejny adres","type":"string","example":"jan.kowalski@acme.pl"},"phone":{"description":"Normalizowany do formatu międzynarodowego (9 cyfr = +48..., prefiks bez plusa dostaje +, mniej niż 9 cyfr = wartość odrzucana; od wersji API 1.32.0); przy podpięciu trafia w wolny numer","type":"string","example":"+48 601 234 567"},"phoneAlternative":{"type":"string"},"note":{"type":"string"},"contactStatusId":{"description":"1 = aktywny (default)","type":"integer"},"ownerUserId":{"description":"Default = użytkownik klucza API. Opiekunem może być tylko aktywny użytkownik - konto nieaktywne albo techniczne CRM odrzuca.","type":"integer"},"externalId":{"description":"Klucz integracji (tylko przy tworzeniu)","type":"string"},"contractorId":{"description":"Od razu powiąż z kontrahentem jako głównym. Przy podpięciu do istniejącego kontaktu: dopina powiązanie i ustawia ten kontrahent jako główny (od wersji API 2.10.0).","type":"integer","example":121},"contractorIds":{"description":"Pełna lista kontrahentów kontaktu, pierwszy = główny (nowy kontakt: wszystkie powiązania; podpięcie do istniejącego: ZASTĘPUJE dotychczasową listę). Podane razem z contractorId: contractorId zostaje głównym, reszta listy za nim. Pusta lista = 422 (odpięcie od wszystkich kontrahentów nie jest dostępne przez API). Dostępne od wersji API 2.10.0.","type":"array","items":{"type":"integer"},"example":[121,125]},"customField":{"description":"Pola wielowartościowe (multiselect, produkty, szanse sprzedaży) przyjmują listę wartości, np. [\"12\",\"15\"] - pojedyncza wartość jest równoważna liście jednoelementowej. Lista dostępna od wersji API 2.9.0.","type":"object","additionalProperties":{"nullable":true}},"duplicateCheck":{"description":"Pola, po których szukamy istniejącego kontaktu, w kolejności priorytetu (default: [\"email\",\"phone\"]) Sprawdzane są wyłącznie pola, których wartość jest w tym samym żądaniu; pominięte wracają w info.warnings.duplicateCheck, a gdy żadne nie ma wartości - 422 body.duplicateCheckUnusable. Opcja `requireDuplicateCheck: true` (tryb dla importów, fail-closed) wymaga, by KAŻDE pole z listy miało wartość - brak choćby jednego (np. klucza integracji przy podanym NIP-ie) kończy się 422 wskazującym to konkretne pole, a nie samo duplicateCheck; działa też przy domyślnym zestawie. Opcja dostępna od wersji API 1.5.1 (wersja instancji: GET /v2/health).","type":"array","items":{"type":"string"},"example":["email","phone"]},"allowDuplicates":{"type":"boolean","default":false}},"type":"object"}}}},"responses":{"201":{"description":"Kontakt utworzony","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/Contact"},"info":{"$ref":"#/components/schemas/WriteInfo"}},"type":"object"}}}},"200":{"description":"Kontakt już istnieje - dane podpięte do istniejącego, info.duplicate wskazuje pole, po którym go znaleziono","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/Contact"},"info":{"$ref":"#/components/schemas/WriteInfo"}},"type":"object"}}}},"422":{"description":"Błędy walidacji - lista {field, code, message}","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień do kontaktów","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/contacts/{id}":{"get":{"tags":["Contacts"],"summary":"Pojedynczy kontakt po id","description":"Pełne dane kontaktu wraz z głównym mailem, powiązanymi kontrahentami i polami niestandardowymi. Wystarczy id kontaktu, bez znajomości kontrahenta.","operationId":"getContact","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":50}}],"responses":{"200":{"description":"Kontakt","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/Contact"}},"type":"object"}}}},"404":{"description":"Brak kontaktu o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"put":{"tags":["Contacts"],"summary":"Aktualizacja kontaktu (partial)","description":"Częściowa aktualizacja kontaktu - wysyłasz TYLKO pola do zmiany\n(nadpisanie, w odróżnieniu od podpinania w POST), reszta zostaje\nnietknięta. `email` DOKŁADA adres (istniejące zostają). `externalId` tylko-create.\n\n**Kontrahenci kontaktu** (od wersji API 2.10.0):\n- `contractorId` dopina istniejący kontakt do kontrahenta BEZ tworzenia duplikatu\n  i ustawia go jako kontrahenta GŁÓWNEGO (`contractorId` w odczycie, pierwszy\n  na kartotece kontaktu w panelu). Dotychczasowe powiązania zostają, schodzą niżej.\n  Kontakt bez kontrahenta: pierwsze powiązanie = główny. Ten sam kontrahent\n  ponownie = bez zmian.\n- `contractorIds` ZASTĘPUJE całą listę powiązań (pierwszy = główny). Kontrahent\n  usunięty z listy traci też powiązanie tego kontaktu ze swoimi szansami sprzedaży\n  i zgłoszeniami (zachowanie CRM). Pusta lista = 422.\n- oba naraz: `contractorId` zostaje głównym, reszta `contractorIds` za nim.\n\nDo wersji 2.9.x `contractorId` dokładał powiązanie na koniec listy i przy\nistniejących powiązaniach NIE zmieniał głównego.","operationId":"updateContact","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":50}}],"requestBody":{"description":"Pola do zmiany (podzbiór pól z POST + customField)","required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Kontakt po aktualizacji","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/Contact"},"info":{"$ref":"#/components/schemas/WriteInfo"}},"type":"object"}}}},"404":{"description":"Brak kontaktu o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Błędy walidacji","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień do kontaktów","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/contacts/upsert":{"post":{"tags":["Contacts"],"summary":"Batch upsert kontaktów (create albo podpięcie danych)","description":"Batch create-or-attach: dla każdego itemu API sprawdza, czy kontakt już\nistnieje (pola z `duplicateCheck` w kopercie, default email/phone) -\n**nie istnieje → create**, **istnieje → podpięcie danych** (semantyka\njak w POST /v2/contacts: uzupełnianie pustych pól, dokładanie\nmaili/powiązań, nadpisanie customField).\n\nKoperta: `items` (1-100 obiektów jak payload POST, bez opcji) +\n`duplicateCheck` wspólne dla paczki. HTTP zawsze **200** przy poprawnej\nkopercie - wynik per item w `data.results[]`\n(`status: created|attached|failed`), podsumowanie w `info.summary`.\nBłąd jednego itemu nie przerywa paczki; bez transakcji między itemami.","operationId":"upsertContacts","requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["items"],"properties":{"createdAt":{"description":"IMPORT DANYCH: data utworzenia rekordu (ISO 8601, strefa instancji). Pozwala zachować chronologię przy wciąganiu historii sprzed lat - bez niej rekord dostaje datę importu. Tylko przy tworzeniu: przy aktualizacji zwracamy 422 (pomyłkę w imporcie poprawia się kasując rekord i importując ponownie). Data z przyszłości = 422. Dostępne od wersji API 1.16.0.","type":"string","format":"date-time","example":"2023-05-17T09:30:00"},"creatorUserId":{"description":"IMPORT DANYCH: użytkownik, który ma figurować jako twórca rekordu (GET /v2/users). Brak = użytkownik przypisany do klucza API. Zapis wykonuje się w kontekście tej osoby, więc obowiązują JEJ uprawnienia. Dostępne od wersji API 1.16.0.","type":"integer","example":42},"items":{"description":"Lista kontaktów (payload jak POST /v2/contacts, bez opcji) - max 100","type":"array","items":{"type":"object"},"example":[{"firstName":"Jan","lastName":"Kowalski","email":"jan@acme.pl","contractorId":121}]},"duplicateCheck":{"type":"array","items":{"type":"string"},"example":["email","phone"]}},"type":"object"}}}},"responses":{"200":{"description":"Wynik per item (multi-status w body) + podsumowanie","content":{"application/json":{"schema":{"properties":{"data":{"properties":{"results":{"type":"array","items":{"properties":{"index":{"type":"integer"},"status":{"type":"string","enum":["created","attached","failed"]},"contactId":{"type":"integer"},"matchedBy":{"description":"Pole, po którym znaleziono istniejący kontakt (tylko status=attached)","type":"string"},"warnings":{"type":"object"},"errors":{"type":"array","items":{"type":"object"}}},"type":"object"}}},"type":"object"},"info":{"properties":{"summary":{"properties":{"total":{"type":"integer"},"created":{"type":"integer"},"attached":{"type":"integer"},"failed":{"type":"integer"}},"type":"object"}},"type":"object"}},"type":"object"}}}},"422":{"description":"Zepsuta koperta requestu - itemy nie były przetwarzane","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień do kontaktów","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/contractors":{"get":{"tags":["Contractors"],"summary":"Lista kontrahentów z filtrowaniem po dowolnym polu i polach niestandardowych","description":"Podstawowe źródło kontrahentów dla integracji (ERP, import, synchronizacja) i aplikacji.\n\n**Synchronizacja przyrostowa:** przekaż `updatedAfter` z datą ostatniego udanego syncu -\ndostaniesz wyłącznie rekordy utworzone lub zmienione po tej chwili (`updatedAt` obejmuje\ntakże świeżo utworzone rekordy). Koniec ze skanowaniem całej bazy.\n\n**Matchowanie rekordów integracji:** filtruj po swoim kluczu, np. polu niestandardowym\n`customField[Optima ID]=8123` albo `taxId=5252344078` (dopasowanie dokładne).\nPusta wartość pola niestandardowego (`customField[Optima ID]=`) znajduje rekordy,\nktóre NIE mają ustawionej wartości - typowo \"jeszcze niepowiązane z ERP\".\n\nPola tekstowe (`name`, `email`, `phone`...) filtrują częściowo (zawiera),\nidentyfikatory i klucze integracyjne (`taxId`, `externalId`, `*Id`) - dokładnie.\n\nPrzykład - nowi/zmienieni kontrahenci oznaczeni do synchronizacji z Optimą:\n```\nGET /v2/contractors?updatedAfter=2026-08-01T00:00:00Z&customField[Synchronizuj z Optima]=5&include=address\n```","operationId":"listContractors","parameters":[{"name":"updatedAfter","in":"query","description":"Tylko rekordy utworzone/zmienione PO tej chwili (ISO 8601). Fundament syncu przyrostowego.","required":false,"schema":{"type":"string","format":"date-time","example":"2026-08-01T00:00:00Z"}},{"name":"updatedBefore","in":"query","description":"Górna granica zakresu zmian (ISO 8601, włącznie).","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"createdAfter","in":"query","description":"Tylko rekordy utworzone po tej chwili (ISO 8601).","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"createdBefore","in":"query","description":"Tylko rekordy utworzone do tej chwili (ISO 8601, włącznie).","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"customField","in":"query","description":"Filtry pól niestandardowych: `customField[<klucz>]=<wartość>` (dokładne; klucze i typy w GET /v2/<encja>/custom-fields). Pusta wartość = pole nieustawione.","required":false,"style":"deepObject","explode":true,"schema":{"type":"object","example":{"contractor_str_2":"8123"},"additionalProperties":{"type":"string"}}},{"name":"name","in":"query","description":"Filtr częściowy (zawiera). Analogicznie: alias, fullName, email, phone, note. Uwaga: domain filtruje DOKŁADNIE - to klucz sprawdzania, czy kontrahent już istnieje.","required":false,"schema":{"type":"string"}},{"name":"taxId","in":"query","description":"NIP - dopasowanie dokładne. Analogicznie dokładne: externalId, regon, pesel, country, revenueCurrency oraz wszystkie pola `*Id`.","required":false,"schema":{"type":"string","example":"5252344078"}},{"name":"phone","in":"query","description":"Filtr częściowy (zawiera) po numerze telefonu firmy, dosłownie po zapisie w bazie. Do dopasowania DOKŁADNEGO (kontrahenci i kontakty naraz, niezależnie od zapisu z plusem/bez) służy GET /v2/lookup/phone.","required":false,"schema":{"type":"string","example":"601234"}},{"name":"id","in":"query","required":false,"description":"Filtr po polu id - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"alias","in":"query","required":false,"description":"Filtr po polu alias - dopasowanie częściowe (zawiera).","schema":{"type":"string"}},{"name":"fullName","in":"query","required":false,"description":"Filtr po polu fullName - dopasowanie częściowe (zawiera).","schema":{"type":"string"}},{"name":"regon","in":"query","required":false,"description":"Filtr po polu regon - dopasowanie dokładne.","schema":{"type":"string"}},{"name":"pesel","in":"query","required":false,"description":"Filtr po polu pesel - dopasowanie dokładne.","schema":{"type":"string"}},{"name":"email","in":"query","required":false,"description":"Filtr po polu email - dopasowanie częściowe (zawiera).","schema":{"type":"string"}},{"name":"domain","in":"query","required":false,"description":"Filtr po polu domain - dopasowanie dokładne.","schema":{"type":"string"}},{"name":"country","in":"query","required":false,"description":"Filtr po polu country - dopasowanie dokładne.","schema":{"type":"string"}},{"name":"externalId","in":"query","required":false,"description":"Filtr po polu externalId - dopasowanie dokładne.","schema":{"type":"string"}},{"name":"note","in":"query","required":false,"description":"Filtr po polu note - dopasowanie częściowe (zawiera).","schema":{"type":"string"}},{"name":"contractorTypeId","in":"query","required":false,"description":"Filtr po polu contractorTypeId - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"contractorStatusId","in":"query","required":false,"description":"Filtr po polu contractorStatusId - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"industryId","in":"query","required":false,"description":"Filtr po polu industryId - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"contractorSourceId","in":"query","required":false,"description":"Filtr po polu contractorSourceId - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"contractorPriorityId","in":"query","required":false,"description":"Filtr po polu contractorPriorityId - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"paymentTypeId","in":"query","required":false,"description":"Filtr po polu paymentTypeId - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"legalFormId","in":"query","required":false,"description":"Filtr po polu legalFormId - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"ownerUserId","in":"query","required":false,"description":"Filtr po polu ownerUserId - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"parentId","in":"query","required":false,"description":"Filtr po polu parentId - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"employeesCount","in":"query","required":false,"description":"Filtr po polu employeesCount - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"revenueCurrency","in":"query","required":false,"description":"Filtr po polu revenueCurrency - dopasowanie dokładne.","schema":{"type":"string"}},{"name":"sort","in":"query","description":"Pole sortowania: id, name, alias, createdAt, updatedAt, lastActivityAt.","required":false,"schema":{"type":"string","default":"updatedAt"}},{"name":"sortDir","in":"query","required":false,"schema":{"type":"string","default":"asc","enum":["asc","desc"]}},{"name":"page","in":"query","description":"Strona wyników, od 1.","required":false,"schema":{"type":"integer","default":1,"maximum":1000000,"minimum":1}},{"name":"limit","in":"query","description":"Rozmiar strony (max 1000).","required":false,"schema":{"type":"integer","default":100,"maximum":1000,"minimum":1}},{"name":"include","in":"query","description":"Dane powiązane, CSV. Dostępne: `address`. `customField` jest zawsze w odpowiedzi.","required":false,"schema":{"type":"string","example":"address"}}],"responses":{"200":{"description":"Lista kontrahentów + metadane stronicowania","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Contractor"}},"pagination":{"$ref":"#/components/schemas/Pagination"}},"type":"object"}}}},"400":{"description":"Błąd walidacji zapytania - lista błędów w kontrakcie {field, code, message}","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"post":{"tags":["Contractors"],"summary":"Utworzenie kontrahenta","description":"Tworzy kontrahenta tą samą ścieżką zapisu, której używa aplikacja Tillio\n(walidacje, historia zmian i automatyzacje CRM działają jak przy ręcznym dodaniu).\n\n**Zachowanie wokół firmy (dane rejestrowe):**\n- `taxId` (NIP): jeśli firma o tym NIP już istnieje w CRM, kontrahent\n  zostaje z nią powiązany; jeśli nie - firma powstaje,\n  a jej dane (pełna nazwa, REGON, forma prawna) są AUTOMATYCZNIE uzupełniane\n  z rejestru (Tillio Scout), o ile NIP jest prawidłowy.\n- `fullName`, `regon`, `pesel`, `legalFormId` można też podać wprost.\n\n**Higiena danych:** `email`, `phone`, `domain`, `taxId` są normalizowane\ndo jednego standardu zapisu (np. telefon bez spacji, NIP bez separatorów).\nWartość nie do uratowania po normalizacji = błąd 422, nie cichy zapis śmieci.\n\n**Pola niestandardowe:** obiekt `customField` klucz→wartość\n(klucze w GET /v2/contractor/custom-fields), np. `\"Optima ID\"` przy\nzakładaniu rekordu z integracji.\n\n`alias` jest opcjonalny - domyślnie powstaje z `name` (CRM normalizuje go\ndo formy bez znaków specjalnych). Autor rekordu = użytkownik przypisany\ndo klucza API.\n\n**Normalizacja i warnings:** wartość typowana (email/phone/domain/taxId),\nktórej nie da się sprowadzić do standardu, jest zapisywana jako null,\na odpowiedź niesie `info.warnings.<pole> = {valid: false, input: \"oryginał\"}` -\nrequest NIE jest odrzucany, a oryginał można odtworzyć (także z notatki\nsystemowej).\n\n**Ochrona przed duplikatami:** przed zapisem API samo sprawdza, czy taki\nkontrahent już istnieje - domyślnie po `taxId`. Tablicą `duplicateCheck`\n(enum: taxId, phone, email, name, domain oraz `custom:<klucz>`) ustawiasz\npola, po których szukamy, i ICH KOLEJNOŚĆ (priorytet); porównanie po\nwartościach znormalizowanych, tylko niepustych. `custom:<klucz>` = szukanie\npo polu niestandardowym kontrahenta (klucz z\nGET /v2/contractor/custom-fields, wartość podana w `customField`) -\nwyłącznie pola typu INT/STR/VARCHAR (proste identyfikatory, np. \"Optima ID\");\nselect/relacje/pliki dostają 422. Znaleziony duplikat = **HTTP 200**\nz istniejącym rekordem i `info.duplicate = {matchedBy, contractorId}` -\nżadne dane nie są zmieniane. `allowDuplicates: true` wyłącza to sprawdzanie\n(zawsze insert, HTTP 201).\n\n**Flagi:**\n- `createSystemNote` (default **true**) - notatka systemowa (typ \"System\")\n  na kontrahencie: pełne wejście requestu + lista wartości odrzuconych przy\n  normalizacji. Pełni funkcję loga (kiedy, które API, jakie wejście).\n- `createContractorContacts` (default false) - b2c: z `name`/`phone`/`email`\n  powstaje też KONTAKT-osoba powiązany z kontrahentem (imię = pierwszy człon\n  name, nazwisko = reszta).\n\nOdpowiedź zawsze niesie `info.ids` (contractorId, ownerUserId oraz\nnoteId/contactId, jeśli powstały).\n\nSłowniki dla pól `*Id`: /v2/contractor/types, /v2/contractor/statuses,\n/v2/contractor/industries, /v2/contractor/sources, /v2/contractor/priorities,\n/v2/legal-forms; użytkownicy: /v2/users.","operationId":"createContractor","requestBody":{"description":"Dane kontrahenta (camelCase). Wymagane MINIMUM: name + contractorTypeId. Fallbacki dla niepodanych: ownerUserId = użytkownik klucza API; contractorStatusId/industryId/contractorSourceId/contractorPriorityId/paymentTypeId = pierwsza pozycja słownika instancji (najniższe id).","required":true,"content":{"application/json":{"schema":{"required":["name","contractorTypeId"],"properties":{"createdAt":{"description":"IMPORT DANYCH: data utworzenia rekordu (ISO 8601, strefa instancji). Pozwala zachować chronologię przy wciąganiu historii sprzed lat - bez niej rekord dostaje datę importu. Tylko przy tworzeniu: przy aktualizacji zwracamy 422 (pomyłkę w imporcie poprawia się kasując rekord i importując ponownie). Data z przyszłości = 422. Dostępne od wersji API 1.16.0.","type":"string","format":"date-time","example":"2023-05-17T09:30:00"},"creatorUserId":{"description":"IMPORT DANYCH: użytkownik, który ma figurować jako twórca rekordu (GET /v2/users). Brak = użytkownik przypisany do klucza API. Zapis wykonuje się w kontekście tej osoby, więc obowiązują JEJ uprawnienia. Dostępne od wersji API 1.16.0.","type":"integer","example":42},"name":{"type":"string","example":"Acme Sp. z o.o."},"alias":{"description":"Opcjonalny - domyślnie z name","type":"string","example":"acme"},"fullName":{"type":"string","example":"Acme Spółka z ograniczoną odpowiedzialnością"},"taxId":{"description":"NIP - wiąże z istniejącą firmą o tym NIP albo zakłada nową z auto-uzupełnieniem danych rejestrowych","type":"string","example":"5252344078"},"regon":{"type":"string"},"pesel":{"type":"string"},"email":{"type":"string","example":"biuro@acme.pl"},"phone":{"description":"Normalizowany do formatu międzynarodowego (kanon CRM): separatory zdejmowane, 9 cyfr dostaje prefiks +48, prefiks kraju bez plusa dostaje +, mniej niż 9 cyfr = wartość odrzucana (info.warnings). Od wersji API 1.32.0.","type":"string","example":"+48 22 123 45 67"},"domain":{"type":"string","example":"acme.pl"},"country":{"description":"Kod kraju ISO 3166-1 alpha-2 na kartotece (adres pocztowy to osobny obiekt address).","type":"string","example":"PL"},"note":{"type":"string"},"externalId":{"description":"Klucz zewnętrzny integracji - można go też dopiąć później przez PUT. Jedna wartość = jedna kartoteka (zajęta wartość: 422 body.externalIdAlreadyUsed).","type":"string","example":"op1_10023"},"contractorTypeId":{"type":"integer","example":2},"contractorStatusId":{"type":"integer","example":2},"industryId":{"type":"integer","example":1},"contractorSourceId":{"type":"integer","example":1},"contractorPriorityId":{"type":"integer","example":1},"paymentTypeId":{"type":"integer","example":1},"legalFormId":{"type":"integer","example":4},"ownerUserId":{"description":"Opiekun handlowy. Opiekunem może być tylko aktywny użytkownik - konto nieaktywne albo techniczne CRM odrzuca.","type":"integer","example":12345},"parentId":{"type":"integer"},"employeesCount":{"type":"integer"},"revenue":{"type":"string","example":"1250000.00"},"revenueCurrency":{"type":"string","example":"PLN"},"customField":{"description":"Pola wielowartościowe (multiselect, produkty, szanse sprzedaży) przyjmują listę wartości, np. [\"12\",\"15\"] - pojedyncza wartość jest równoważna liście jednoelementowej. Lista dostępna od wersji API 2.9.0.","type":"object","example":{"contractor_str_2":"8123"},"additionalProperties":{"nullable":true}},"address":{"description":"Adresy fizyczne (opcjonalne - brak obiektu = nic nie dodajemy). Każdy adres z addressTypeId wg GET /v2/address/types (podstawowy, korespondencyjny...).","type":"array","items":{"required":["addressTypeId"],"properties":{"addressTypeId":{"description":"Typ adresu z GET /v2/address/types","type":"integer","example":1},"street":{"type":"string","example":"Marszałkowska 1"},"street2":{"type":"string"},"postCode":{"type":"string","example":"00-624"},"city":{"type":"string","example":"Warszawa"},"region":{"type":"string","example":"mazowieckie"},"district":{"type":"string"},"country":{"type":"string","example":"PL"}},"type":"object"}},"duplicateCheck":{"description":"Pola, po których szukamy istniejącego kontrahenta, w kolejności priorytetu (default: [\"taxId\"]). Poza enumem także \"custom:<klucz>\" - szukanie po polu niestandardowym kontrahenta typu INT/STR/VARCHAR (klucz z GET /v2/contractor/custom-fields). WAŻNE: sprawdzane są tylko te pola, których wartość jest w TYM SAMYM żądaniu (np. \"custom:optima_id\" wymaga customField[optima_id]); pominięte pola wracają w info.warnings.duplicateCheck, a gdy żadne z podanych pól nie ma wartości - 422 body.duplicateCheckUnusable zamiast cichego założenia duplikatu. Opcja `requireDuplicateCheck: true` (tryb dla importów, fail-closed) wymaga, by KAŻDE pole z listy miało wartość - brak choćby jednego (np. klucza integracji przy podanym NIP-ie) kończy się 422 wskazującym to konkretne pole, a nie samo duplicateCheck; działa też przy domyślnym zestawie. Opcja dostępna od wersji API 1.5.1 (wersja instancji: GET /v2/health).","type":"array","items":{"type":"string","example":"custom:optima_id"},"example":["custom:optima_id","taxId"]},"taxIdLookup":{"description":"Weryfikacja polskiego NIP w GUS (Tillio Scout). validateOnly = sprawdź istnienie; validateAndUpdate = dodatkowo nadpisz dane wejściowe danymi rejestrowymi (name = przyjazna nazwa, fullName, regon, legalFormId, adres rejestrowy). Adres z GUS ląduje pod typem PODSTAWOWY i wygrywa z adresem typu Podstawowy przesłanym w `address` (od wersji API 1.30.0; wcześniej trafiał pod typ „Inny\"); adresy przesłane pod innymi typami zostają nietknięte. Geokodowanie adresu wykonuje CRM, o ile moduł Mapy jest aktywny W MOMENCIE wywołania (przy imporcie włącz Mapę przed startem skryptu; późniejsze włączenie modułu geokoduje istniejące adresy). Polski NIP ZAWSZE przechodzi walidację składni i sumy kontrolnej, niezależnie od tej flagi.","type":"string","enum":["validateOnly","validateAndUpdate"],"nullable":true},"failOnInvalidTaxId":{"description":"true = twarde 422 zamiast zapisu, gdy polski NIP jest niepoprawny (składnia/suma kontrolna) albo nie istnieje w GUS (przy włączonym taxIdLookup i działającym GUS). Niedostępność GUS nigdy nie blokuje zapisu.","type":"boolean","default":false},"allowDuplicates":{"description":"true = bez sprawdzania duplikatów, zawsze insert","type":"boolean","default":false},"createSystemNote":{"description":"Notatka systemowa z wejściem requestu (log)","type":"boolean","default":true},"createContractorContacts":{"description":"b2c: utwórz też kontakt-osobę z name/phone/email","type":"boolean","default":false}},"type":"object"}}}},"responses":{"201":{"description":"Kontrahent utworzony - pełny rekord + info (ids/warnings) + nagłówek Location","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/Contractor"},"info":{"$ref":"#/components/schemas/WriteInfo"}},"type":"object"}}}},"200":{"description":"Kontrahent już istnieje - zwrócony istniejący rekord, info.duplicate wskazuje pole, po którym go znaleziono; żadne dane nie zostały zmienione","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/Contractor"},"info":{"$ref":"#/components/schemas/WriteInfo"}},"type":"object"}}}},"422":{"description":"Błędy walidacji - lista {field, code, message} (wszystkie naraz)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień do tworzenia kontrahentów","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/contractors/{id}":{"get":{"tags":["Contractors"],"summary":"Pojedynczy kontrahent po id","description":"Pełne dane kontrahenta wraz z polami niestandardowymi i adresami.","operationId":"getContractor","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":121}}],"responses":{"200":{"description":"Kontrahent","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/Contractor"}},"type":"object"}}}},"404":{"description":"Brak kontrahenta o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"put":{"tags":["Contractors"],"summary":"Aktualizacja kontrahenta (partial)","description":"Częściowa aktualizacja kontrahenta - wysyłasz TYLKO pola do zmiany, reszta\nzostaje nietknięta. Ta sama ścieżka zapisu co edycja w aplikacji Tillio\n(walidacje, historia zmian, automatyzacje).\n\n**Pola edytowalne:** name, alias, fullName, email, phone, domain, note,\nrevenue, revenueCurrency, employeesCount, ownerUserId, taxId,\nregon, pesel, legalFormId, **externalId**, **country**, **parentId**,\n**contractorStatusId**, **contractorPriorityId**, **industryId**, **contractorSourceId**,\n**paymentTypeId** oraz `customField` (obiekt klucz→wartość) i `address`.\n\n**`parentId` (struktura kapitałowa, dostępne od wersji API 1.31.0):**\nustawia kontrahenta nadrzędnego (podmiana, gdy inny już jest przypięty),\n`parentId: null` wypina ze struktury. Obowiązują reguły CRM: jeden rodzic,\nrelacja zwrotna A↔B odrzucana (422 `contractor.relatedExist`).\n\n**Przypięcia słownikowe (contractorStatusId, contractorPriorityId, industryId, contractorSourceId,\npaymentTypeId - dostępne od wersji API 1.31.0):** ta sama ścieżka co\nzmiana na kartotece w aplikacji Tillio (odświeża datę aktywności).\nKolumny są wymagane w CRM, więc przyjmują wyłącznie id istniejącej\npozycji słownika - czyszczenie (null) nie jest możliwe.\n\n**`externalId` po utworzeniu:** można go dopiąć do istniejącej kartoteki\n(migracja rekordów sprzed integracji). Obowiązuje reguła z CRM - jedno\n`externalId` wskazuje najwyżej jedną kartotekę; próba przypisania wartości\nzajętej przez innego kontrahenta = 422 `body.externalIdAlreadyUsed`.\nWyczyszczenie: `externalId: null`.\n\n**Pole tylko przy tworzeniu (POST):** `contractorTypeId` - typ kontrahenta jest\nz projektu CRM NIEZMIENNY przez cały cykl życia rekordu (decyzja\nproduktowa): kontrahent innego typu to NOWY rekord, nie edycja.\nPUT odpowiada 422 `body.fieldNotUpdatable` zamiast po cichu zignorować\nwartość.\n\n**Czyszczenie pola:** null albo pusty string = jawne wyczyszczenie wartości.\n\n**Normalizacja i warnings:** jak w POST - wartość typowana nie do\nuratowania NIE nadpisuje istniejącej (pomijana + `info.warnings`).\n\n`taxId` działa jak w POST: wiąże z istniejącą firmą o tym NIP (albo\nzakłada nową) + auto-uzupełnienie danych rejestrowych.","operationId":"updateContractor","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":121}}],"requestBody":{"description":"Pola do zmiany (podzbiór pól z POST + customField)","required":true,"content":{"application/json":{"schema":{"properties":{"name":{"type":"string","example":"Acme Sp. z o.o."},"parentId":{"description":"Kontrahent nadrzędny (struktura kapitałowa): id = ustaw/podmień, null = wypnij. Dostępne od wersji API 1.31.0.","type":"integer","example":121,"nullable":true},"contractorStatusId":{"description":"Status kontrahenta (GET /v2/contractor/statuses). Dostępne od wersji API 1.31.0.","type":"integer","example":4},"contractorPriorityId":{"description":"Priorytet/segment (GET /v2/contractor/priorities). Dostępne od wersji API 1.31.0.","type":"integer","example":2},"industryId":{"description":"Branża (GET /v2/contractor/industries). Dostępne od wersji API 1.31.0.","type":"integer","example":3},"contractorSourceId":{"description":"Źródło pozyskania (GET /v2/contractor/sources). Dostępne od wersji API 1.31.0.","type":"integer","example":1},"paymentTypeId":{"description":"Typ płatności (GET /v2/contractor/payment-types). Dostępne od wersji API 1.31.0.","type":"integer","example":1},"email":{"type":"string","example":"nowy@acme.pl"},"phone":{"description":"Normalizowany do formatu międzynarodowego (9 cyfr = +48..., prefiks bez plusa dostaje +, mniej niż 9 cyfr = wartość odrzucana - info.warnings). Od wersji API 1.32.0.","type":"string","example":"+48 601 234 567"},"ownerUserId":{"description":"Opiekun handlowy. Opiekunem może być tylko aktywny użytkownik - konto nieaktywne albo techniczne CRM odrzuca.","type":"integer","example":12345},"taxId":{"type":"string","example":"5252344078"},"externalId":{"description":"Dopięcie klucza integracji do istniejącej kartoteki; null czyści. Zajęty przez innego kontrahenta = 422 body.externalIdAlreadyUsed.","type":"string","example":"op1_10023","nullable":true},"country":{"description":"Kod kraju ISO 3166-1 alpha-2 na kartotece.","type":"string","example":"PL"},"customField":{"description":"Pola wielowartościowe (multiselect, produkty, szanse sprzedaży) przyjmują listę wartości, np. [\"12\",\"15\"] - pojedyncza wartość jest równoważna liście jednoelementowej. Lista dostępna od wersji API 2.9.0.","type":"object","example":{"contractor_str_2":"8123"},"additionalProperties":{"nullable":true}},"address":{"description":"Adresy do DODANIA (bez id; addressTypeId wg GET /v2/address/types). Istniejące adresy zostają.","type":"array","items":{"required":["addressTypeId"],"properties":{"addressTypeId":{"type":"integer","example":4},"street":{"type":"string"},"postCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string","example":"PL"}},"type":"object"}}},"type":"object"}}}},"responses":{"200":{"description":"Zaktualizowany rekord + info (warnings)","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/Contractor"},"info":{"$ref":"#/components/schemas/WriteInfo"}},"type":"object"}}}},"404":{"description":"Brak kontrahenta o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Błędy walidacji / pole niedostępne do edycji / brak pól do zmiany","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień do edycji kontrahentów","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/contractors/upsert":{"post":{"tags":["Contractors"],"summary":"Batch upsert kontrahentów (create albo update po duplicateCheck)","description":"Batch: dla każdego itemu API sprawdza, czy taki kontrahent już istnieje\n(pola z `duplicateCheck`, jak w POST - także `custom:<klucz>`):\n- **nie istnieje → create** pełną ścieżką POST (defaulty, NIP/GUS, adresy,\n  notatka, kontakt b2c),\n- **istnieje → update** znalezionego rekordu polami itemu (ścieżka PUT).\n  Pola tylko-create (contractorTypeId, contractorStatusId, contractorSourceId...) są na tej gałęzi\n  pomijane bez błędu - item niesie je z konieczności, bo musi być\n  kompletny na wypadek create'a. Kontakt b2c nie jest dotykany.\n  `taxIdLookup=validateAndUpdate` odświeża dane rejestrowe także\n  istniejącego rekordu.\n\n**Koperta:** `items` (1-100 obiektów jak payload POST) + opcje wspólne\ndla całej paczki (`duplicateCheck`, `createSystemNote`,\n`createContractorContacts`, `taxIdLookup`, `failOnInvalidTaxId`).\nOpcji nie podaje się w itemach. `allowDuplicates` nie ma tu zastosowania.\n\n**Wynik per item, nie per request:** błąd walidacji jednego itemu NIE\nprzerywa paczki - HTTP zawsze **200** przy poprawnej kopercie,\na `data.results[]` (w kolejności wejścia) niesie dla każdego itemu\n`status: created|updated|failed` + id/warnings albo listę błędów.\n`info.summary` podsumowuje liczbowo. 422 dostaje tylko zepsuta koperta\n(items nie-tablica/pusta/ponad limit, złe opcje wspólne).\n\n**Bez transakcji między itemami** (batch = pętla po zapisach core,\npoprawność > szybkość): częściowy sukces jest normalny, `results`\nmówi dokładnie, co przeszło. Notatka systemowa (default włączona)\nloguje każdy zapis: \"Utworzono...\" albo \"Zaktualizowano... za pomocą API\".","operationId":"upsertContractors","requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["items"],"properties":{"items":{"description":"Lista kontrahentów (payload jak POST /v2/contractors, bez opcji) - max 100","type":"array","items":{"type":"object"},"example":[{"name":"ACME Sp. z o.o.","contractorTypeId":2,"taxId":"5252344078","customField":{"optima_id":"OPT-8123"}}]},"duplicateCheck":{"description":"Pola, po których szukamy istniejącego kontrahenta - wspólne dla paczki (default: [\"taxId\"]; także \"custom:<klucz>\")","type":"array","items":{"type":"string"},"example":["custom:optima_id","taxId"]},"taxIdLookup":{"type":"string","enum":["validateOnly","validateAndUpdate"],"nullable":true},"failOnInvalidTaxId":{"type":"boolean","default":false},"createSystemNote":{"type":"boolean","default":true},"createContractorContacts":{"type":"boolean","default":false}},"type":"object"}}}},"responses":{"200":{"description":"Wynik per item (multi-status w body) + podsumowanie","content":{"application/json":{"schema":{"properties":{"data":{"properties":{"results":{"type":"array","items":{"properties":{"index":{"description":"Pozycja itemu w items","type":"integer","example":0},"status":{"type":"string","enum":["created","updated","failed"],"example":"updated"},"contractorId":{"type":"integer","example":950301},"matchedBy":{"description":"Pole, po którym znaleziono istniejący rekord (tylko status=updated)","type":"string","example":"custom:optima_id"},"noteId":{"type":"integer"},"contactId":{"type":"integer"},"warnings":{"description":"Jak info.warnings w POST","type":"object"},"errors":{"description":"Tylko status=failed - lista {field, code, message}","type":"array","items":{"type":"object"}}},"type":"object"}}},"type":"object"},"info":{"properties":{"summary":{"properties":{"total":{"type":"integer","example":3},"created":{"type":"integer","example":1},"updated":{"type":"integer","example":1},"failed":{"type":"integer","example":1}},"type":"object"}},"type":"object"}},"type":"object"}}}},"422":{"description":"Zepsuta koperta requestu (items/opcje wspólne) - itemy nie były przetwarzane","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień do zapisu kontrahentów","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/{entity}/custom-fields":{"get":{"tags":["CustomFields"],"summary":"Definicje pól niestandardowych encji","description":"Słownik pól niestandardowych encji: klucze (do filtrów `customField[<klucz>]`\ni odczytu wartości z obiektów), typy oraz konfiguracja (np. opcje selectów -\nwartości pól typu select przychodzą jako id opcji, etykiety znajdziesz w `config`).\n\n**Kluczem jest zawsze `key`** (np. `contractor_str_2`), nigdy etykieta z `name`\nczy `displayName`. Ta sama zasada obowiązuje w odczycie (`customField.<key>`),\nw filtrach (`customField[<key>]=`) i przy zapisie encji - etykieta służy tylko\ndo pokazania pola człowiekowi.\n\nIntegracja ERP typowo szuka tu swoich pól (\"<System> ID\", \"Synchronizuj z <System>\")\nprzed pierwszą synchronizacją, np. `GET /v2/contractor/custom-fields`.\n\nEncje przypisujące pola do podtypów rekordów (note, ticket, service, lead,\npipeline) zwracają dodatkowo `assignedTo` - listę id podtypów, w których pole\ndziała (rozszerzysz ją przez PUT /v2/{entity}/custom-fields/{key}).\nDostępne od wersji API 1.28.0.","operationId":"listCustomFields","parameters":[{"name":"entity","in":"path","description":"Encja, której pola zwrócić.","required":true,"schema":{"type":"string","enum":["contractor","pipeline","contact","note","service","task","project","ticket","lead"],"example":"contractor"}}],"responses":{"200":{"description":"Definicje pól","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/CustomFieldDefinition"}}},"type":"object"}}}},"400":{"description":"Encja bez pól niestandardowych","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/custom-fields":{"post":{"tags":["CustomFields"],"summary":"Nowe pole niestandardowe (provisioning integracji)","description":"**Provisioning pól niestandardowych** - do instalacji integracji\n(np. pole \"Optima ID\" typu STR + \"Synchronizuj z Optimą\" typu SELECT).\nPole tworzy core CRM: klucz (`key`) jest generowany automatycznie,\netykieta (`name`) musi być unikalna w encji.\n\n**Wymaga klucza API z pełnymi uprawnieniami** (superadmin) - tworzenie\npól to zmiana konfiguracji instancji.\n\n- `type`: pełny zestaw typów interfejsu CRM - patrz enum pola\n  (od wersji API 1.20.0, wcześniej tylko STR/TEXT/DATETIME/INT/SELECT/MULTISELECT).\n- `options` - wymagane dla SELECT/MULTISELECT (lista stringów albo\n  obiektów {name, color}).\n- `assignedTo` - lista id podtypów rekordów. **WYMAGANE** dla encji\n  przypisujących pola do podtypów: `note` (typy notatek), `ticket`\n  (procesy zgłoszeń), `service` (pozycje katalogu usług), `lead`\n  i `pipeline` (grupy statusów/etapów) - bez niego 422 `body.required`.\n  Pole działa wyłącznie w podanych podtypach; „pole globalne\" = pole\n  przypisane do wszystkich podtypów naraz (i do rozszerzenia przy\n  dokładaniu kolejnego podtypu). Dla `contractor`/`contact`/`task`/\n  `project` pomijane.\n- `editableBy` - ACL pola: {userIds, groupIds, departmentIds}; pole\n  widzą/edytują wyłącznie wskazani (np. tylko admini integracji);\n  brak = wszyscy.\n\nOdpowiedź niesie wygenerowany `key` - używaj go w `customField[<key>]`\ni w zapisach.","operationId":"createCustomField","requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["entity","name","type"],"properties":{"entity":{"type":"string","enum":["contractor","pipeline","contact","note","service","task","project","ticket","lead"],"example":"contractor"},"name":{"description":"Etykieta pola (unikalna w encji) - to, co widzi użytkownik w CRM. UWAGA: nie jest to klucz. Klucz operacyjny (`key`, np. `contractor_str_5`) nadaje CRM i zwracamy go w odpowiedzi - i to nim potem odczytujesz, filtrujesz i zapisujesz wartości.","type":"string","example":"Optima ID"},"type":{"description":"Typ pola TAKI JAK W INTERFEJSIE CRM (od wersji API 1.20.0 dostępny pełny zestaw).\n\n**Lista zależy od wersji CRM instalacji** - typy dochodzą z czasem\n(`SALESPIPELINE`, czyli pole wskazujące szansę sprzedaży, jest w nowszych\nwydaniach), a część wymaga włączonego modułu. Typ nieobsługiwany przez\ndaną instalację kończy się 422 z listą typów dostępnych właśnie tam.\nKolumnę bazową dobiera CRM: `EMAIL`/`PHONE`/`URL`/`PASSWORD`/`DOMAIN` zapisują się\njako VARCHAR, `ADDRESS` jako STR, `DURATION` jako INT, a `INT` z `config.precision > 0`\njako DECIMAL. Pola typu `ADDRESS` trafiają na mapę, `URL` jest klikalny,\n`PHONE`/`EMAIL` dostają walidację formatu.","type":"string","enum":["STR","TEXT","DATETIME","INT","SELECT","MULTISELECT","USERS","EMAIL","PHONE","DOMAIN","PASSWORD","URL","ADDRESS","DURATION","CONTRACTOR","PRODUCT","PROJECT","FILE","SALESPIPELINE"],"example":"URL"},"options":{"description":"Opcje selecta (SELECT/MULTISELECT)","type":"array","items":{"type":"string"},"example":["TAK","NIE"]},"config":{"description":"Formatowanie pól liczbowych (`INT`). Reszta wariantów wynika z samego `type`,\nwięc `config` nie jest potrzebny do założenia adresu, URL-a czy telefonu.\n\n- `option`: `none` (domyślnie), `percent`, `currency`, `custom`,\n- `position`: `suffix` (domyślnie) albo `prefix`,\n- `precision`: 0-4 (wartość > 0 przełącza kolumnę na DECIMAL),\n- `customLabel`: własny sufiks/prefiks, np. `gr/szt.` (dla `option: custom`),\n- `currencyCode`: kod waluty ISO 4217 (wymaga `option: currency`).\n\nKlucze przyjmujemy też w konwencji CRM (`custom_label`, `currency_code`).\nDostępne od wersji API 1.20.0.","type":"object","example":{"option":"custom","position":"suffix","customLabel":"gr/szt."}},"required":{"type":"boolean","default":false},"assignedTo":{"description":"Id podtypów rekordów. WYMAGANE dla encji note, ticket, service, lead, pipeline (przypisania pól do podtypów) - bez niego 422 body.required. Id spoza słownika podtypów = 422 body.invalidValue z listą brakujących, pole nie powstaje. Dla pozostałych encji pomijane.","type":"array","items":{"type":"integer"}},"editableBy":{"oneOf":[{"$ref":"#/components/schemas/AclJson"}],"nullable":true,"description":"ACL pola - widoczność/edycja tylko dla wskazanych (jednolity kształt ACL API: {userIds, departmentIds, groupIds}). Inny kształt - płaska lista id, nieznany klucz (np. `userId`), wartość skalarna - to 422, NIE „pole dla wszystkich\". Brak albo null = bez ograniczeń."}},"type":"object"}}}},"responses":{"201":{"description":"Pole utworzone - definicja z wygenerowanym key","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/CustomFieldDefinition"},"info":{"$ref":"#/components/schemas/WriteInfo"}},"type":"object"}}}},"422":{"description":"Błędy walidacji (m.in. duplikat etykiety, zły kształt editableBy, nieistniejące podtypy w assignedTo)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień superadmina","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/{entity}/custom-fields/{key}":{"put":{"tags":["CustomFields"],"summary":"Zmiana przypisań pola niestandardowego (assignedTo)","description":"**Zmiana przypisań pola do podtypów rekordów** (`assignedTo`) - typowe użycie:\npo dołożeniu siódmego lejka rozszerzasz nim pola \"globalne\" (czyli przypisane\ndo wszystkich lejków), zamiast przepinać je w panelu albo zakładać od nowa.\n\n`assignedTo` to KOMPLETNA lista docelowa - zastępuje obecne przypisania\n(aktualny stan pola zwraca GET /v2/{entity}/custom-fields, pole `assignedTo`).\n\n**UWAGA - zwężenie listy kasuje dane:** usunięcie podtypu z listy powoduje,\nże CRM kasuje zapisane wartości pola w rekordach tego podtypu. Dlatego lista,\nktóra usuwa istniejące przypisanie, wymaga jawnego `allowUnassign: true` -\nbez niego 422 `customField.unassignNotConfirmed`.\n\nDziała dla encji z przypisaniami pól: note, ticket, service, lead, pipeline.\nPozostałych własności pola (etykieta, typ, opcje) API nie zmienia - panel.\nWymaga klucza API superadmina. Dostępne od wersji API 1.28.0.","operationId":"updateCustomFieldAssignments","parameters":[{"name":"entity","in":"path","required":true,"schema":{"type":"string","enum":["note","ticket","service","lead","pipeline"],"example":"ticket"}},{"name":"key","in":"path","description":"Klucz pola (cff_name) z GET /v2/{entity}/custom-fields","required":true,"schema":{"type":"string","example":"ticket_str_1"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["assignedTo"],"properties":{"assignedTo":{"description":"Docelowa (kompletna) lista id podtypów","type":"array","items":{"type":"integer"},"example":[3,4,5,6,7,8,9]},"allowUnassign":{"description":"Jawna zgoda na usunięcie przypisań (CRM skasuje wartości pola w usuwanych podtypach). WYŁĄCZNIE boolean: zgodę wyraża tylko `true`; napis \"true\"/\"false\", liczba 1/0 albo lista = 422 body.invalidValue, nic nie jest zapisywane.","type":"boolean","default":false}},"type":"object"}}}},"responses":{"200":{"description":"Przypisania po zmianie","content":{"application/json":{"schema":{"properties":{"data":{"properties":{"key":{"type":"string","example":"ticket_str_1"},"assignedTo":{"type":"array","items":{"type":"integer"},"example":[3,4,5,6,7,8,9]}},"type":"object"}},"type":"object"}}}},"404":{"description":"Brak pola o tym kluczu w encji","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Błędy walidacji (m.in. customField.unassignNotConfirmed przy zwężaniu listy bez allowUnassign: true; body.invalidValue, gdy allowUnassign nie jest booleanem albo assignedTo zawiera nieistniejące podtypy)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień superadmina","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/{entity}/{id}/custom-fields/{key}/file":{"get":{"tags":["CustomFields"],"summary":"Plik z pola niestandardowego","description":"Plik z pola niestandardowego typu **FILE** wskazanego rekordu: metadane\ni podpisany, tymczasowy `downloadUrl` (ważny 1 minutę - po tym czasie\nodpytaj końcówkę ponownie).\n\nAdres tej końcówki niesie każdy odczyt rekordu w `customField.<klucz>.fileUrl`,\nwięc nie trzeba go składać samemu. Podpisanego linku odczyt rekordu NIE zawiera:\npodpis to lokalna kryptografia per plik, a strona listy płaciłaby za pliki,\npo które nikt nie sięgnął.\n\n`?download=1` zamiast JSON-a robi przekierowanie (302) prosto na plik -\n`curl -L` z tym adresem pobiera go jednym żądaniem.\n\n`data: null` = pole istnieje, ale nie ma w nim pliku (z `?download=1`: 404).\nDostępne od wersji API 2.6.0.","operationId":"getCustomFieldFile","parameters":[{"name":"entity","in":"path","required":true,"schema":{"type":"string","enum":["contractor","contact","note","lead","ticket","service","project","pipeline"],"example":"note"}},{"name":"id","in":"path","description":"Id rekordu (notatki, kontrahenta, ...)","required":true,"schema":{"type":"integer","example":18270}},{"name":"key","in":"path","description":"Klucz pola (cff_name) z GET /v2/{entity}/custom-fields","required":true,"schema":{"type":"string","example":"note_file_1"}},{"name":"download","in":"query","description":"Zamiast JSON-a przekierowanie (302) na plik","required":false,"schema":{"type":"boolean","default":false}}],"responses":{"200":{"description":"Plik z pola (albo null, gdy pole puste)","content":{"application/json":{"schema":{"properties":{"data":{"oneOf":[{"$ref":"#/components/schemas/CustomFieldFile"}],"nullable":true}},"type":"object"}}}},"302":{"description":"Przekierowanie na plik (tylko z ?download=1)"},"404":{"description":"Brak rekordu, pola o tym kluczu albo pliku w polu (przy ?download=1)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Pole nie jest typu FILE","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"post":{"tags":["CustomFields"],"summary":"Wgranie pliku do pola niestandardowego (multipart)","description":"Wgrywa plik do pola niestandardowego typu **FILE** - request\n**multipart/form-data** z plikiem w polu `file`. Pole mieści JEDEN plik,\nwięc kolejny zapis zastępuje poprzedni (poprzedni przestaje być osiągalny\nz pola - tak samo działa panel Tillio). Limit rozmiaru: 25 MB (twardy limit\nCRM dla pól niestandardowych - załączniki notatki i zadania mają 128 MB).\n\nOsobna końcówka, bo wartością pola FILE jest plik, a nie tekst:\n`customField` w POST/PUT rekordu przyjmuje wyłącznie wartości proste\ni pole plikowe odrzuci.\n\nPole musi być przypisane do podtypu rekordu (typ notatki, proces ticketa,\ngrupa statusu leada, typ usługi, grupa etapów szansy) - inaczej 422\n`customField.notAssigned`, bo CRM zapisałby wartość, której formularz\ni tak nie pokaże. Dostępne od wersji API 2.6.0.","operationId":"uploadCustomFieldFile","parameters":[{"name":"entity","in":"path","required":true,"schema":{"type":"string","enum":["contractor","contact","note","lead","ticket","service","project","pipeline"],"example":"note"}},{"name":"id","in":"path","description":"Id rekordu (notatki, kontrahenta, ...)","required":true,"schema":{"type":"integer","example":18270}},{"name":"key","in":"path","description":"Klucz pola (cff_name) z GET /v2/{entity}/custom-fields","required":true,"schema":{"type":"string","example":"note_file_1"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"required":["file"],"properties":{"file":{"description":"Plik do wgrania (maksymalnie 25 MB)","type":"string","format":"binary"}},"type":"object"}}}},"responses":{"200":{"description":"Plik zapisany w polu","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/CustomFieldFile"}},"type":"object"}}}},"404":{"description":"Brak rekordu albo pola o tym kluczu","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Brak pliku, pole nie jest typu FILE, pole nieprzypisane do podtypu albo plik odrzucony przez CRM","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"delete":{"tags":["CustomFields"],"summary":"Usunięcie pliku z pola niestandardowego","description":"Czyści pole niestandardowe typu **FILE**: kasuje plik z przestrzeni plików\ninstancji i zeruje wartość pola. Pole bez pliku zwraca 204 tak samo -\noperacja jest idempotentna. Dostępne od wersji API 2.6.0.","operationId":"deleteCustomFieldFile","parameters":[{"name":"entity","in":"path","required":true,"schema":{"type":"string","enum":["contractor","contact","note","lead","ticket","service","project","pipeline"],"example":"note"}},{"name":"id","in":"path","description":"Id rekordu (notatki, kontrahenta, ...)","required":true,"schema":{"type":"integer","example":18270}},{"name":"key","in":"path","description":"Klucz pola (cff_name) z GET /v2/{entity}/custom-fields","required":true,"schema":{"type":"string","example":"note_file_1"}}],"responses":{"204":{"description":"Pole wyczyszczone"},"404":{"description":"Brak rekordu albo pola o tym kluczu","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Pole nie jest typu FILE albo CRM odrzucił zmianę","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/contractor/types":{"get":{"tags":["Dictionaries"],"summary":"Typy kontrahenta","description":"Słownik typów kontrahenta (np. klient, lead, dostawca). Mapuje wartości `contractorTypeId` z GET /v2/contractors. **Konfiguracji tego słownika nie da się zmienić przez API** - CRM nie ma dla niego żadnej ścieżki zapisu (pozycje pochodzą z instalacji systemu), więc zmiany wyklikuje się w panelu.","operationId":"listContractorTypes","responses":{"200":{"description":"Pozycje słownika (id, name)","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/DictionaryEntry"}}},"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/contractor/statuses":{"get":{"tags":["Dictionaries"],"summary":"Statusy kontrahenta","description":"Słownik statusów kontrahenta - mapuje `contractorStatusId` z GET /v2/contractors.\nKolejność pozycji jak na liście w CRM (priorytet malejąco). `active=false`\noznacza status wyłączony w konfiguracji - stare rekordy nadal mogą go mieć,\ndlatego zwracamy też pozycje nieaktywne.","operationId":"listContractorStatuses","responses":{"200":{"description":"Pozycje słownika (id, name, color, active)","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/DictionaryEntry"}}},"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"post":{"tags":["Dictionaries"],"summary":"Nowy status kontrahenta","description":"Nowy status kontrahenta (np. „Klient\", „Zapytanie ofertowe\"). Statusów systemowych CRM nie pozwala przemianować - próba edycji takiej pozycji kończy się odmową po stronie CRM.","operationId":"createContractorStatus","requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["name"],"properties":{"name":{"type":"string","example":"Partner"},"color":{"description":"Kolor etykiety (#rrggbb)","type":"string","example":"#4b78c5"},"order":{"description":"Kolejność na liście","type":"integer","example":2},"active":{"type":"boolean","example":true}},"type":"object"}}}},"responses":{"201":{"description":"Pozycja utworzona","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DictionaryEntry"}}}},"422":{"description":"Błąd walidacji albo odmowa CRM (pozycja systemowa, duplikat)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień administracyjnych","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/contractor/sources":{"get":{"tags":["Dictionaries"],"summary":"Źródła pozyskania kontrahenta","description":"Słownik źródeł pozyskania kontrahenta (np. polecenie, kampania, targi). Mapuje `contractorSourceId` z GET /v2/contractors (i leadów - ten sam słownik). Zawiera też pozycje nieaktywne (`active=false`).","operationId":"listContractorSources","responses":{"200":{"description":"Pozycje słownika (id, name, active)","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/DictionaryEntry"}}},"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"post":{"tags":["Dictionaries"],"summary":"Nowy źródło pozyskania","description":"Nowe źródło pozyskania kontrahenta (polecenie, kampania, targi). CRM zapisuje tu wyłącznie nazwę - nie ma koloru ani statusu.","operationId":"createContractorSource","requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["name"],"properties":{"name":{"type":"string","example":"Targi branżowe"},"active":{"description":"Pozycji systemowych CRM nie pozwala wyłączyć","type":"boolean","example":true}},"type":"object"}}}},"responses":{"201":{"description":"Pozycja utworzona","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DictionaryEntry"}}}},"422":{"description":"Błąd walidacji albo odmowa CRM (pozycja systemowa, duplikat)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień administracyjnych","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/contractor/priorities":{"get":{"tags":["Dictionaries"],"summary":"Priorytety kontrahenta","description":"Słownik priorytetów kontrahenta (np. VIP, standard). Mapuje `contractorPriorityId` z GET /v2/contractors. Sortowany po nazwie; zawiera też pozycje nieaktywne (`active=false`).","operationId":"listContractorPriorities","responses":{"200":{"description":"Pozycje słownika (id, name, color, active)","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/DictionaryEntry"}}},"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"post":{"tags":["Dictionaries"],"summary":"Nowy priorytet kontrahenta","description":"Nowy priorytet (segment) kontrahenta. Typowe zastosowanie: postawienie segmentacji klienta automatem przy wdrożeniu, zamiast klikania w panelu.","operationId":"createContractorPriority","requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["name"],"properties":{"name":{"type":"string","example":"Hurt"},"color":{"description":"Kolor etykiety (#rrggbb). Pominięty = neutralny szary.","type":"string","example":"#4b78c5"},"icon":{"description":"Nazwa ikony z zestawu CRM","type":"string","nullable":true},"order":{"description":"Kolejność na liście (rosnąco)","type":"integer","example":1},"active":{"type":"boolean","example":true}},"type":"object"}}}},"responses":{"201":{"description":"Priorytet utworzony","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DictionaryEntry"}}}},"422":{"description":"Błąd walidacji","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/contractor/industries":{"get":{"tags":["Dictionaries"],"summary":"Branże","description":"Słownik branż - mapuje `industryId` z GET /v2/contractors. Sortowany po nazwie; zawiera też pozycje nieaktywne (`active=false`).","operationId":"listIndustries","responses":{"200":{"description":"Pozycje słownika (id, name, active)","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/DictionaryEntry"}}},"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"post":{"tags":["Dictionaries"],"summary":"Nowy branża","description":"Nowa branża kontrahenta. Jak przy źródłach, CRM trzyma tu samą nazwę.","operationId":"createIndustry","requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["name"],"properties":{"name":{"type":"string","example":"Logistyka"},"active":{"description":"Pozycji systemowych CRM nie pozwala wyłączyć","type":"boolean","example":true}},"type":"object"}}}},"responses":{"201":{"description":"Pozycja utworzona","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DictionaryEntry"}}}},"422":{"description":"Błąd walidacji albo odmowa CRM (pozycja systemowa, duplikat)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień administracyjnych","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/contractor/legal-forms":{"get":{"tags":["Dictionaries"],"summary":"Formy prawne","description":"Słownik form prawnych firmy (np. sp. z o.o., JDG). Mapuje `legalFormId` z GET /v2/contractors.","operationId":"listLegalForms","responses":{"200":{"description":"Pozycje słownika (id, name)","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/DictionaryEntry"}}},"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"post":{"tags":["Dictionaries"],"summary":"Nowy forma prawna","description":"Nowa forma prawna firmy (sp. z o.o., JDG, fundacja). Mapuje `legalFormId` z GET /v2/contractors.","operationId":"createLegalForm","requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["name"],"properties":{"name":{"type":"string","example":"Prosta spółka akcyjna"},"order":{"type":"integer","example":1},"active":{"type":"boolean","example":true}},"type":"object"}}}},"responses":{"201":{"description":"Pozycja utworzona","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DictionaryEntry"}}}},"422":{"description":"Błąd walidacji albo odmowa CRM (pozycja systemowa, duplikat)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień administracyjnych","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/contractor/payment-types":{"get":{"tags":["Dictionaries"],"summary":"Typy płatności kontrahenta","description":"Słownik typów płatności kontrahenta (przelew, gotówka, pobranie). Mapuje `paymentTypeId` z GET /v2/contractors. Dostępny od wersji API 1.24.0.","operationId":"listContractorPaymentTypes","responses":{"200":{"description":"Pozycje słownika (id, name, color, active)","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/DictionaryEntry"}}},"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"post":{"tags":["Dictionaries"],"summary":"Nowy typ płatności","description":"Nowy typ płatności kontrahenta (przelew, gotówka, pobranie). Mapuje `paymentTypeId` z GET /v2/contractors.","operationId":"createContractorPaymentType","requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["name"],"properties":{"name":{"type":"string","example":"Przelew 14 dni"},"description":{"description":"Opis widoczny w panelu","type":"string","example":"Standardowy termin dla stałych klientów"},"color":{"type":"string","example":"#4b78c5"},"order":{"type":"integer","example":1},"active":{"type":"boolean","example":true}},"type":"object"}}}},"responses":{"201":{"description":"Pozycja utworzona","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DictionaryEntry"}}}},"422":{"description":"Błąd walidacji albo odmowa CRM (pozycja systemowa, duplikat)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień administracyjnych","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/address/types":{"get":{"tags":["Dictionaries"],"summary":"Typy adresu","description":"Słownik typów adresu (siedziba, korespondencyjny...). Mapuje `addressTypeId` adresów z GET /v2/contractors?include=address oraz z /v2/contractors/{id}/addresses. Zwraca wyłącznie typy aktywne. `isUnique` (od wersji API 1.21.0) mówi, czy typ może wystąpić u kontrahenta tylko raz - typy nieunikalne (np. własny „Punkt odbioru\") można dokładać bez ograniczeń.","operationId":"listAddressTypes","responses":{"200":{"description":"Pozycje słownika (id, name)","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/DictionaryEntry"}}},"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"post":{"tags":["Dictionaries"],"summary":"Nowy typ adresu","description":"Nowy typ adresu kontrahenta. `isUnique: false` pozwala trzymać WIELE adresów tego typu na jednym kontrahencie - tak działają np. punkty odbioru.","operationId":"createAddressType","requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["name"],"properties":{"name":{"type":"string","example":"Punkt odbioru"},"isUnique":{"description":"true = jeden adres tego typu na kontrahenta (domyślnie), false = wiele","type":"boolean","example":false},"active":{"type":"boolean","example":true}},"type":"object"}}}},"responses":{"201":{"description":"Typ adresu utworzony","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DictionaryEntry"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/note/types":{"get":{"tags":["Dictionaries"],"summary":"Typy notatek","description":"Słownik typów notatek CRM (np. telefon, spotkanie, e-mail). Zwraca wyłącznie typy aktywne.","operationId":"listNoteTypes","responses":{"200":{"description":"Pozycje słownika (id, name, color, acl)","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/DictionaryEntry"}}},"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"post":{"tags":["Dictionaries"],"summary":"Nowy typ notatki","description":"Nowy typ notatki. Uwaga: typów systemowych (m.in. notatki API, id 11) CRM nie pozwala edytować ani kasować. `acl` ogranicza widoczność notatek tego typu do wskazanych użytkowników/działów/grup (notatka nie ma własnego ACL - ograniczenia niesie jej typ); jednolity kształt {userIds, departmentIds, groupIds}, dostępne od wersji API 1.29.0.","operationId":"createNoteType","requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["name"],"properties":{"name":{"description":"Maksymalnie 32 znaki","type":"string","example":"Rozmowa telefoniczna"},"color":{"type":"string","example":"#4b78c5"},"icon":{"description":"Ikona z zestawu CRM","type":"string","example":"fa-phone"},"order":{"type":"integer","example":1},"acl":{"oneOf":[{"$ref":"#/components/schemas/AclJson"}],"nullable":true,"description":"Widoczność notatek tego typu: {userIds?, departmentIds?, groupIds?}. Pominięte albo puste = bez ograniczeń."},"active":{"type":"boolean","example":true}},"type":"object"}}}},"responses":{"201":{"description":"Pozycja utworzona","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DictionaryEntry"}}}},"422":{"description":"Błąd walidacji albo odmowa CRM (pozycja systemowa, duplikat)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień administracyjnych","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/calendar/types":{"get":{"tags":["Dictionaries"],"summary":"Rodzaje kalendarzy","description":"Rodzaje kalendarzy dostępne w instancji (kalendarz Tillio, Microsoft, Google). Wartość mapuje pole `calendarTypeId` z GET /v2/calendars. Dostępne od wersji API 2.2.0.","operationId":"listCalendarTypes","responses":{"200":{"description":"Pozycje słownika (id, name)","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/DictionaryEntry"}}},"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/order/statuses":{"get":{"tags":["Dictionaries"],"summary":"Statusy zamówień","description":"Słownik statusów zamówień (dokumentów sprzedażowych) - mapuje status zamówień z modułu Orders.","operationId":"listOrderStatuses","responses":{"200":{"description":"Pozycje słownika (id, name)","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/DictionaryEntry"}}},"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"post":{"tags":["Dictionaries"],"summary":"Nowy status zamówienia","description":"Nowy status zamówienia. CRM trzyma tu samą nazwę - bez koloru i flagi aktywności.","operationId":"createOrderStatus","requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["name"],"properties":{"name":{"type":"string","example":"Wysłane do klienta"}},"type":"object"}}}},"responses":{"201":{"description":"Pozycja utworzona","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DictionaryEntry"}}}},"422":{"description":"Błąd walidacji albo odmowa CRM (pozycja systemowa, duplikat)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień administracyjnych","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/project/statuses":{"get":{"tags":["Dictionaries"],"summary":"Statusy projektu","description":"Słownik statusów projektu - mapuje status projektów z modułu Projects.","operationId":"listProjectStatuses","responses":{"200":{"description":"Pozycje słownika (id, name, color)","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/DictionaryEntry"}}},"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"post":{"tags":["Dictionaries"],"summary":"Nowy status projektu","description":"Nowy status projektu. `isDefault: true` przypisuje status każdemu nowo utworzonemu projektowi (poprzedni domyślny traci tę flagę).","operationId":"createProjectStatus","requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["name"],"properties":{"name":{"type":"string","example":"W realizacji"},"color":{"type":"string","example":"#66bb6a"},"isDefault":{"description":"Status nadawany nowym projektom","type":"boolean","example":false},"passTasks":{"description":"Czy zadania projektu przechodzą dalej przy zmianie statusu","type":"boolean","example":false},"order":{"type":"integer","example":2}},"type":"object"}}}},"responses":{"201":{"description":"Pozycja utworzona","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DictionaryEntry"}}}},"422":{"description":"Błąd walidacji albo odmowa CRM (pozycja systemowa, duplikat)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień administracyjnych","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/user/statuses":{"get":{"tags":["Dictionaries"],"summary":"Statusy użytkownika","description":"Słownik statusów użytkownika systemu (aktywny, zablokowany...). Mapuje `userStatusId` użytkowników zwracanych przez endpointy systemowe. Statusy systemowe - stała lista wbudowana w CRM, bez zapisu przez API.","operationId":"listUserStatuses","responses":{"200":{"description":"Pozycje słownika (id, name)","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/DictionaryEntry"}}},"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/ticket/statuses":{"get":{"tags":["Dictionaries"],"summary":"Statusy zgłoszeń (globalne, z flagą isFinal)","description":"GLOBALNE statusy zgłoszeń (tickets) - konfigurowalne per instancja,\nkolejność jak w widoku zgłoszeń CRM. `isFinal=true` = status zamykający\nzgłoszenie. Mapuje pole `ticketStatusId` zgłoszenia; niezależne od tego etapy\nPROCESU ticketowego (`ticketStageId`) żyją w GET /v2/ticket/processes.","operationId":"listTicketStatuses","responses":{"200":{"description":"Pozycje słownika (id, name, color, isFinal)","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/DictionaryEntry"}}},"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"post":{"tags":["Dictionaries"],"summary":"Nowy status zgłoszenia","description":"Nowy status zgłoszenia. `isFinal: true` oznacza status zamykający - po nim zgłoszenie liczy się jako rozwiązane (SLA, statystyki). Wymaga aktywnego modułu Zgłoszenia. Bez flagi active - słownik statusów zgłoszeń w CRM jej nie ma.","operationId":"createTicketStatus","requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["name"],"properties":{"name":{"type":"string","example":"Oczekuje na dostawcę"},"color":{"type":"string","example":"#FFA726"},"order":{"type":"integer","example":3},"isFinal":{"description":"Status końcowy (zamyka zgłoszenie)","type":"boolean","example":false}},"type":"object"}}}},"responses":{"201":{"description":"Status utworzony","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DictionaryEntry"}}}},"403":{"description":"Moduł Zgłoszenia nieaktywny na tej instancji","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/ticket/sources":{"get":{"tags":["Dictionaries"],"summary":"Źródła zgłoszeń","description":"Źródła zgłoszeń - kanał, którym zgłoszenie wpłynęło (Wiadomość email, Zadanie, Formularz, Telefon). Mapuje pole `ticketSourceId` zgłoszenia (zapis przy tworzeniu: POST /v2/tickets). **Słownik systemowy z instalacji CRM - nie ma ścieżki zapisu** (jak typy kontrahenta); pozycje są stałe per instancja. Dostępne od wersji API 1.35.0.","operationId":"listTicketSources","responses":{"200":{"description":"Pozycje słownika (id, name)","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/DictionaryEntry"}}},"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/ticket/processes":{"get":{"tags":["Dictionaries"],"summary":"Procesy ticketowe z etapami","description":"Procesy ticketowe z ZAGNIEŻDŻONYMI etapami - struktura jak w CRM: proces →\netapy w kolejności tablicy kanban (`order` od 1). Wszystkie procesy z flagą\n`isActive`; `acl` procesu - ograniczenia widoczności (null = bez ograniczeń).\nSLA (`resolutionTimeMinutes`), automatyczne zamykanie (`autoCloseTimeMinutes`)\ni zakładanie zgłoszeń z maili (`autoTicket`) - te same nazwy co w zapisie,\nwięc konfigurację da się zweryfikować odczytem (dostępne od wersji API 1.28.0).\n\nId etapu (`stages[].id`) mapuje pole `ticketStageId` ticketa.\nNiezależny od procesu GLOBALNY status zgłoszenia (`ticketStatusId`) - w GET /v2/ticket/statuses.","operationId":"listTicketProcesses","responses":{"200":{"description":"Procesy z zagnieżdżonymi etapami","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"properties":{"id":{"type":"integer","example":3},"name":{"type":"string","example":"Reklamacje"},"isActive":{"type":"boolean","example":true},"resolutionTimeMinutes":{"description":"SLA w minutach (0 = brak)","type":"integer","example":1440},"autoCloseTimeMinutes":{"description":"Automatyczne zamknięcie po N minutach (0 = wyłączone)","type":"integer","example":0},"autoTicket":{"description":"Zakładanie zgłoszeń z maili przychodzących na skrzynkę lejka","type":"boolean","example":true},"acl":{"oneOf":[{"$ref":"#/components/schemas/AclJson"}],"nullable":true},"stages":{"type":"array","items":{"properties":{"id":{"type":"integer","example":9},"name":{"type":"string","example":"Nowe"},"color":{"type":"string","example":"#2196f3"},"order":{"description":"Pozycja etapu w procesie (1 = pierwszy)","type":"integer","example":1}},"type":"object"}}},"type":"object"}}},"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"post":{"tags":["Dictionaries"],"summary":"Nowy lejek zgłoszeń (z etapami)","description":"Nowy lejek zgłoszeń wraz z etapami (etapy możesz podać od razu w `stages`,\nżeby nie wysyłać kilkunastu żądań przy stawianiu instancji).\n\n`resolutionTimeMinutes` to czas na rozwiązanie (SLA) w MINUTACH - tak liczy go\nCRM. 24 h = 1440, 48 h = 2880, 72 h = 4320.\n\n`acl` ogranicza widoczność lejka: obiekt `{userIds?, departmentIds?, groupIds?}`\nz listami id - ten sam kształt, który zwraca odczyt (GET /v2/ticket/processes).\nPominięcie albo pusty obiekt = lejek widoczny bez ograniczeń.","operationId":"createTicketProcess","requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["name"],"properties":{"name":{"type":"string","example":"Logistyka"},"resolutionTimeMinutes":{"description":"SLA w minutach (24 h = 1440)","type":"integer","example":1440},"autoCloseTimeMinutes":{"description":"Automatyczne zamknięcie po X minutach bez aktywności (0 = wyłączone)","type":"integer","example":0},"autoTicket":{"description":"Zakładanie zgłoszeń z maili przychodzących na skrzynkę lejka","type":"boolean","example":true},"order":{"type":"integer","example":1},"acl":{"oneOf":[{"$ref":"#/components/schemas/AclJson"}],"nullable":true,"description":"Ograniczenie widoczności: {userIds?, departmentIds?, groupIds?}. Pominięte albo puste = bez ograniczeń. Format obiektowy dostępny od wersji API 1.28.0 (wcześniejszy zapis w tym polu nie działał)."},"active":{"type":"boolean","example":true},"stages":{"description":"Etapy lejka do utworzenia razem z nim","type":"array","items":{"properties":{"name":{"type":"string","example":"Nowe"},"color":{"type":"string","example":"#E0E0E0"},"order":{"type":"integer","example":1}},"type":"object"}}},"type":"object"}}}},"responses":{"201":{"description":"Lejek utworzony (z id etapów)","content":{"application/json":{"schema":{"properties":{"data":{"properties":{"id":{"type":"integer","example":12},"stageIds":{"type":"array","items":{"type":"integer"}}},"type":"object"}},"type":"object"}}}},"403":{"description":"Moduł Zgłoszenia nieaktywny albo brak uprawnień administracyjnych","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/task/statuses":{"get":{"tags":["Dictionaries"],"summary":"Statusy zadań (per wykonawca, z flagą isFinal)","description":"Realne statusy zadań (słownik konfigurowalny per instancja) - przypisywane\nper WYKONAWCA zadania. `isFinal=true` oznacza status kończący zadanie.\n\nUwaga: pole `done` w GET /v2/tasks to uproszczony stan całego zadania\n(1 = niewykonane, 2 = wykonane); ten słownik opisuje szczegółowe statusy\nper wykonawca.","operationId":"listTaskStatuses","responses":{"200":{"description":"Pozycje słownika (id, name, color, isFinal)","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/DictionaryEntry"}}},"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"post":{"tags":["Dictionaries"],"summary":"Nowy status zadania","description":"Nowy status zadania. `isFinal: true` oznacza status kończący - po nim zadanie nie jest już liczone jako otwarte. Statusy systemowe CRM pozwala zmienić wyłącznie na kolorze.","operationId":"createTaskStatus","requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["name"],"properties":{"name":{"type":"string","example":"Czeka na klienta"},"color":{"type":"string","example":"#ffa726"},"isFinal":{"description":"Status kończący zadanie","type":"boolean","example":false},"order":{"type":"integer","example":3}},"type":"object"}}}},"responses":{"201":{"description":"Pozycja utworzona","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DictionaryEntry"}}}},"422":{"description":"Błąd walidacji albo odmowa CRM (pozycja systemowa, duplikat)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień administracyjnych","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/task/tags":{"get":{"tags":["Dictionaries"],"summary":"Tagi zadań","description":"Słownik tagów zadań (np. #umowa, #spotkanie) - do mapowania `tagIds` zadań. Sortowany po nazwie; obejmuje tagi systemowe i prywatne użytkowników.","operationId":"listTaskTags","responses":{"200":{"description":"Pozycje słownika (id, name)","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/DictionaryEntry"}}},"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"post":{"tags":["Dictionaries"],"summary":"Nowy tag zadania","description":"Nowy tag zadania. API zakłada wyłącznie tagi SYSTEMOWE - widoczne dla całej instancji, a nie prywatne tagi jednego użytkownika. CRM sam dokłada `#` na początku nazwy i odrzuca duplikaty.","operationId":"createTaskTag","requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["name"],"properties":{"name":{"description":"Bez „#\" na początku CRM doda go sam","type":"string","example":"#reklamacja"},"color":{"type":"string","example":"#e57373"},"order":{"type":"integer","example":1}},"type":"object"}}}},"responses":{"201":{"description":"Pozycja utworzona","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DictionaryEntry"}}}},"422":{"description":"Błąd walidacji albo odmowa CRM (pozycja systemowa, duplikat)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień administracyjnych","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/user/roles":{"get":{"tags":["Dictionaries"],"summary":"Role użytkowników","description":"Role (grupy uprawnień) zdefiniowane w instancji - wybierasz jedną przy zakładaniu konta (`roleId` w POST /v2/users). Rola decyduje o tym, co użytkownik widzi i może robić w systemie. Od wersji API 1.37.0 każda pozycja niesie też opis i pełną listę uprawnień roli (klucze z GET /v2/user/permissions).","operationId":"listUserRoles","responses":{"200":{"description":"Role z uprawnieniami","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"properties":{"id":{"type":"integer","example":5},"name":{"type":"string","example":"Zewnętrzny handlowiec"},"description":{"type":"string","nullable":true},"permissions":{"type":"array","items":{"type":"string","example":"contractor.canEdit"}}},"type":"object"}}},"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"post":{"tags":["Dictionaries"],"summary":"Nowa rola użytkowników (z uprawnieniami)","description":"Nowa rola (grupa uprawnień) użytkowników - POST /v2/users, pole roleId.\n\n**Uprawnienia od razu w żądaniu** (od wersji API 1.37.0): `permissions` to lista\nkluczy z GET /v2/user/permissions (np. `contractor.canEdit`); rola zapisuje się\ntą samą ścieżką co formularz panelu - z kompletem uprawnień w jednej transakcji.\nBez `permissions` rola powstaje pusta. Uprawnień Super Administratora\n(`_special.superadmin`) API nie nadaje - wyłącznie panel.\n\n**UWAGA - dostęp do modułów to osobna warstwa:** rola daje uprawnienia W modułach,\nale to, które moduły użytkownik w ogóle widzi, ustawia się per moduł w panelu\n(zakładka Role wybranego modułu). Nową rolę trzeba tam jeszcze zaznaczyć,\ninaczej użytkownik z tą rolą nie zobaczy modułu mimo nadanych uprawnień.","operationId":"createUserRole","requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["name"],"properties":{"name":{"description":"Nazwa roli (max 32 znaki)","type":"string","example":"Zewnętrzny handlowiec"},"description":{"type":"string","nullable":true},"permissions":{"description":"Uprawnienia roli - klucze (albo id) z GET /v2/user/permissions. Pominięte = rola bez uprawnień.","type":"array","items":{"type":"string","example":"contractor.canEdit"}}},"type":"object"}}}},"responses":{"201":{"description":"Rola utworzona - w data pełny stan z listą uprawnień","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserRole"}}}},"422":{"description":"Błąd walidacji (m.in. nieznane uprawnienie)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień administracyjnych","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/user/permissions":{"get":{"tags":["Dictionaries"],"summary":"Uprawnienia do ról użytkowników","description":"Słownik uprawnień systemu - wartości pola `permissions` przy tworzeniu i edycji ról (POST/PUT /v2/user/roles). `key` to stały identyfikator uprawnienia (np. `contractor.canEdit`), `name` - nazwa z panelu. Dostępne od wersji API 1.37.0.","operationId":"listUserPermissions","responses":{"200":{"description":"Uprawnienia (id, key, name, description)","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"properties":{"id":{"type":"integer","example":38},"key":{"type":"string","example":"contractor.canEdit"},"name":{"type":"string","example":"Edycja Kontrahentów"},"description":{"type":"string","nullable":true}},"type":"object"}}},"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/user/departments":{"get":{"tags":["Dictionaries"],"summary":"Działy użytkowników","description":"Działy (grupy organizacyjne) instancji - mapują `departmentId` w POST /v2/users.","operationId":"listUserDepartments","responses":{"200":{"description":"Pozycje słownika (id, name)","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/DictionaryEntry"}}},"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"post":{"tags":["Dictionaries"],"summary":"Nowy dział użytkowników","description":"Nowy dział użytkowników (POST /v2/users, pole departmentId). Bez flagi active - słownik działów w CRM jej nie ma (dział istnieje albo nie).","operationId":"createUserDepartment","requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["name"],"properties":{"name":{"type":"string","example":"BOK"},"order":{"type":"integer","example":2}},"type":"object"}}}},"responses":{"201":{"description":"Dział utworzony","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DictionaryEntry"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/service/statuses":{"get":{"tags":["Dictionaries"],"summary":"Statusy usług","description":"Statusy usług (kontraktów serwisowych) - mapują `serviceStatusId` z GET /v2/services. **Konfiguracji tego słownika nie da się zmienić przez API** - CRM nie ma dla niego żadnej ścieżki zapisu (pozycje pochodzą z instalacji systemu), więc zmiany wyklikuje się w panelu.","operationId":"listServiceStatuses","responses":{"200":{"description":"Pozycje słownika (id, name)","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/DictionaryEntry"}}},"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/service/billing-periods":{"get":{"tags":["Dictionaries"],"summary":"Okresy rozliczeniowe usług","description":"Okresy rozliczeniowe usług - mapują `billingPeriodId` z GET /v2/services. **Konfiguracji tego słownika nie da się zmienić przez API** - CRM nie ma dla niego żadnej ścieżki zapisu (pozycje pochodzą z instalacji systemu), więc zmiany wyklikuje się w panelu.","operationId":"listServiceBillingPeriods","responses":{"200":{"description":"Pozycje słownika (id, name, active)","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/DictionaryEntry"}}},"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/service/payment-terms":{"get":{"tags":["Dictionaries"],"summary":"Terminy płatności usług","description":"Terminy płatności usług - `days` to liczba dni terminu (name = ta sama wartość stringiem), `isDefault` wyróżnia termin domyślny instancji. Mapują `paymentTermId` z GET /v2/services.","operationId":"listServicePaymentTerms","responses":{"200":{"description":"Pozycje słownika (id, name, days, isDefault, active)","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/DictionaryEntry"}}},"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"post":{"tags":["Dictionaries"],"summary":"Nowy termin płatności","description":"Nowy termin płatności usługi. Termin to LICZBA DNI (`days`), a nie nazwa - w odczycie `name` zwraca tę samą liczbę. CRM wymaga zakresu 0-365 i pilnuje, żeby termin o tej liczbie dni nie istniał już w słowniku. `isDefault: true` zdejmuje flagę z dotychczasowego domyślnego.","operationId":"createServicePaymentTerm","requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["days"],"properties":{"days":{"description":"Liczba dni terminu (0-365)","type":"integer","example":14},"isDefault":{"description":"Termin domyślny instancji","type":"boolean","example":false},"order":{"type":"integer","example":1},"active":{"type":"boolean","example":true}},"type":"object"}}}},"responses":{"201":{"description":"Pozycja utworzona","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DictionaryEntry"}}}},"422":{"description":"Błąd walidacji albo odmowa CRM (pozycja systemowa, duplikat)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień administracyjnych","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/service/invoice-types":{"get":{"tags":["Dictionaries"],"summary":"Typy faktur usług","description":"Typy faktur usług - mapują `invoiceTypeId` z GET /v2/services.","operationId":"listServiceInvoiceTypes","responses":{"200":{"description":"Pozycje słownika (id, name, active)","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/DictionaryEntry"}}},"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"post":{"tags":["Dictionaries"],"summary":"Nowy typ faktury","description":"Nowy typ faktury usługi (np. faktura VAT, proforma).","operationId":"createServiceInvoiceType","requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["name"],"properties":{"name":{"type":"string","example":"Faktura proforma"},"order":{"type":"integer","example":1},"active":{"type":"boolean","example":true}},"type":"object"}}}},"responses":{"201":{"description":"Pozycja utworzona","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DictionaryEntry"}}}},"422":{"description":"Błąd walidacji albo odmowa CRM (pozycja systemowa, duplikat)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień administracyjnych","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/pipeline/funnels":{"get":{"tags":["Dictionaries"],"summary":"Lejki sprzedaży z etapami","description":"Lejki sprzedaży (grupy etapów) z ZAGNIEŻDŻONYMI etapami - struktura jak\nw CRM: lejek → etapy w kolejności tablicy kanban (`order` od 1). Zwracane\nsą wszystkie lejki z flagą `isActive` (nieaktywne bywają potrzebne do\nmapowania istniejących szans). `probability` etapu = szacowane prawdopodobieństwo\nwygranej w procentach; `acl` lejka - ograniczenia widoczności (null = bez ograniczeń).\n\nId etapu (`stages[].id`) mapuje pole `pipelineStageId` szansy w GET/POST/PUT /v2/pipeline/items.","operationId":"listPipelineFunnels","responses":{"200":{"description":"Lejki z zagnieżdżonymi etapami","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"properties":{"id":{"type":"integer","example":1},"name":{"type":"string","example":"Lejek działu sprzedaży"},"isActive":{"type":"boolean","example":true},"acl":{"oneOf":[{"$ref":"#/components/schemas/AclJson"}],"nullable":true},"stages":{"type":"array","items":{"properties":{"id":{"type":"integer","example":5},"name":{"type":"string","example":"Kontakt"},"color":{"type":"string","example":"#2196f3"},"probability":{"type":"integer","example":20,"nullable":true},"order":{"description":"Pozycja etapu w lejku (1 = pierwszy)","type":"integer","example":1}},"type":"object"}}},"type":"object"}}},"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"post":{"tags":["Dictionaries"],"summary":"Nowy lejek sprzedaży (z etapami)","description":"Nowy lejek sprzedaży wraz z etapami. `probability` etapu (0-100) to prawdopodobieństwo wygranej - z niego CRM liczy ważoną wartość lejka.","operationId":"createPipelineFunnel","requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["name"],"properties":{"name":{"type":"string","example":"Program partnerski"},"order":{"type":"integer","example":2},"requireChangeReason":{"description":"Wymagaj powodu przy zmianie etapu","type":"boolean","example":false},"automaticAmountUpdate":{"description":"Automatyczna aktualizacja wartości szansy","type":"boolean","example":false},"acl":{"oneOf":[{"$ref":"#/components/schemas/AclJson"}],"nullable":true,"description":"Ograniczenie widoczności: {userIds?, departmentIds?, groupIds?}. Pominięte albo puste = bez ograniczeń."},"active":{"type":"boolean","example":true},"stages":{"type":"array","items":{"properties":{"name":{"type":"string","example":"Oferta"},"color":{"type":"string","example":"#d54d91"},"probability":{"type":"integer","example":60},"order":{"type":"integer","example":3}},"type":"object"}}},"type":"object"}}}},"responses":{"201":{"description":"Lejek utworzony (z id etapów)"},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/lead/statuses":{"get":{"tags":["Dictionaries"],"summary":"Grupy statusów leadów ze statusami","description":"Grupy statusów leadów (procesy leadowe) z ZAGNIEŻDŻONYMI statusami -\nstruktura jak w CRM: grupa → statusy w kolejności tablicy kanban (`order` od 1).\nWszystkie grupy z flagą `isActive`; `acl` grupy - ograniczenia widoczności\n(null = bez ograniczeń).\n\n`type` statusu: `default` (w toku), `qualified` (zakwalifikowany - lead\nkończy proces sukcesem), `disqualified` (zdyskwalifikowany - koniec porażką).\nId statusu (`statuses[].id`) mapuje pole `leadStatusId` leada w GET/POST/PUT /v2/leads.","operationId":"listLeadStatuses","responses":{"200":{"description":"Grupy z zagnieżdżonymi statusami","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"properties":{"id":{"type":"integer","example":2},"name":{"type":"string","example":"Proces leadowy handlowy"},"isActive":{"type":"boolean","example":true},"acl":{"oneOf":[{"$ref":"#/components/schemas/AclJson"}],"nullable":true},"statuses":{"type":"array","items":{"properties":{"id":{"type":"integer","example":11},"name":{"type":"string","example":"Nowy"},"color":{"type":"string","example":"#4caf50","nullable":true},"type":{"type":"string","enum":["default","qualified","disqualified"],"example":"default"},"order":{"description":"Pozycja statusu w grupie (1 = pierwszy)","type":"integer","example":1}},"type":"object"}}},"type":"object"}}},"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/currencies":{"get":{"tags":["Dictionaries"],"summary":"Dostępne waluty","description":"Waluty dostępne w instancji (konfiguracja systemu) - kody ISO 4217. To lista stringów, nie obiektów id/name; w tych walutach wyrażane są kwoty w systemie (np. `revenueCurrency` kontrahenta).","operationId":"listCurrencies","responses":{"200":{"description":"Kody walut","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"type":"string"},"example":["PLN","EUR","USD"]}},"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/contractor/priorities/{id}":{"put":{"tags":["Dictionaries"],"summary":"Aktualizacja priorytetu kontrahenta","description":"Częściowa aktualizacja priorytetu kontrahenta - wysyłasz tylko pola do zmiany.","operationId":"updateContractorPriority","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":3}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Priorytet po aktualizacji","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DictionaryEntry"}}}},"404":{"description":"Brak pozycji o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/address/types/{id}":{"put":{"tags":["Dictionaries"],"summary":"Aktualizacja typu adresu","description":"Częściowa aktualizacja typu adresu.","operationId":"updateAddressType","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":4}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Typ adresu po aktualizacji","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DictionaryEntry"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/ticket/statuses/{id}":{"put":{"tags":["Dictionaries"],"summary":"Aktualizacja statusu zgłoszenia","description":"Częściowa aktualizacja statusu zgłoszenia.","operationId":"updateTicketStatus","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":2}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Status po aktualizacji","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DictionaryEntry"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/service/catalog":{"get":{"tags":["ServiceCatalog"],"summary":"Lista pozycji katalogu usług","description":"Słownik pozycji katalogu (cennika) usług. Usługi kontrahentów (GET /v2/services)\nwskazują pozycję katalogu przez `catalogId` - ta lista pozwala zmapować id na\nnazwy, grupy i domyślne stawki.\n\nNieaktywne pozycje (`active=false`) SĄ na liście, bo historyczne usługi mogą\nwskazywać wyłączoną pozycję - bieżącą ofertę wybierzesz filtrem `?active=1`.\n\nSłownik konfiguracyjny bez dat i pól niestandardowych - brak `createdAfter`\ni `customField`; katalog jest mały, pobieraj całość.","operationId":"listServiceCatalog","parameters":[{"name":"active","in":"query","description":"Tylko pozycje aktywne (1) / nieaktywne (0). Bez filtra - wszystkie.","required":false,"schema":{"type":"boolean","example":true}},{"name":"name","in":"query","description":"Filtr częściowy (zawiera). Analogicznie: groupName.","required":false,"schema":{"type":"string"}},{"name":"groupId","in":"query","description":"Grupa katalogu - dokładne dopasowanie. Analogicznie dokładne: id, currency.","required":false,"schema":{"type":"integer"}},{"name":"id","in":"query","required":false,"description":"Filtr po polu id - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"currency","in":"query","required":false,"description":"Filtr po polu currency - dopasowanie dokładne.","schema":{"type":"string"}},{"name":"groupName","in":"query","required":false,"description":"Filtr po polu groupName - dopasowanie częściowe (zawiera).","schema":{"type":"string"}},{"name":"sort","in":"query","description":"Pole sortowania: id, name, priority.","required":false,"schema":{"type":"string","default":"name"}},{"name":"sortDir","in":"query","required":false,"schema":{"type":"string","default":"asc","enum":["asc","desc"]}},{"name":"page","in":"query","description":"Strona wyników, od 1.","required":false,"schema":{"type":"integer","default":1,"maximum":1000000,"minimum":1}},{"name":"limit","in":"query","description":"Rozmiar strony (max 1000).","required":false,"schema":{"type":"integer","default":100,"maximum":1000,"minimum":1}}],"responses":{"200":{"description":"Lista pozycji katalogu + metadane stronicowania","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ServiceCatalogItem"}},"pagination":{"$ref":"#/components/schemas/Pagination"}},"type":"object"}}}},"400":{"description":"Błąd walidacji zapytania - lista błędów w kontrakcie {field, code, message}","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"post":{"tags":["ServiceCatalog"],"summary":"Nowa pozycja katalogu usług","description":"Nowa pozycja katalogu (cennika) usług - z takiej pozycji powstają potem usługi kontrahentów (POST /v2/services, pole catalogId). Nazwa musi mieć min. 3 znaki (wymóg CRM). Grupa (`groupId`) jest WYMAGANA - CRM nie przyjmuje pozycji bez grupy; grupę zakładasz przez POST /v2/service/catalog/groups.","operationId":"createServiceCatalogItem","requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["name","groupId"],"properties":{"name":{"type":"string","example":"Punkt odbioru"},"groupId":{"description":"Grupa katalogu usług (GET /v2/service/catalog/groups)","type":"integer"},"icon":{"description":"Ikona z zestawu CRM, np. `fal fa-cog`","type":"string","example":"fal fa-cog"},"defaultPayValue":{"description":"Domyślna cena pozycji - ta sama nazwa co przy odczycie (do wersji API 2.0.3: `price`)","type":"string","example":"199.00","nullable":true},"currency":{"description":"Waluta domyślnej ceny (ISO 4217); brak = główna waluta instancji","type":"string","example":"PLN","nullable":true},"isAgreement":{"description":"Pozycja-umowa: tylko usługa z takiej pozycji przyjmuje daty umowy (agreementDate itd. w POST /v2/services). Dostępne od wersji API 1.27.0.","type":"boolean","example":false},"active":{"type":"boolean","example":true}},"type":"object"}}}},"responses":{"201":{"description":"Pozycja katalogu utworzona","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DictionaryEntry"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/service/catalog/{id}":{"put":{"tags":["ServiceCatalog"],"summary":"Aktualizacja pozycji katalogu usług","description":"Częściowa aktualizacja pozycji katalogu usług (name, groupId, icon, defaultPayValue, currency, isAgreement, active). Zdjęcia flagi `isAgreement` CRM odmówi, gdy na pozycji istnieją już usługi z umową.","operationId":"updateServiceCatalogItem","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":12}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Pozycja po aktualizacji","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DictionaryEntry"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/service/catalog/groups":{"get":{"tags":["ServiceCatalog"],"summary":"Lista grup katalogu usług","description":"Grupy katalogu usług. Grupa jest wymagana przy zakładaniu pozycji katalogu\n(POST /v2/service/catalog, pole `groupId`) - ta lista pozwala sprawdzić,\nczy grupa już istnieje, zamiast zakładać ją w ciemno przy każdym uruchomieniu\nskryptu konfiguracyjnego.\n\nSłownik konfiguracyjny - zwracamy całość, bez stronicowania.\nDostępne od wersji API 1.27.0.","operationId":"listServiceCatalogGroups","responses":{"200":{"description":"Lista grup katalogu","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"properties":{"id":{"type":"integer","example":59},"name":{"type":"string","example":"Usługi odbioru"},"parentId":{"description":"Grupa nadrzędna (drzewo)","type":"integer","example":null,"nullable":true},"color":{"type":"string","example":"#4b78c5","nullable":true},"icon":{"type":"string","example":"fal fa-cog","nullable":true},"order":{"type":"integer","example":10},"active":{"type":"boolean","example":true}},"type":"object"}}},"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"post":{"tags":["ServiceCatalog"],"summary":"Nowa grupa katalogu usług","description":"Nowa grupa katalogu usług. Grupa jest WYMAGANA przy zakładaniu pozycji katalogu (CRM nie przyjmuje pozycji bez grupy), więc przy stawianiu instancji zaczynasz od niej.","operationId":"createServiceCatalogGroup","requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["name"],"properties":{"name":{"type":"string","example":"Usługi odbioru"},"parentId":{"description":"Grupa nadrzędna (drzewo)","type":"integer","nullable":true},"color":{"type":"string","example":"#4b78c5"},"icon":{"description":"Ikona z zestawu CRM, np. `fal fa-cog`","type":"string","example":"fal fa-cog"},"order":{"type":"integer","example":1},"active":{"type":"boolean","example":true}},"type":"object"}}}},"responses":{"201":{"description":"Grupa utworzona","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DictionaryEntry"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/service/catalog/groups/{id}":{"put":{"tags":["ServiceCatalog"],"summary":"Aktualizacja grupy katalogu usług","description":"Częściowa aktualizacja grupy katalogu usług.","operationId":"updateServiceCatalogGroup","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":59}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Grupa po aktualizacji","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DictionaryEntry"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/user/departments/{id}":{"put":{"tags":["Dictionaries"],"summary":"Aktualizacja działu użytkowników","description":"Częściowa aktualizacja działu użytkowników.","operationId":"updateUserDepartment","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":2}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Dział po aktualizacji","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DictionaryEntry"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/user/roles/{id}":{"put":{"tags":["Dictionaries"],"summary":"Aktualizacja roli użytkowników","description":"Częściowa aktualizacja roli: name, description, permissions. `permissions` ZASTĘPUJE cały zestaw uprawnień roli (klucze albo id z GET /v2/user/permissions; pusta lista odbiera wszystkie); pominięte - uprawnienia zostają bez zmian. Zapis uprawnień dostępny od wersji API 1.37.0.","operationId":"updateUserRole","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":5}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"properties":{"name":{"type":"string","example":"Zewnętrzny handlowiec"},"description":{"type":"string","nullable":true},"permissions":{"type":"array","items":{"type":"string","example":"contractor.canEdit"}}},"type":"object"}}}},"responses":{"200":{"description":"Rola po aktualizacji - z listą uprawnień","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserRole"}}}},"404":{"description":"Brak roli o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/contractor/statuses/{id}":{"put":{"tags":["Dictionaries"],"summary":"Aktualizacja: status kontrahenta","description":"Częściowa aktualizacja: wysyłasz tylko pola do zmiany (name, color, order, active).","operationId":"updateContractorStatus","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":3}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Pozycja po aktualizacji","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DictionaryEntry"}}}},"404":{"description":"Brak pozycji o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/contractor/sources/{id}":{"put":{"tags":["Dictionaries"],"summary":"Aktualizacja: źródło pozyskania","description":"Częściowa aktualizacja: wysyłasz tylko pola do zmiany (name).","operationId":"updateContractorSource","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":3}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Pozycja po aktualizacji","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DictionaryEntry"}}}},"404":{"description":"Brak pozycji o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/contractor/industries/{id}":{"put":{"tags":["Dictionaries"],"summary":"Aktualizacja: branża","description":"Częściowa aktualizacja: wysyłasz tylko pola do zmiany (name).","operationId":"updateIndustry","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":3}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Pozycja po aktualizacji","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DictionaryEntry"}}}},"404":{"description":"Brak pozycji o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/contractor/payment-types/{id}":{"put":{"tags":["Dictionaries"],"summary":"Aktualizacja: typ płatności","description":"Częściowa aktualizacja: wysyłasz tylko pola do zmiany (name, description, color, order, active).","operationId":"updateContractorPaymentType","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":3}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Pozycja po aktualizacji","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DictionaryEntry"}}}},"404":{"description":"Brak pozycji o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/contractor/legal-forms/{id}":{"put":{"tags":["Dictionaries"],"summary":"Aktualizacja: forma prawna","description":"Częściowa aktualizacja: wysyłasz tylko pola do zmiany (name, order, active).","operationId":"updateLegalForm","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":3}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Pozycja po aktualizacji","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DictionaryEntry"}}}},"404":{"description":"Brak pozycji o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/note/types/{id}":{"put":{"tags":["Dictionaries"],"summary":"Aktualizacja: typ notatki","description":"Częściowa aktualizacja: wysyłasz tylko pola do zmiany (name, color, icon, order, acl, active). `acl` w kształcie {userIds?, departmentIds?, groupIds?}; pusty obiekt zdejmuje ograniczenia (od wersji API 1.29.0).","operationId":"updateNoteType","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":3}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Pozycja po aktualizacji","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DictionaryEntry"}}}},"404":{"description":"Brak pozycji o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/task/statuses/{id}":{"put":{"tags":["Dictionaries"],"summary":"Aktualizacja: status zadania","description":"Częściowa aktualizacja: wysyłasz tylko pola do zmiany (name, color, isFinal, order).","operationId":"updateTaskStatus","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":3}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Pozycja po aktualizacji","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DictionaryEntry"}}}},"404":{"description":"Brak pozycji o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/task/tags/{id}":{"put":{"tags":["Dictionaries"],"summary":"Aktualizacja: tag zadania","description":"Częściowa aktualizacja: wysyłasz tylko pola do zmiany (name, color, order).","operationId":"updateTaskTag","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":3}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Pozycja po aktualizacji","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DictionaryEntry"}}}},"404":{"description":"Brak pozycji o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/project/statuses/{id}":{"put":{"tags":["Dictionaries"],"summary":"Aktualizacja: status projektu","description":"Częściowa aktualizacja: wysyłasz tylko pola do zmiany (name, color, isDefault, passTasks, order).","operationId":"updateProjectStatus","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":3}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Pozycja po aktualizacji","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DictionaryEntry"}}}},"404":{"description":"Brak pozycji o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/order/statuses/{id}":{"put":{"tags":["Dictionaries"],"summary":"Aktualizacja: status zamówienia","description":"Częściowa aktualizacja: wysyłasz tylko pola do zmiany (name).","operationId":"updateOrderStatus","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":3}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Pozycja po aktualizacji","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DictionaryEntry"}}}},"404":{"description":"Brak pozycji o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/service/payment-terms/{id}":{"put":{"tags":["Dictionaries"],"summary":"Aktualizacja: termin płatności","description":"Częściowa aktualizacja: wysyłasz tylko pola do zmiany (days, isDefault, order, active).","operationId":"updateServicePaymentTerm","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":3}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Pozycja po aktualizacji","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DictionaryEntry"}}}},"404":{"description":"Brak pozycji o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/service/invoice-types/{id}":{"put":{"tags":["Dictionaries"],"summary":"Aktualizacja: typ faktury","description":"Częściowa aktualizacja: wysyłasz tylko pola do zmiany (name, order, active).","operationId":"updateServiceInvoiceType","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":3}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Pozycja po aktualizacji","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DictionaryEntry"}}}},"404":{"description":"Brak pozycji o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/contractors/{contractorId}/dms":{"get":{"tags":["DMS"],"summary":"Listing katalogów i plików DMS kontrahenta","description":"Zawartość jednego poziomu drzewa DMS kontrahenta: podkatalogi + pliki.\nBez `directoryId` = poziom główny; nawigacja w głąb przez id katalogów\nz `directories`. Każdy plik ma `downloadUrl` (podpisany link ważny\n1 minutę - pobieraj od razu, po wygaśnięciu odczytaj listing ponownie).","operationId":"listContractorDms","parameters":[{"name":"contractorId","in":"path","required":true,"schema":{"type":"integer","example":121}},{"name":"directoryId","in":"query","description":"Katalog do wylistowania; brak = poziom główny.","required":false,"schema":{"type":"integer","example":61}}],"responses":{"200":{"description":"Zawartość poziomu drzewa DMS","content":{"application/json":{"schema":{"properties":{"data":{"properties":{"directory":{"oneOf":[{"$ref":"#/components/schemas/DmsDirectory"}],"nullable":true,"description":"Katalog, którego zawartość listujemy; null na poziomie głównym"},"directories":{"type":"array","items":{"$ref":"#/components/schemas/DmsDirectory"}},"documents":{"type":"array","items":{"$ref":"#/components/schemas/DmsDocument"}}},"type":"object"}},"type":"object"}}}},"404":{"description":"Brak kontrahenta albo katalogu o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/contractors/{contractorId}/dms/directories":{"post":{"tags":["DMS"],"summary":"Nowy katalog DMS kontrahenta","description":"Tworzy katalog w DMS kontrahenta (`parentId` = katalog nadrzędny,\nbrak = poziom główny). Nazwy czyści CRM; duplikat nazwy w tym samym\nmiejscu drzewa dostaje sufiks. Zmiana nazwy i usuwanie katalogów\nNIE są dostępne przez API.","operationId":"createDmsDirectory","parameters":[{"name":"contractorId","in":"path","required":true,"schema":{"type":"integer","example":121}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["name"],"properties":{"name":{"type":"string","example":"Umowy 2026"},"parentId":{"description":"Katalog nadrzędny; brak = poziom główny","type":"integer"}},"type":"object"}}}},"responses":{"201":{"description":"Katalog utworzony","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/DmsDirectory"}},"type":"object"}}}},"404":{"description":"Brak kontrahenta o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Błędy walidacji (nazwa, katalog nadrzędny)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień do DMS","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/contractors/{contractorId}/dms/documents":{"post":{"tags":["DMS"],"summary":"Upload pliku do DMS kontrahenta (multipart)","description":"Upload pliku do DMS kontrahenta - request **multipart/form-data**\nz plikiem w polu `file` (limit produktowy 128 MB) i opcjonalnym polem\nformularza `directoryId` (katalog docelowy; brak = poziom główny).\nDuplikat nazwy w katalogu dostaje sufiks od CRM. Odpowiedź: rekord\npliku z gotowym `downloadUrl`. Usuwanie plików NIE jest dostępne\nprzez API.","operationId":"uploadDmsDocument","parameters":[{"name":"contractorId","in":"path","required":true,"schema":{"type":"integer","example":121}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"required":["file"],"properties":{"file":{"description":"Plik do zapisania","type":"string","format":"binary"},"directoryId":{"description":"Katalog docelowy; brak = poziom główny","type":"integer"}},"type":"object"}}}},"responses":{"201":{"description":"Plik zapisany","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/DmsDocument"}},"type":"object"}}}},"404":{"description":"Brak kontrahenta o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Brak pliku / zły katalog / plik odrzucony przez CRM","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień do DMS","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/dms/documents/{publicId}":{"get":{"tags":["DMS"],"summary":"Plik DMS po publicId (z linkiem do pobrania)","description":"Metadane pliku DMS + świeży `downloadUrl` (podpisany link ważny 1 minutę) - do pobrania pliku po wygaśnięciu wcześniejszego linku. Adresowanie po `publicId` z listingu katalogu: identyfikator jest nieciągły, więc plików nie da się przelecieć pętlą - najpierw listing katalogów kontrahenta, potem pobranie konkretnego pliku.","operationId":"getDmsDocument","parameters":[{"name":"publicId","in":"path","description":"publicId pliku z listingu DMS (nie id rekordu CRM)","required":true,"schema":{"type":"string","example":"3417800497430272"}}],"responses":{"200":{"description":"Plik DMS","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/DmsDocument"}},"type":"object"}}}},"404":{"description":"Brak pliku o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"put":{"tags":["DMS"],"summary":"Zmiana nazwy pliku DMS","description":"Zmiana nazwy pliku DMS (jedyne edytowalne pole). Rozszerzenie pilnuje CRM (zostaje ze starej nazwy), duplikat nazwy w katalogu dostaje sufiks. Adresowanie po `publicId` z listingu.","operationId":"renameDmsDocument","parameters":[{"name":"publicId","in":"path","description":"publicId pliku z listingu DMS (nie id rekordu CRM)","required":true,"schema":{"type":"string","example":"3417800497430272"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["fileName"],"properties":{"fileName":{"type":"string","example":"umowa-2026-aneks"}},"type":"object"}}}},"responses":{"200":{"description":"Plik po zmianie nazwy","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/DmsDocument"}},"type":"object"}}}},"404":{"description":"Brak pliku o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Błędy walidacji (nazwa)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień do DMS","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/document/types":{"get":{"tags":["Documents"],"summary":"Typy dokumentów generatora (z szablonami)","description":"Aktywne typy dokumentów generatora z kategorią i - dla typów z szablonami\nHTML - listą szablonów (`templates`; przy generowaniu taki typ wymaga\n`templateId`). `requiresTemplate` mówi, który wariant obowiązuje.\n\n`includeInactive=true` pokazuje też typy nieaktywne i szkice (drafty\nzakładane przez POST /v2/document/types) oraz typy spoza katalogowanych\nkategorii - do podglądu pełnego cyklu tworzenia szablonu. Stan niesie\npara pól `active`/`draft`.","operationId":"listDocumentTypes","parameters":[{"name":"includeInactive","in":"query","description":"true = pokaż też typy nieaktywne i szkice (drafty). Dostępne od wersji API 2.4.0.","required":false,"schema":{"type":"boolean","default":false}}],"responses":{"200":{"description":"Typy dokumentów","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"properties":{"id":{"type":"integer","example":411},"name":{"type":"string","example":"Oferta handlowa"},"description":{"type":"string","nullable":true},"categoryId":{"type":"integer","nullable":true},"categoryName":{"type":"string","nullable":true},"requiresTemplate":{"type":"boolean","example":false},"active":{"description":"Dostępne od wersji API 2.4.0.","type":"boolean","example":true},"draft":{"description":"Szkic - formularz nie pokrywa jeszcze zmiennych pliku źródłowego. Dostępne od wersji API 2.4.0.","type":"boolean","example":false},"templates":{"description":"Szablony HTML (tylko typy z requiresTemplate=true)","type":"array","items":{"properties":{"id":{"type":"integer"},"name":{"type":"string"},"description":{"type":"string","nullable":true}},"type":"object"}}},"type":"object"}}},"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"post":{"tags":["Documents"],"summary":"Nowy typ dokumentu generatora (szkic)","description":"Zakłada typ dokumentu generatora jako **szkic** (`draft: true`,\n`active: false`) - krok 1 z 4 cyklu tworzenia szablonu:\n\n1. **POST /v2/document/types** - dane podstawowe (ten endpoint),\n2. POST /v2/document/types/{id}/source - plik źródłowy (CRM rejestruje go\n   w usłudze Google Docs i wykrywa zmienne `{{...}}`),\n3. PUT /v2/document/types/{id}/form - formularz mapujący zmienne na pola,\n4. POST /v2/document/types/{id}/activate - typ zaczyna generować dokumenty.\n\nSzkic widać w GET /v2/document/types dopiero z `includeInactive=true`.\n`shareEmails` = adresy, którym plik źródłowy zostanie udostępniony\nw Google Docs (CRM dokłada do nich adresy z ustawień instancji).\nPrzy `store=false` CRM zeruje `publishDays` (dokument bez zapisu\nnie może być publikowany online).\n\nWymaga uprawnienia administracji szablonami dokumentów\n(`documents.templateAdmin` albo `system.admin`).","operationId":"createDocumentType","requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["name"],"properties":{"name":{"type":"string","example":"Umowa serwisowa"},"description":{"type":"string","nullable":true},"categoryId":{"description":"Kategoria dokumentów (niesystemowa)","type":"integer","nullable":true},"numerationId":{"description":"Aktywny schemat numeracji dokumentów","type":"integer","nullable":true},"mailTemplateId":{"description":"Szablon maila do wysyłki dokumentu","type":"integer","nullable":true},"store":{"description":"Zapisywać wygenerowane dokumenty w CRM. Typ ze store=false NIE nadaje się do generowania przez API - POST /v2/contractors/{id}/documents odpowiada 422 document.typeNotStored, bo CRM wysyła wtedy plik prosto do przeglądarki zamiast odpowiedzi.","type":"boolean","default":true},"publishDays":{"description":"Dni publikacji online; 0 = bez publikacji. Ignorowane (zerowane) przy store=false.","type":"integer","default":0,"example":14},"shareEmails":{"description":"Adresy, którym udostępnić plik źródłowy w Google Docs","type":"array","items":{"type":"string"},"example":["anna.nowak@example.com"]}},"type":"object"}}}},"responses":{"201":{"description":"Szkic typu założony (draft: true, active: false)","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/DocumentType"}},"type":"object"}}}},"422":{"description":"Błędy walidacji (brak nazwy, nieistniejąca kategoria/numeracja/szablon maila)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnienia administracji szablonami dokumentów","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/document/types/{id}/form":{"get":{"tags":["Documents"],"summary":"Formularz danych typu dokumentu (prefill z kontrahenta)","description":"Definicja pól formularza typu dokumentu, z wartościami wstępnie\nuzupełnionymi danymi kontrahenta (`contractorId` wymagane). Dla typów\nz szablonami HTML podaj też `templateId`. Wypełnione dane wysyłasz\npotem jako `data` w POST /v2/contractors/{id}/documents.","operationId":"getDocumentTypeForm","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":411}},{"name":"contractorId","in":"query","required":true,"schema":{"type":"integer","example":121}},{"name":"templateId","in":"query","description":"Szablon HTML - wymagany dla typów z requiresTemplate=true.","required":false,"schema":{"type":"integer"}}],"responses":{"200":{"description":"Definicja pól formularza (struktura core CRM: fieldsGrouped z prefillami)","content":{"application/json":{"schema":{"properties":{"data":{"type":"object"}},"type":"object"}}}},"400":{"description":"Brak contractorId / zły typ","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"put":{"tags":["Documents"],"summary":"Formularz typu dokumentu (mapowanie zmiennych na pola)","description":"Zapisuje definicję formularza typu - mapowanie zmiennych z pliku źródłowego\nna pola wypełniane przy generowaniu. `formFields` to lista GRUP:\n\n```\n[{\"name\": \"Dane umowy\", \"items\": [\n  {\"name\": \"Numer umowy\", \"type\": \"STR\", \"variable\": \"numer_umowy\",\n   \"rules\": {\"isRequired\": 1}},\n  {\"name\": \"Klient\", \"type\": \"Contractor\", \"variable\": \"nazwa_klienta\",\n   \"options\": {\"field\": \"cc_name\"}}\n]}]\n```\n\nTypy pól jak w panelu CRM: `STR`, `SELECT`/`MULTISELECT` (z\n`options.selectOptions`), `DOCUMENT_NUMBER` (numer z numeracji typu),\npola modeli CRM (`Contractor`, `ContractorAddress`, `Contact`,\n`SalesPipeline` - z `options.field`) oraz kontener `sublist`\n(`type: \"sublist\"` + własne `items`). Struktura 1:1 z formularzem\npanelu CRM - jego pola widać w GET /v2/document/types/{id}/form.\n\nGdy formularz pokryje WSZYSTKIE zmienne z pliku, `draft` schodzi\nautomatycznie (odpowiedź niesie stan po przeliczeniu oraz\n`missingVariables` - zmienne wciąż bez pola).","operationId":"updateDocumentTypeForm","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":512}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["formFields"],"properties":{"formFields":{"description":"Grupy pól formularza (patrz opis endpointu)","type":"array","items":{"type":"object"}},"formHtml":{"description":"Układ HTML formularza dla panelu CRM (opcjonalny - generuje go panel; API może pominąć)","type":"string","nullable":true}},"type":"object"}}}},"responses":{"200":{"description":"Formularz zapisany, draft przeliczony","content":{"application/json":{"schema":{"properties":{"data":{"properties":{"id":{"type":"integer","example":512},"draft":{"description":"false = wszystkie zmienne pokryte, można aktywować","type":"boolean","example":false},"templateVersion":{"type":"integer","example":3},"variables":{"type":"array","items":{"type":"string"},"example":["numer_umowy","nazwa_klienta"]},"missingVariables":{"description":"Zmienne z pliku wciąż bez pola w formularzu","type":"array","items":{"type":"string"},"example":[]}},"type":"object"}},"type":"object"}}}},"404":{"description":"Brak typu dokumentu o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Błędy struktury formFields","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnienia administracji szablonami dokumentów","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/contractors/{contractorId}/documents":{"get":{"tags":["Documents"],"summary":"Dokumenty generatora kontrahenta","description":"Dokumenty generatora dla kontrahenta, najnowsze pierwsze. `salesPipelineId` zawęża do dokumentów przypiętych do jednej szansy. Bez paginacji.","operationId":"listContractorDocuments","parameters":[{"name":"contractorId","in":"path","required":true,"schema":{"type":"integer","example":121}},{"name":"salesPipelineId","in":"query","description":"Tylko dokumenty przypięte do tej szansy.","required":false,"schema":{"type":"integer"}}],"responses":{"200":{"description":"Dokumenty kontrahenta","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/GeneratedDocument"}}},"type":"object"}}}},"404":{"description":"Brak kontrahenta o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"post":{"tags":["Documents"],"summary":"Generowanie dokumentu z szablonu (z publikacją online)","description":"Generuje dokument z szablonu na kontrahencie - **synchronicznie**\n(render robi serwis Tillio Docs; odpowiedź wraca po zapisaniu pliku,\ntypowo kilka sekund). Dokument przechodzi pełną ścieżkę CRM: numeracja\nwg schematu instancji, plik PDF w storage, publikacja online.\n\n- `documentTypeId` (wymagane) + `templateId` (dla typów z requiresTemplate=true),\n- `data` - dane formularza (pola z GET /v2/document/types/{id}/form),\n- **`salesPipelineId`** - przypina dokument do szansy sprzedaży\n  (musi należeć do kontrahenta),\n- **`fillFromPipeline: true`** - uzupełnia `data.order.products`\n  i walutę produktami szansy (gdy `data` ich nie zawiera),\n- **`updatePipeline: true`** - po wygenerowaniu nadpisuje produkty\n  i wartość SZANSY danymi dokumentu (kierunek dokument → szansa).\n  **Ograniczenie:** dla typów z szablonem HTML (`requiresTemplate: true`)\n  niedostępne - 422 `document.updatePipelineUnsupported`, bo CRM kasuje\n  tam pozycje szansy bez odtworzenia (do naprawy po stronie CRM;\n  `fillFromPipeline` działa dla obu wariantów).\n\nTyp musi zapisywać dokumenty w CRM (`store: true` w POST /v2/document/types) -\ntyp ze `store=false` kończy się 422 `document.typeNotStored`, bo CRM wysyłałby\nplik prosto do przeglądarki zamiast odpowiedzi API. Operacja nie przyjmuje\nparametrów zapytania w adresie (400 `query.unknownField`).\n\nOdpowiedź niesie **`publishUrl`** - publiczny link online dokumentu\n(docs.tillio.app) do wysłania klientowi (typy z włączoną publikacją;\nważny do `publishValidTo`) oraz `downloadUrl` do pliku PDF.","operationId":"generateContractorDocument","parameters":[{"name":"contractorId","in":"path","required":true,"schema":{"type":"integer","example":121}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["documentTypeId"],"properties":{"documentTypeId":{"type":"integer","example":411},"templateId":{"type":"integer","nullable":true},"data":{"description":"Dane formularza dokumentu","type":"object"},"salesPipelineId":{"type":"integer","nullable":true},"fillFromPipeline":{"type":"boolean","default":false},"updatePipeline":{"type":"boolean","default":false}},"type":"object"}}}},"responses":{"201":{"description":"Dokument wygenerowany (documentStatusId=4) albo z błędem renderu (documentStatusId=5, szczegóły w error)","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/GeneratedDocument"}},"type":"object"}}}},"400":{"description":"Parametr zapytania w adresie - ta operacja żadnych nie przyjmuje (query.unknownField)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Brak kontrahenta o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Błędy walidacji (typ, szablon, dane formularza, szansa; updatePipeline z typem HTML - document.updatePipelineUnsupported; typ ze store=false - document.typeNotStored)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień do dokumentów","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/documents/{id}/regenerate":{"post":{"tags":["Documents"],"summary":"Regeneracja dokumentu (nowy render, ten sam numer)","description":"Regeneruje istniejący dokument - ten sam rekord i numer, nowy render pliku.\nUwaga: publikacja online dostaje NOWY link (`publishUrl` zmienia się, stary\nwygasa) - po regeneracji wyślij klientowi świeży link z odpowiedzi.\nPrzydatne po zmianie danych szansy/kontrahenta albo poprawce szablonu w CRM.\n\n- `data` - nowe dane formularza (brak = regeneracja z dotychczasowych\n  danych dokumentu),\n- `fillFromPipeline: true` - odświeża `data.order.products` i walutę\n  produktami przypiętej szansy,\n- `updatePipeline: true` - po wygenerowaniu nadpisuje produkty i wartość\n  SZANSY danymi dokumentu. Niedostępne dla dokumentów z szablonu HTML\n  (`templateId` ustawione) - 422 `document.updatePipelineUnsupported`,\n  bo CRM kasuje tam pozycje szansy bez odtworzenia.\n\nPrzypięcia (kontrahent, szansa), typ i szablon dokumentu zostają bez zmian.\nTyp dokumentu musi zapisywać dokumenty w CRM (`store: true`) - inaczej 422\n`document.typeNotStored`. Operacja nie przyjmuje parametrów zapytania\nw adresie (400 `query.unknownField`).","operationId":"regenerateDocument","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":294}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"properties":{"data":{"description":"Nowe dane formularza; brak = użycie danych dokumentu","type":"object","nullable":true},"fillFromPipeline":{"type":"boolean","default":false},"updatePipeline":{"type":"boolean","default":false}},"type":"object"}}}},"responses":{"200":{"description":"Dokument po regeneracji","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/GeneratedDocument"}},"type":"object"}}}},"400":{"description":"Parametr zapytania w adresie - ta operacja żadnych nie przyjmuje (query.unknownField)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Brak dokumentu o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Błędy walidacji (dane formularza, brak przypiętej szansy dla flag; updatePipeline na dokumencie z szablonu HTML - document.updatePipelineUnsupported; typ ze store=false - document.typeNotStored)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień do dokumentów","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/documents/{id}":{"get":{"tags":["Documents"],"summary":"Dokument generatora po id","description":"Dokument z generatora: status, numer, publiczny publishUrl i świeży downloadUrl (podpisany link do PDF, ważny 1 minutę).","operationId":"getGeneratedDocument","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":294}}],"responses":{"200":{"description":"Dokument","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/GeneratedDocument"}},"type":"object"}}}},"404":{"description":"Brak dokumentu o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/document/types/{id}/source":{"post":{"tags":["Documents"],"summary":"Upload pliku źródłowego szablonu (multipart)","description":"Wgrywa plik źródłowy szablonu - request **multipart/form-data** z plikiem\nw polu `file` (doc, docx, odt, rtf, txt, html, ppt, pptx, odp; maks. 50 MB).\nCRM rejestruje plik w usłudze Google Docs, udostępnia go na `shareEmails`\ntypu, generuje podgląd PDF i wykrywa zmienne `{{...}}` - odpowiedź niesie\nwykrytą listę `variables`. Ponowny upload podmienia poprzednie źródło.\n\nZmienne wstawia się w treści dokumentu jako `{{nazwa_zmiennej}}` - każda\nmusi potem dostać pole w formularzu (PUT /v2/document/types/{id}/form),\ninaczej typu nie da się aktywować.\n\nGdy usługa dokumentowa jest niedostępna, wraca 502\n`documentType.serviceUnavailable` - szczegóły w logu pod `requestId`.","operationId":"uploadDocumentTypeSource","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":512}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"required":["file"],"properties":{"file":{"description":"Plik źródłowy szablonu (doc/docx/odt/rtf/txt/html/ppt/pptx/odp, maks. 50 MB)","type":"string","format":"binary"}},"type":"object"}}}},"responses":{"200":{"description":"Plik zarejestrowany, zmienne wykryte","content":{"application/json":{"schema":{"properties":{"data":{"properties":{"id":{"type":"integer","example":512},"variables":{"description":"Zmienne {{...}} wykryte w pliku","type":"array","items":{"type":"string"},"example":["numer_umowy","nazwa_klienta"]},"draft":{"type":"boolean","example":true},"templateVersion":{"type":"integer","example":2},"sourceMime":{"type":"string","example":"application/vnd.google-apps.document","nullable":true}},"type":"object"}},"type":"object"}}}},"404":{"description":"Brak typu dokumentu o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Brak pliku / zły format / typ systemowy bez cyklu Google Docs","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnienia administracji szablonami dokumentów","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"Usługa dokumentowa (Google Docs) niedostępna - documentType.serviceUnavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/document/types/{id}/activate":{"post":{"tags":["Documents"],"summary":"Aktywacja typu dokumentu","description":"Aktywuje typ dokumentu - szkic staje się pełnoprawnym typem: pojawia się\nw GET /v2/document/types (bez `includeInactive`) i generuje dokumenty\nprzez POST /v2/contractors/{id}/documents.\n\nWarunki: wgrany plik źródłowy oraz formularz pokrywający wszystkie\nzmienne z pliku - inaczej 422 z listą brakujących zmiennych\n(`documentType.variableNotCovered` per zmienna).","operationId":"activateDocumentType","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":512}}],"responses":{"200":{"description":"Typ aktywny (active: true, draft: false)","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/DocumentType"}},"type":"object"}}}},"404":{"description":"Brak typu dokumentu o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Brak pliku źródłowego albo zmienne bez pól w formularzu (lista w errors)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnienia administracji szablonami dokumentów","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/document/categories":{"get":{"tags":["Documents"],"summary":"Kategorie typów dokumentów generatora","description":"Wszystkie kategorie typów dokumentów generatora - płaska lista z `parentId`\n(drzewo składasz po stronie klienta). Kategorie z `system: true` to katalogi\nwbudowane CRM: widoczne tutaj, ale tylko do odczytu i nie mogą być rodzicem.\nId kategorii niesystemowej podajesz jako `categoryId`\nw POST /v2/document/types.\n\nSortowanie: `priority` malejąco, potem `name` rosnąco, potem `id` rosnąco.\n\nDostępne od wersji API 2.4.0.","operationId":"listDocumentCategories","responses":{"200":{"description":"Kategorie dokumentów","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/DocumentCategory"}}},"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"post":{"tags":["Documents"],"summary":"Nowa kategoria typów dokumentów","description":"Zakłada kategorię typów dokumentów (zawsze niesystemową). `parentId`\nwskazuje istniejącą, niesystemową kategorię nadrzędną (null/brak =\nnajwyższy poziom). Nazwa maksymalnie 64 znaki (limit CRM).\n\nWymaga uprawnienia administracji szablonami dokumentów\n(`documents.templateAdmin` albo `system.admin`).\n\nDostępne od wersji API 2.4.0.","operationId":"createDocumentCategory","requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["name"],"properties":{"name":{"description":"Maksymalnie 64 znaki","type":"string","example":"Umowy handlowe"},"parentId":{"description":"Istniejąca, niesystemowa kategoria nadrzędna; null/brak = najwyższy poziom","type":"integer","example":null,"nullable":true},"priority":{"description":"Kolejność na listach (wyższa = wyżej)","type":"integer","default":0}},"type":"object"}}}},"responses":{"201":{"description":"Kategoria założona","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/DocumentCategory"}},"type":"object"}}}},"422":{"description":"Błędy walidacji (brak nazwy, nazwa ponad 64 znaki, zły parentId)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnienia administracji szablonami dokumentów","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/document/categories/{id}":{"put":{"tags":["Documents"],"summary":"Edycja kategorii typów dokumentów","description":"Edycja kategorii typów dokumentów - zmienia TYLKO podane pola\n(`name`, `parentId`, `priority`). `parentId: null` przenosi kategorię\nna najwyższy poziom; rodzic nie może być kategorią systemową, tą samą\nkategorią ani jej podkategorią (cykl w drzewie). Kategorie systemowe\n(`system: true`) są tylko do odczytu - 422 `documentCategory.systemReadOnly`.\n\nWymaga uprawnienia administracji szablonami dokumentów\n(`documents.templateAdmin` albo `system.admin`).\n\nDostępne od wersji API 2.4.0.","operationId":"updateDocumentCategory","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":512}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"properties":{"name":{"description":"Maksymalnie 64 znaki","type":"string","example":"Umowy serwisowe"},"parentId":{"description":"Nowy rodzic (niesystemowy, bez cyklu); null = najwyższy poziom","type":"integer","example":511,"nullable":true},"priority":{"description":"Kolejność na listach (wyższa = wyżej)","type":"integer","example":5}},"type":"object"}}}},"responses":{"200":{"description":"Kategoria po zapisie","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/DocumentCategory"}},"type":"object"}}}},"404":{"description":"Brak kategorii o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Kategoria systemowa (documentCategory.systemReadOnly), zły parentId (cykl, kategoria systemowa), nazwa ponad 64 znaki, puste body","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnienia administracji szablonami dokumentów","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/document/numerations":{"get":{"tags":["Documents"],"summary":"Schematy numeracji dokumentów generatora","description":"Schematy numeracji dokumentów generatora - tylko odczyt (numeracje mają\nwłasne liczniki, zarządza nimi panel CRM). Lista obejmuje też schematy\nnieaktywne (`active: false`) - ale jako `numerationId`\nw POST /v2/document/types przejdzie wyłącznie aktywny.\n\nW `schema` znaczniki: `[NR]` = kolejny numer licznika, `[MM]` = miesiąc,\n`[YYYY]` = rok. `resetPeriod` mówi, kiedy licznik wraca do `startNumber`:\n`year` = co rok, `month` = co miesiąc.\n\nSortowanie: `priority` malejąco, potem `id` rosnąco.\n\nDostępne od wersji API 2.4.0.","operationId":"listDocumentNumerations","responses":{"200":{"description":"Schematy numeracji","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"properties":{"id":{"type":"integer","example":399},"schema":{"description":"Wzór numeru ([NR] = licznik, [MM] = miesiąc, [YYYY] = rok)","type":"string","example":"DOK/[NR]/[MM]/[YYYY]"},"startNumber":{"description":"Wartość licznika po resecie","type":"integer","example":1},"resetPeriod":{"description":"Reset licznika: year = co rok, month = co miesiąc","type":"string","enum":["year","month"],"example":"year","nullable":true},"active":{"description":"Tylko aktywny schemat można wskazać jako numerationId typu dokumentu","type":"boolean","example":true},"priority":{"description":"Kolejność na listach (wyższa = wyżej)","type":"integer","example":0}},"type":"object"}}},"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/health":{"get":{"tags":["System"],"summary":"Healthcheck (publiczny, bez nagłówków auth)","description":"Self-check API: weryfikuje, że kod CRM, na którym wisi v2, jest na miejscu (autoload, klasy). Publiczny - dla monitoringu i deploy-verification core (działa też podczas przerwy serwisowej). Degradacja = 503.","operationId":"health","responses":{"200":{"description":"API sprawne","content":{"application/json":{"schema":{"properties":{"status":{"type":"string","example":"ok"},"version":{"description":"Wersja API v2 wdrożona na TEJ instancji (semver). Integracja podpięta do kilku instalacji o różnym stanie wdrożenia porównuje ją z wersją, od której działa dana opcja - jest podana w opisie pola w dokumentacji.","type":"string","example":"1.7.0"},"contract":{"description":"Adresy opisu kontraktu tej instancji: pełna specyfikacja (openapi) i dokumentacja do czytania (docs).","properties":{"openapi":{"type":"string","example":"/v2/openapi.json"},"docs":{"type":"string","example":"/v2/docs"}},"type":"object"},"checks":{"properties":{"crmAutoload":{"type":"boolean","example":true}},"type":"object"}},"type":"object"}}}},"503":{"description":"Degradacja - część kontraktu z CRM zniknęła (szczegóły w checks)"}}}},"/v2/integrations/tillio-calls":{"get":{"tags":["Integrations"],"summary":"Stan integracji Tillio Calls","description":"Stan integracji z Tillio Calls w tej instancji: czy jest zarejestrowana, pod jakim\nadresem i czy ma zapisany klucz. **Klucza nie zwracamy** - `hasApiKey` mówi tylko,\nczy jest.\n\n`registered: false` znaczy, że integracji nie zarejestrowano (albo została\nodłączona). Zapis rozmów może mimo to działać, jeśli konfiguracja powstała sama\nprzy pierwszym zapisie - wtedy jednak nie ma klucza i nagrania są niedostępne.\n\nDostępne od wersji API 2.11.0.","operationId":"getTillioCallsIntegration","responses":{"200":{"description":"Stan integracji","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/TillioCallsIntegration"}},"type":"object"}}}},"403":{"description":"Moduł telefonii nieaktywny (module.notActive)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"put":{"tags":["Integrations"],"summary":"Rejestracja integracji Tillio Calls (adres API i klucz)","description":"Rejestruje (albo aktualizuje) integrację z Tillio Calls w tej instancji CRM.\nWoła się to RAZ, zaraz po tym, jak usługa dostanie klucz do API v2 - dalej\nwszystko idzie już normalnymi końcówkami (`/v2/phone-calls`, `/v2/text-messages`).\n\nCo się dzieje: w CRM powstaje dostawca telefonii „Tillio Calls\" i jego\nkonfiguracja, a w niej **adres API Calls i klucz**. Klucz jest poświadczeniem do\nsystemu Calls - to Calls go generuje przy aktywacji instancji i to Calls go\nunieważnia przy odłączeniu; API v2 zapisuje go zaszyfrowany tym samym mechanizmem,\nktórego CRM używa dla pozostałych integracji telefonicznych. Dzięki temu CRM ma\nczym sięgnąć po nagranie rozmowy zamiast kopiować plik do siebie.\n\n**Idempotentne:** ponowne wywołanie podmienia poświadczenia, nie tworzy drugiego\ndostawcy. Zapis rozmów działa też bez rejestracji (konfiguracja powstanie sama\nprzy pierwszym zapisie), ale wtedy jest bez klucza, więc nagrania są niedostępne.\n\nOdpowiedź nigdy nie zawiera klucza.\n\nDostępne od wersji API 2.11.0.","operationId":"registerTillioCallsIntegration","requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["apiUrl","apiKey"],"properties":{"apiUrl":{"description":"Adres API Tillio Calls tej instancji (https)","type":"string","example":"https://k7f3a2c1-calls.tillio.app"},"apiKey":{"description":"Klucz do API Tillio Calls (tylko odczyt rozmów i nagrań), wygenerowany po stronie Calls","type":"string","example":"tc_live_9c1e..."}},"type":"object"}}}},"responses":{"200":{"description":"Integracja zarejestrowana","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/TillioCallsIntegration"}},"type":"object"}}}},"403":{"description":"Moduł telefonii nieaktywny na instancji (module.notActive) - bez niego nie ma gdzie podpiąć Tillio Calls","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Brak albo zły adres/klucz - {field, code, message}","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"delete":{"tags":["Integrations"],"summary":"Odłączenie integracji Tillio Calls","description":"Odłącza integrację: konfiguracja z poświadczeniami przestaje być aktywna, więc CRM\nnie sięga już po nagrania. **Historia zostaje** - rozmowy, SMS-y i notatki, które\njuż trafiły do CRM, nie są ruszane; to dane klienta, nie poświadczenia.\n\nPonowna rejestracja (`PUT`) zakłada świeżą konfigurację i wszystko działa dalej.\nOdłączenie integracji, której nie ma, kończy się 404.\n\nDostępne od wersji API 2.11.0.","operationId":"disconnectTillioCallsIntegration","responses":{"204":{"description":"Odłączona (bez treści)"},"403":{"description":"Moduł telefonii nieaktywny (module.notActive)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Integracja nie jest zarejestrowana (integration.notFound)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/leads":{"get":{"tags":["Leads"],"summary":"Lista leadów z filtrowaniem po dowolnym polu i polach niestandardowych","description":"Leady (szanse sprzedażowe przed konwersją na kontrahenta) dla integracji\n(marketing automation, import, raportowanie) i aplikacji.\n\n**Synchronizacja przyrostowa:** przekaż `updatedAfter` z datą ostatniego udanego\nsyncu - dostaniesz wyłącznie leady utworzone lub zmienione po tej chwili\n(`updatedAt` obejmuje także świeżo utworzone rekordy).\n\n**Typowe scenariusze:**\n- lejek użytkownika: `?ownerUserId=7&leadStatusId=6`\n- sprawdzenie przed importem, czy lead już istnieje: `?taxId=5252344078` albo `?domain=acme.pl`\n- matchowanie po kluczu integracji: `customField[<klucz>]=<wartość>` (pusta wartość\n  = pole nieustawione)\n\nPola tekstowe (`title`, `companyName`, `name`, `phone`, `city`...) filtrują częściowo\n(zawiera), klucze integracyjne (`taxId`, `regon`, `postCode`, `country`) i wszystkie\npola `*Id` - dokładnie.","operationId":"listLeads","parameters":[{"name":"leadStatusId","in":"query","description":"Status leada wg słownika CRM (dokładne).","required":false,"schema":{"type":"integer","example":6}},{"name":"leadStageId","in":"query","description":"Etap procesu - grupa statusów (dokładne).","required":false,"schema":{"type":"integer"}},{"name":"ownerUserId","in":"query","description":"Leady opiekuna (dokładne). Analogicznie dokładne: creatorUserId, contractorId, contactId, salesPipelineId, categoryId, contractorSourceId, statusChangeReasonId, priority, id.","required":false,"schema":{"type":"integer","example":7}},{"name":"title","in":"query","description":"Filtr częściowy (zawiera). Analogicznie: companyName, firstName, lastName, position, phone, phoneAlternative, street, city, region. Uwaga: domain filtruje DOKŁADNIE - to klucz sprawdzania, czy lead już istnieje.","required":false,"schema":{"type":"string"}},{"name":"taxId","in":"query","description":"NIP - dopasowanie dokładne. Analogicznie dokładne: regon, postCode, country.","required":false,"schema":{"type":"string","example":"5252344078"}},{"name":"updatedAfter","in":"query","description":"Tylko leady utworzone/zmienione PO tej chwili (ISO 8601). Fundament syncu przyrostowego.","required":false,"schema":{"type":"string","format":"date-time","example":"2026-08-01T00:00:00Z"}},{"name":"updatedBefore","in":"query","description":"Górna granica zakresu zmian (ISO 8601, włącznie).","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"createdAfter","in":"query","description":"Tylko leady utworzone po tej chwili (ISO 8601).","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"createdBefore","in":"query","description":"Tylko leady utworzone do tej chwili (ISO 8601, włącznie).","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"customField","in":"query","description":"Filtry pól niestandardowych: `customField[<klucz>]=<wartość>` (dokładne; klucze i typy w GET /v2/<encja>/custom-fields). Pusta wartość = pole nieustawione.","required":false,"style":"deepObject","explode":true,"schema":{"type":"object","additionalProperties":{"type":"string"}}},{"name":"id","in":"query","required":false,"description":"Filtr po polu id - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"statusChangeReasonId","in":"query","required":false,"description":"Filtr po polu statusChangeReasonId - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"categoryId","in":"query","required":false,"description":"Filtr po polu categoryId - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"contractorSourceId","in":"query","required":false,"description":"Filtr po polu contractorSourceId - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"priority","in":"query","required":false,"description":"Filtr po polu priority - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"creatorUserId","in":"query","required":false,"description":"Filtr po polu creatorUserId - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"contractorId","in":"query","required":false,"description":"Filtr po polu contractorId - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"contactId","in":"query","required":false,"description":"Filtr po polu contactId - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"salesPipelineId","in":"query","required":false,"description":"Filtr po polu salesPipelineId - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"companyName","in":"query","required":false,"description":"Filtr po polu companyName - dopasowanie częściowe (zawiera).","schema":{"type":"string"}},{"name":"regon","in":"query","required":false,"description":"Filtr po polu regon - dopasowanie dokładne.","schema":{"type":"string"}},{"name":"domain","in":"query","required":false,"description":"Filtr po polu domain - dopasowanie dokładne.","schema":{"type":"string"}},{"name":"firstName","in":"query","required":false,"description":"Filtr po polu firstName - dopasowanie częściowe (zawiera).","schema":{"type":"string"}},{"name":"lastName","in":"query","required":false,"description":"Filtr po polu lastName - dopasowanie częściowe (zawiera).","schema":{"type":"string"}},{"name":"position","in":"query","required":false,"description":"Filtr po polu position - dopasowanie częściowe (zawiera).","schema":{"type":"string"}},{"name":"phone","in":"query","required":false,"description":"Filtr po polu phone - dopasowanie częściowe (zawiera).","schema":{"type":"string"}},{"name":"phoneAlternative","in":"query","required":false,"description":"Filtr po polu phoneAlternative - dopasowanie częściowe (zawiera).","schema":{"type":"string"}},{"name":"street","in":"query","required":false,"description":"Filtr po polu street - dopasowanie częściowe (zawiera).","schema":{"type":"string"}},{"name":"postCode","in":"query","required":false,"description":"Filtr po polu postCode - dopasowanie dokładne.","schema":{"type":"string"}},{"name":"city","in":"query","required":false,"description":"Filtr po polu city - dopasowanie częściowe (zawiera).","schema":{"type":"string"}},{"name":"region","in":"query","required":false,"description":"Filtr po polu region - dopasowanie częściowe (zawiera).","schema":{"type":"string"}},{"name":"country","in":"query","required":false,"description":"Filtr po polu country - dopasowanie dokładne.","schema":{"type":"string"}},{"name":"sort","in":"query","description":"Pole sortowania: id, title, priority, companyName, createdAt, updatedAt, closedAt, lastActivityAt.","required":false,"schema":{"type":"string","default":"updatedAt"}},{"name":"sortDir","in":"query","required":false,"schema":{"type":"string","default":"asc","enum":["asc","desc"]}},{"name":"page","in":"query","description":"Strona wyników, od 1.","required":false,"schema":{"type":"integer","default":1,"maximum":1000000,"minimum":1}},{"name":"limit","in":"query","description":"Rozmiar strony (max 1000).","required":false,"schema":{"type":"integer","default":100,"maximum":1000,"minimum":1}}],"responses":{"200":{"description":"Lista leadów + metadane stronicowania","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Lead"}},"pagination":{"$ref":"#/components/schemas/Pagination"}},"type":"object"}}}},"400":{"description":"Błąd walidacji zapytania - lista błędów w kontrakcie {field, code, message}","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"post":{"tags":["Leads"],"summary":"Nowy lead (create-or-attach po email/phone)","description":"**Create-or-attach (od wersji API 2.13.0):** przed zapisem API samo sprawdza,\nczy taki lead już istnieje - domyślnie po `email` (którykolwiek adres leada\nz listy `emails`) i `phone` (oba numery leada). ZNALEZIONY lead NIE jest\nduplikowany - API **podpina do niego dane** z żądania: pola tekstowe\nuzupełniają wyłącznie puste (nie nadpisują - tytuł istniejącego leada\nzostaje), telefon trafia w wolny numer (główny → alternatywny → warning gdy\noba zajęte), `emails` są DOKŁADANE jako kolejne adresy (istniejące zostają,\ngłówny bez zmiany - inaczej niż w PUT, który wymienia listę), `customField`\njest nadpisywane (klucze integracji mają być aktualne), a `leadStatusId`,\n`contractorSourceId`, `createdAt` i `creatorUserId` (tylko przy tworzeniu)\nsą pomijane z wpisem w `info.warnings`. Odpowiedź: **HTTP 200**,\n`info.created = false`, `info.duplicate = {matchedBy, leadId}` i lead po\npodpięciu w `data`. Gdy znaleziony lead jest już skonwertowany na\nkontrahenta, `info.duplicate.contractorId` wskazuje tego kontrahenta - to\njuż klient, a co z tym zrobić, decyduje integracja. Odmowa przy podpinaniu\nnie zmienia kodu 200: lead istnieje i to on jest wynikiem, a szczegół\ntrafia do `info.warnings.attach` (pola i adresy) albo\n`info.warnings.customField` (np. pole niestandardowe nieprzypisane do grupy\nstatusu, w której jest istniejący lead).\n\nTablicą `duplicateCheck` (enum: email, phone, taxId, domain, companyName\noraz `custom:<klucz>` - pola typu INT/STR/VARCHAR z GET /v2/lead/custom-fields)\nustawiasz pola i ich kolejność (priorytet). Lead nie ma `externalId` -\nklucz integracji trzyma się w polu niestandardowym. `allowDuplicates: true`\nwyłącza sprawdzanie (zawsze nowy lead). Integracja formularzowa (Zapier)\nwysyła jeden POST i nie musi sama szukać leada po e-mailu.\n\n**Nowy lead** powstaje tą samą ścieżką co aplikacja Tillio. Wymagane tylko\n`title`. Pola: title, note (HTML), ownerUserId, priority, companyName, taxId,\nregon, domain, firstName, lastName, position, phone, phoneAlternative, adres\n(street, street2, postCode, city, country), leadStatusId (GET /v2/lead/statuses -\nbrak = status domyślny wg konfiguracji CRM), contractorSourceId, emails (lista\nadresów e-mail, pierwszy = główny - od wersji API 2.13.0), customField\n(klucze w GET /v2/lead/custom-fields; pola przypisane do GRUP statusów -\nnieprzypisane do grupy statusu leada dostają 422). Autor = użytkownik\nklucza API. Wartości typowane (taxId/domain/phone) normalizowane -\nnie do uratowania = null + info.warnings. Odpowiedź: **HTTP 201**,\n`info.created = true`.\n\n**Adresy e-mail (`emails`):** walidowane PRZED zapisem - błędny adres to 422\nna `emails[<indeks>]` i lead nie powstaje. Gdy CRM odrzuci zapis adresów już\npo utworzeniu leada, lead zostaje, a odpowiedź niesie `info.warnings.emails`.","operationId":"createLead","requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["title"],"properties":{"title":{"type":"string","example":"ACME - zapytanie o wdrożenie"},"note":{"type":"string","description":"Treść w HTML (CRM czyści niebezpieczne znaczniki)."},"ownerUserId":{"type":"integer"},"priority":{"type":"integer"},"companyName":{"type":"string","example":"ACME Sp. z o.o."},"taxId":{"type":"string"},"regon":{"type":"string"},"domain":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"position":{"type":"string"},"phone":{"description":"Normalizowany do formatu międzynarodowego (9 cyfr = +48..., prefiks bez plusa dostaje +, mniej niż 9 cyfr = wartość odrzucana - info.warnings). Od wersji API 1.32.0.","type":"string","example":"+48 601 234 567"},"phoneAlternative":{"type":"string"},"street":{"type":"string"},"street2":{"type":"string"},"postCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"leadStatusId":{"description":"GET /v2/lead/statuses; brak = default CRM","type":"integer"},"contractorSourceId":{"type":"integer","description":"Wartość musi istnieć w słowniku CRM; tylko przy tworzeniu."},"createdAt":{"description":"IMPORT DANYCH: data utworzenia rekordu (ISO 8601, strefa instancji). Pozwala zachować chronologię przy wciąganiu historii sprzed lat - bez niej rekord dostaje datę importu. Tylko przy tworzeniu: przy aktualizacji zwracamy 422 (pomyłkę w imporcie poprawia się kasując rekord i importując ponownie). Data z przyszłości = 422. Dostępne od wersji API 1.16.0.","type":"string","format":"date-time","example":"2023-05-17T09:30:00"},"creatorUserId":{"description":"IMPORT DANYCH: użytkownik, który ma figurować jako twórca rekordu (GET /v2/users). Brak = użytkownik przypisany do klucza API. Zapis wykonuje się w kontekście tej osoby, więc obowiązują JEJ uprawnienia. Dostępne od wersji API 1.16.0.","type":"integer","example":42},"emails":{"description":"Adresy e-mail leada - lista tekstów, PIERWSZY = adres główny (ta sama nazwa i forma co w odczycie). Każdy adres normalizowany jak pole email kontaktu (małe litery, wyjęcie adresu z tekstu); powtórzenia pomijane, najwyżej 20 adresów. Niepoprawny adres = 422 na emails[<indeks>], zły typ (tekst zamiast listy, liczba na liście) = 422 na emails. Pusta lista = lead bez adresów. Przy podpięciu do istniejącego leada adresy są DOKŁADANE (istniejące zostają, główny bez zmiany). Dostępne od wersji API 2.13.0.","type":"array","items":{"type":"string","example":"jan.kowalski@acme.pl"},"maxItems":20},"customField":{"description":"Klucze w GET /v2/lead/custom-fields. Przy podpięciu do istniejącego leada wartości są NADPISYWANE (klucze integracji mają być aktualne).","type":"object","additionalProperties":{"nullable":true}},"duplicateCheck":{"description":"Pola, po których szukamy istniejącego leada, w kolejności priorytetu (default: [\"email\",\"phone\"]). Enum: email (którykolwiek adres z `emails` leada), phone (oba numery leada, niezależnie od zapisu z plusem/bez), taxId, domain, companyName (dokładnie) oraz \"custom:<klucz>\" - pole niestandardowe leada typu INT/STR/VARCHAR (klucz z GET /v2/lead/custom-fields). Sprawdzane są wyłącznie pola, których wartość jest w TYM SAMYM żądaniu (np. \"custom:zapier_id\" wymaga customField[zapier_id]); pominięte wracają w info.warnings.duplicateCheck, a gdy żadne nie ma wartości - 422 body.duplicateCheckUnusable zamiast cichego założenia dubla. Opcja `requireDuplicateCheck: true` (tryb dla importów, fail-closed) wymaga, by KAŻDE pole z listy miało wartość - brak choćby jednego kończy się 422 wskazującym to konkretne pole, a nie samo duplicateCheck; działa też przy domyślnym zestawie. Dostępne od wersji API 2.13.0.","type":"array","items":{"type":"string"},"example":["email","phone"]},"requireDuplicateCheck":{"description":"true = każde pole z duplicateCheck musi mieć wartość w żądaniu (tryb importu, fail-closed). Dostępne od wersji API 2.13.0.","type":"boolean","default":false},"allowDuplicates":{"description":"true = bez sprawdzania, czy lead już istnieje - zawsze nowy lead. Dostępne od wersji API 2.13.0.","type":"boolean","default":false}},"type":"object"}}}},"responses":{"201":{"description":"Lead utworzony (info.created = true)","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/Lead"},"info":{"$ref":"#/components/schemas/WriteInfo"}},"type":"object"}}}},"200":{"description":"Lead już istnieje - dane podpięte do istniejącego. info.created = false, info.duplicate = {matchedBy, leadId, contractorId?} (contractorId tylko, gdy lead jest już skonwertowany na kontrahenta), info.ids.leadId = id istniejącego leada. Od wersji API 2.13.0.","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/Lead"},"info":{"$ref":"#/components/schemas/WriteInfo"}},"type":"object"}}}},"422":{"description":"Błędy walidacji - lista {field, code, message}; body.duplicateCheckUnusable = żadne pole z duplicateCheck nie ma wartości w żądaniu","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień do leadów","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/leads/{id}":{"get":{"tags":["Leads"],"summary":"Pojedynczy lead po id","description":"Pełne dane leada wraz z polami niestandardowymi i adresami e-mail.","operationId":"getLead","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":659}}],"responses":{"200":{"description":"Lead","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/Lead"}},"type":"object"}}}},"404":{"description":"Brak leada o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"put":{"tags":["Leads"],"summary":"Aktualizacja leada (partial)","description":"Częściowa aktualizacja leada - wysyłasz TYLKO pola do zmiany. `leadStatusId`\ni `contractorSourceId` nie podlegają edycji przez API (zmiana statusu leada to\nw CRM osobny proces z powodami zmian) - 422 body.fieldNotUpdatable.\n\n**Adresy e-mail (`emails`, od wersji API 2.13.0):** KOMPLETNA lista docelowa -\nzastępuje dotychczasową (pusta lista `[]` usuwa wszystkie adresy), pierwszy =\ngłówny. Adresy, które zostają na liście, zachowują swoje identyfikatory w CRM\n(nie są kasowane i wstawiane od nowa). Walidacja PRZED zapisem: błędny adres\n= 422 i lead nie zmienia się; odmowa CRM już po zapisie pól leada =\n`info.warnings.emails`.","operationId":"updateLead","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":659}}],"requestBody":{"description":"Aktualizacja CZĘŚCIOWA - wysyłasz tylko pola do zmiany. Poza listą poniżej zapis przyjmuje `customField`. Pola ustawiane WYŁĄCZNIE przy tworzeniu (próba zmiany = 422 `body.fieldNotUpdatable`): leadStatusId, contractorSourceId.","required":true,"content":{"application/json":{"schema":{"properties":{"title":{"type":"string"},"note":{"type":"string","description":"Treść w HTML (CRM czyści niebezpieczne znaczniki)."},"ownerUserId":{"type":"integer"},"priority":{"type":"integer"},"companyName":{"type":"string"},"taxId":{"type":"string"},"regon":{"type":"string"},"domain":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"position":{"type":"string"},"phone":{"type":"string"},"phoneAlternative":{"type":"string"},"street":{"type":"string"},"street2":{"type":"string"},"postCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"emails":{"description":"Kompletna lista docelowa adresów e-mail (pierwszy = główny); `[]` usuwa wszystkie. Reguły walidacji jak w POST. Od wersji API 2.13.0.","type":"array","items":{"type":"string","example":"jan.kowalski@acme.pl"},"maxItems":20},"customField":{"type":"object","description":"Pola niestandardowe: klucz → wartość (klucze i typy: GET /v2/<encja>/custom-fields). Pola wielowartościowe (multiselect, produkty, szanse sprzedaży) przyjmują listę wartości, np. [\"12\",\"15\"] - pojedyncza wartość jest równoważna liście jednoelementowej. Lista dostępna od wersji API 2.9.0.","additionalProperties":{"nullable":true}}},"type":"object"}}}},"responses":{"200":{"description":"Lead po aktualizacji","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/Lead"},"info":{"$ref":"#/components/schemas/WriteInfo"}},"type":"object"}}}},"404":{"description":"Brak leada o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Błędy walidacji","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień do leadów","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/leads/upsert":{"post":{"tags":["Leads"],"summary":"Batch upsert leadów (create albo podpięcie danych)","description":"Batch create-or-attach (od wersji API 2.13.0): dla każdego itemu API\nsprawdza, czy lead już istnieje (pola z `duplicateCheck` w kopercie, default\nemail/phone) - **nie istnieje → create**, **istnieje → podpięcie danych**\n(semantyka jak w POST /v2/leads: uzupełnianie pustych pól, dokładanie\nadresów e-mail, nadpisanie customField; status i źródło tylko przy\ntworzeniu).\n\nKoperta: `items` (1-100 obiektów jak payload POST, bez opcji) +\n`duplicateCheck` / `requireDuplicateCheck` wspólne dla paczki\n(`allowDuplicates` nie ma tu zastosowania - 422). HTTP zawsze **200** przy\npoprawnej kopercie - wynik per item w `data.results[]`\n(`status: created|attached|failed`, `leadId`, `matchedBy` i `contractorId`\nskonwertowanego leada przy attached, `errors` przy failed), podsumowanie\nw `info.summary`. Błąd jednego itemu nie przerywa paczki; bez transakcji\nmiędzy itemami.","operationId":"upsertLeads","requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["items"],"properties":{"items":{"description":"Lista leadów (payload jak POST /v2/leads, bez opcji; pola importu createdAt/creatorUserId per item) - max 100","type":"array","items":{"type":"object"},"example":[{"title":"ACME - formularz","companyName":"ACME Sp. z o.o.","emails":["jan@acme.pl"],"phone":"+48 601 234 567"}]},"duplicateCheck":{"description":"Jak w POST /v2/leads (default [\"email\",\"phone\"]), wspólne dla całej paczki.","type":"array","items":{"type":"string"},"example":["email","phone"]},"requireDuplicateCheck":{"description":"Jak w POST /v2/leads - każde pole z duplicateCheck musi mieć wartość w KAŻDYM itemie (item bez niej dostaje status failed).","type":"boolean","default":false}},"type":"object"}}}},"responses":{"200":{"description":"Wynik per item (multi-status w body) + podsumowanie","content":{"application/json":{"schema":{"properties":{"data":{"properties":{"results":{"type":"array","items":{"properties":{"index":{"type":"integer"},"status":{"type":"string","enum":["created","attached","failed"]},"leadId":{"type":"integer"},"matchedBy":{"description":"Pole, po którym znaleziono istniejący lead (tylko status=attached)","type":"string"},"contractorId":{"description":"Kontrahent, na którego lead został już skonwertowany (tylko status=attached i tylko dla skonwertowanego leada)","type":"integer"},"warnings":{"type":"object"},"errors":{"type":"array","items":{"type":"object"}}},"type":"object"}}},"type":"object"},"info":{"properties":{"summary":{"properties":{"total":{"type":"integer"},"created":{"type":"integer"},"attached":{"type":"integer"},"failed":{"type":"integer"}},"type":"object"}},"type":"object"}},"type":"object"}}}},"422":{"description":"Zepsuta koperta requestu (items, opcje) - itemy nie były przetwarzane","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień do leadów","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/lookup/phone":{"get":{"tags":["Lookup"],"summary":"Kto dzwoni - kontakty i kontrahenci po numerze telefonu (dokładnie)","description":"Kto dzwoni: dla podanego numeru zwraca w JEDNYM żądaniu kontakty (osoby) mające ten\nnumer w polu `phone` LUB `phoneAlternative` oraz kontrahentów z tym numerem w polu\n`phone`. Dopasowanie **dokładne** - w odróżnieniu od filtrów `phone` na listach,\nktóre działają częściowo (zawiera) i tylko po jednym polu.\n\nNumer podajesz w dowolnym zapisie (`+48 601 234 567`, `48601234567`, `601234567`,\n`601-234-567`) - API sprowadza go do kanonu międzynarodowego (9 cyfr = numer polski\n`+48...`, więcej cyfr = z prefiksem kraju) i szuka po wszystkich zapisach, jakie\nmogą siedzieć w bazie (z plusem, bez plusa, bez prefiksu kraju). Rekordy sprzed\nujednolicenia zapisu numerów też się znajdują.\n\n**Trafienie = komplet danych** (od wersji API 2.12.0): kontakt wraca w tym samym\nkształcie co `GET /v2/contacts/{id}` (z `email`, `customField`, powiązanymi\nkontrahentami), kontrahent - jak `GET /v2/contractors/{id}` (z `address`\ni `customField`). Rozpoznanie dzwoniącego to JEDNO żądanie, bez dociągania\nkażdego trafienia osobno. Wcześniej lookup oddawał okrojony wiersz\n(id, imię, nazwisko, numery, kontrahenci) - te pola oczywiście zostają.\n\n**Zasady:**\n- brak trafień = HTTP 200 z pustymi listami (nie 404),\n- kontakty nieaktywne wracają z `active: false` (to samo co `contactStatusId: 0`)\n  - to klient decyduje, czy je pokazać,\n- kontakty: aktywne pierwsze; `contractorId` = kontrahent główny, `contractorIds` = wszyscy powiązani,\n- maksymalnie 50 trafień per encja; więcej = `truncated.contacts` / `truncated.contractors` = `true`\n  (numer centrali wpisany do wielu osób) - resztę znajdziesz filtrem `phone` na liście,\n- numer nie do uratowania (mniej niż 9 cyfr, same litery) = 422 `query.invalidValue`.\n\nDostępne od wersji API 2.10.0.","operationId":"lookupPhone","parameters":[{"name":"number","in":"query","description":"Numer telefonu w dowolnym zapisie (spacje, myślniki, z plusem lub bez). Co najmniej 9 cyfr.","required":true,"schema":{"type":"string","example":"+48 601 234 567"}}],"responses":{"200":{"description":"Trafienia w obu encjach (listy mogą być puste)","content":{"application/json":{"schema":{"properties":{"data":{"properties":{"number":{"description":"Numer po sprowadzeniu do kanonu międzynarodowego - po nim szukano","type":"string","example":"+48601234567"},"contacts":{"description":"Kontakty z tym numerem w phone lub phoneAlternative (aktywne pierwsze, max 50). Kształt jak GET /v2/contacts/{id} + pole `active`.","type":"array","items":{"allOf":[{"$ref":"#/components/schemas/Contact"},{"properties":{"active":{"description":"false = kontakt nieaktywny w CRM (to samo co contactStatusId = 0)","type":"boolean","example":true}},"type":"object"}]}},"contractors":{"description":"Kontrahenci z tym numerem w polu phone (max 50). Kształt jak GET /v2/contractors/{id}.","type":"array","items":{"$ref":"#/components/schemas/Contractor"}},"truncated":{"description":"Czy któraś lista została obcięta do limitu 50","properties":{"contacts":{"type":"boolean","example":false},"contractors":{"type":"boolean","example":false}},"type":"object"}},"type":"object"}},"type":"object"}}}},"422":{"description":"Brak parametru number albo wartość nie jest numerem telefonu - {field: number, code: query.required | query.invalidValue}","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/mail/accounts":{"get":{"tags":["Mail"],"summary":"Konta pocztowe instancji","description":"Aktywne konta pocztowe instancji - do wskazania nadawcy w POST /v2/mail/send (po id albo adresie). Bez danych logowania.","operationId":"listMailAccounts","responses":{"200":{"description":"Konta pocztowe","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"properties":{"id":{"type":"integer","example":5},"email":{"type":"string","example":"biuro@firma.pl"},"name":{"description":"Nazwa nadawcy (From)","type":"string","example":"Biuro Firma","nullable":true}},"type":"object"}}},"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/mail/templates":{"get":{"tags":["Mail"],"summary":"Szablony maili","description":"Aktywne szablony maili instancji (bez treści - pełny szablon w GET /v2/mail/templates/{id}). Placeholdery w temacie/treści podstawisz przez `variables` przy wysyłce.","operationId":"listMailTemplates","responses":{"200":{"description":"Szablony","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"properties":{"id":{"type":"integer","example":12},"name":{"type":"string","example":"Oferta - follow up"},"subject":{"type":"string","example":"Twoja oferta od {companyName}"},"categoryId":{"type":"integer","nullable":true}},"type":"object"}}},"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"post":{"tags":["Mail"],"summary":"Nowy szablon maila","description":"Nowy szablon maila - tworzony tą samą ścieżką co panel CRM (walidacje modelu,\nunikalność aliasu, flaga domyślności). Szablon powstaje od razu AKTYWNY\ni jest widoczny w GET /v2/mail/templates.\n\n`alias` pozwala wybrać szablon po nazwie zamiast po id (CRM normalizuje go:\nmałe litery, spacje na `_`, prefix `!`). `default: true` czyni szablon\ndomyślnym - flaga schodzi automatycznie z poprzedniego domyślnego.\n`acl` ogranicza widoczność szablonu do wskazanych użytkowników/działów/grup\n(jednolity kształt {userIds, departmentIds, groupIds}); pominięte = widzą wszyscy.\n\nPlaceholdery `{nazwa}` w temacie i treści podstawisz przez `variables`\nprzy wysyłce (POST /v2/mail/send). Załączniki szablonu konfiguruje się\nw CRM - API ich (jeszcze) nie przyjmuje.\n\nWymaga uprawnienia administratora szablonów webmaila (jak panel CRM).\nDostępne od wersji API 2.4.0.","operationId":"createMailTemplate","requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["name","subject"],"properties":{"name":{"description":"Nazwa szablonu (na listach i w wyborze szablonu)","type":"string","example":"Oferta - follow up"},"subject":{"description":"Temat maila (może zawierać placeholdery {nazwa})","type":"string","example":"Twoja oferta od {companyName}"},"body":{"description":"Treść HTML (może zawierać placeholdery {nazwa})","type":"string","example":"<p>Dzień dobry, w załączeniu oferta: {documentUrl}</p>","nullable":true},"categoryId":{"description":"Kategoria szablonów maili (opcjonalna)","type":"integer","nullable":true},"alias":{"description":"Unikalny alias szablonu (max 31 znaków; CRM znormalizuje do postaci !mały_snake)","type":"string","example":"oferta_follow_up","nullable":true},"to":{"description":"Domyślni adresaci","type":"array","items":{"type":"string"},"example":["biuro@przyklad.pl"]},"cc":{"description":"Domyślni odbiorcy kopii","type":"array","items":{"type":"string"}},"bcc":{"description":"Domyślni odbiorcy ukrytej kopii","type":"array","items":{"type":"string"}},"acl":{"oneOf":[{"$ref":"#/components/schemas/AclJson"}],"nullable":true,"description":"Widoczność szablonu: {userIds?, departmentIds?, groupIds?}. Pominięte albo puste = widzą wszyscy."},"priority":{"description":"Priorytet na liście (wyższy = wyżej)","type":"integer","default":0},"default":{"description":"true = szablon domyślny (flaga schodzi z poprzedniego domyślnego)","type":"boolean","default":false}},"type":"object"}}}},"responses":{"201":{"description":"Szablon utworzony (format jak GET /v2/mail/templates/{id})","content":{"application/json":{"schema":{"properties":{"data":{"properties":{"id":{"type":"integer","example":12},"name":{"type":"string","example":"Oferta - follow up"},"subject":{"type":"string","example":"Twoja oferta od {companyName}"},"body":{"type":"string","nullable":true},"to":{"type":"array","items":{"type":"string"}},"cc":{"type":"array","items":{"type":"string"}},"bcc":{"type":"array","items":{"type":"string"}}},"type":"object"}},"type":"object"}}}},"422":{"description":"Błędy walidacji (m.in. mailTemplate.aliasInUse przy zdublowanym aliasie)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień administratora szablonów webmaila","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/mail/templates/{id}":{"get":{"tags":["Mail"],"summary":"Szablon maila po id","description":"Pełny szablon maila: temat, treść (HTML) i domyślni odbiorcy - do podglądu placeholderów przed wysyłką.","operationId":"getMailTemplate","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":12}}],"responses":{"200":{"description":"Szablon","content":{"application/json":{"schema":{"properties":{"data":{"properties":{"id":{"type":"integer"},"name":{"type":"string"},"subject":{"type":"string"},"body":{"description":"Treść HTML z placeholderami {nazwa}","type":"string","nullable":true},"to":{"type":"array","items":{"type":"string"}},"cc":{"type":"array","items":{"type":"string"}},"bcc":{"type":"array","items":{"type":"string"}}},"type":"object"}},"type":"object"}}}},"404":{"description":"Brak szablonu o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/mail/send":{"post":{"tags":["Mail"],"summary":"Wysyłka maila z konta instancji (szablony + stopka)","description":"Wysyła mail z konta pocztowego instancji - tą samą ścieżką co webmail CRM\n(SMTP/EmailEngine konta, kopia w wysłanych, pixeltrack). Konto nadawcze\nwskazujesz przez `accountId` ALBO `account` (adres e-mail) -\nlista w GET /v2/mail/accounts.\n\n**Szablony:** `templateId` bierze temat, treść i domyślnych odbiorców\nz szablonu (GET /v2/mail/templates); `variables` podstawia placeholdery\n`{nazwa}` w temacie i treści (np. `{documentUrl}` z publishUrl dokumentu);\n`subject`/`body`/`to` podane wprost nadpisują wartości szablonu.\n\n**Stopka:** treść stopki KONTA (konfiguracja webmail instancji,\nz obsługą wersji [*default*]/[*nazwa*] i zmiennych użytkownika/logo)\njest doklejana do treści - jak przy wysyłce z aplikacji.\n`includeFooter: false` wyłącza. Zmienne osobowe stopki (imię, stanowisko,\ntelefon...) renderują się danymi użytkownika z **`footerUserId`** -\nbez niego użyty zostanie techniczny użytkownik klucza API.\n\n`sendAt` (ISO 8601, przyszłość, max +30 dni) = wysyłka zaplanowana.\n\n**Załączniki:** wyślij request jako `multipart/form-data` - parametry\nwysyłki jako JSON w polu `payload`, pliki w polu `attachments[]`\n(limit 50 MB na plik - limit skrzynki, mniejszy niż produktowe 128 MB).\nZałączniki szablonu (skonfigurowane w CRM) dokładają się automatycznie\nprzy `templateId`. Wszystkie załączniki razem (pliki z żądania + załączniki\nszablonu) mogą ważyć najwyżej 50 MB - powyżej `422 body.attachmentsTooLarge`\nna polu `attachments`, sprawdzane przed przyjęciem wiadomości. Pole `payload`\npodlega tym samym limitom co body JSON (2 MB, 20 000 struktur - `413`).","operationId":"sendMail","parameters":[{"name":"Content-Type","in":"header","description":"application/json - bez załączników; multipart/form-data (JSON w polu `payload` + pliki `attachments[]`, max 50 MB/plik i 50 MB łącznie z załącznikami szablonu) - z załącznikami","required":false,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"properties":{"accountId":{"description":"Konto nadawcze po id (to ALBO account) - lista w GET /v2/mail/accounts","type":"integer","example":119},"account":{"description":"Konto nadawcze po adresie e-mail (alternatywa dla accountId, np. \"biuro@firma.pl\")","type":"string"},"to":{"type":"array","items":{"type":"string"},"example":["klient@acme.pl"]},"cc":{"type":"array","items":{"type":"string"}},"bcc":{"type":"array","items":{"type":"string"}},"subject":{"type":"string","example":"Twoja oferta"},"body":{"description":"Treść HTML","type":"string","example":"<p>W załączeniu link do oferty: {documentUrl}</p>"},"templateId":{"description":"Szablon maila (temat/treść/odbiorcy jako podstawa)","type":"integer","nullable":true},"variables":{"description":"Podstawienia placeholderów {nazwa} w temacie i treści","type":"object","example":{"documentUrl":"https://docs.tillio.app/abc12"}},"includeFooter":{"description":"Doklej stopkę konta (default true)","type":"boolean","default":true},"footerUserId":{"description":"Użytkownik systemu (GET /v2/users), którego danymi (imię, stanowisko, telefon...) renderuje się stopka. Bez tego pola stopka użyje technicznego użytkownika klucza API - przy stopkach z danymi osobowymi przekazuj zawsze.","type":"integer","example":1,"nullable":true},"sendAt":{"description":"Wysyłka zaplanowana (ISO 8601, przyszłość, max +30 dni)","type":"string","format":"date-time","nullable":true}},"type":"object"}}}},"responses":{"200":{"description":"Mail wysłany (albo zaplanowany - scheduledAt)","content":{"application/json":{"schema":{"properties":{"data":{"properties":{"sent":{"type":"boolean","example":true},"accountId":{"type":"integer"},"accountEmail":{"type":"string"},"to":{"type":"array","items":{"type":"string"}},"subject":{"type":"string"},"scheduledAt":{"description":"Termin wysyłki zaplanowanej; null = wysłano od razu","type":"string","nullable":true},"attachments":{"description":"Nazwy plików dołączonych z requestu (bez załączników szablonu)","type":"array","items":{"type":"string"}}},"type":"object"}},"type":"object"}}}},"422":{"description":"Błędy walidacji (konto, odbiorcy, szablon; body.attachmentsTooLarge = załączniki ponad 50 MB łącznie)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"Konto pocztowe niepodłączone (mail.accountNotConnected) albo odrzuciło wysyłkę (mail.sendFailed)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień do webmaila","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/mail/template/categories":{"get":{"tags":["Mail"],"summary":"Kategorie szablonów maili","description":"Kategorie szablonów maili jako płaska lista - hierarchię odtworzysz po\n`parentId` (null = kategoria najwyższego poziomu). `id` kategorii podasz\njako `categoryId` w POST /v2/mail/templates.\n\nSortowanie: `priority` malejąco, potem `name` rosnąco (jak w panelu CRM).\n\nDostępne od wersji API 2.4.0.","operationId":"listMailTemplateCategories","responses":{"200":{"description":"Kategorie","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"properties":{"id":{"type":"integer","example":12345},"name":{"type":"string","example":"Oferty"},"parentId":{"description":"Kategoria nadrzędna; null = najwyższy poziom","type":"integer","example":null,"nullable":true},"priority":{"description":"Wyższy = wyżej na liście","type":"integer","example":0}},"type":"object"}}},"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"post":{"tags":["Mail"],"summary":"Nowa kategoria szablonów maili","description":"Nowa kategoria szablonów maili - tą samą ścieżką co panel CRM. `parentId`\nzagnieżdża kategorię pod istniejącą (pominięty albo null = najwyższy poziom).\nUtworzoną kategorię wskażesz jako `categoryId` w POST /v2/mail/templates.\n\nWymaga uprawnienia administratora szablonów webmaila (jak panel CRM).\nDostępne od wersji API 2.4.0.","operationId":"createMailTemplateCategory","requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["name"],"properties":{"name":{"description":"Nazwa kategorii (max 128 znaków)","type":"string","example":"Oferty"},"parentId":{"description":"Kategoria nadrzędna (GET /v2/mail/template/categories); pominięte albo null = najwyższy poziom","type":"integer","example":12345,"nullable":true},"priority":{"description":"Priorytet na liście (wyższy = wyżej)","type":"integer","default":0}},"type":"object"}}}},"responses":{"201":{"description":"Kategoria utworzona","content":{"application/json":{"schema":{"properties":{"data":{"properties":{"id":{"type":"integer","example":12346},"name":{"type":"string","example":"Oferty"},"parentId":{"type":"integer","example":12345,"nullable":true},"priority":{"type":"integer","example":0}},"type":"object"}},"type":"object"}}}},"422":{"description":"Błędy walidacji (brak name, nieistniejący parentId)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień administratora szablonów webmaila","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/mail/template/categories/{id}":{"put":{"tags":["Mail"],"summary":"Edycja kategorii szablonów maili","description":"Edycja kategorii szablonów maili - zmieniane są TYLKO pola podane w body.\n`parentId: null` przenosi kategorię na najwyższy poziom; `parentId` nie może\nwskazywać samej kategorii ani jej potomka (powstałby cykl rodziców).\n\nWymaga uprawnienia administratora szablonów webmaila (jak panel CRM).\nDostępne od wersji API 2.4.0.","operationId":"updateMailTemplateCategory","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":12346}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"properties":{"name":{"description":"Nowa nazwa (max 128 znaków)","type":"string","example":"Oferty handlowe"},"parentId":{"description":"Nowa kategoria nadrzędna; null = najwyższy poziom","type":"integer","example":null,"nullable":true},"priority":{"description":"Priorytet na liście (wyższy = wyżej)","type":"integer"}},"type":"object"}}}},"responses":{"200":{"description":"Kategoria po zmianie","content":{"application/json":{"schema":{"properties":{"data":{"properties":{"id":{"type":"integer","example":12346},"name":{"type":"string","example":"Oferty handlowe"},"parentId":{"type":"integer","example":null,"nullable":true},"priority":{"type":"integer","example":0}},"type":"object"}},"type":"object"}}}},"404":{"description":"Brak kategorii o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Błędy walidacji (puste body, nieistniejący parentId, cykl rodziców)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień administratora szablonów webmaila","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/mail/templates/{id}/attachments":{"get":{"tags":["Mail"],"summary":"Załączniki szablonu maila","description":"Pliki dopięte do szablonu maila. Przy wysyłce z `templateId` CRM dokłada\nje do wiadomości sam - w odpowiedzi POST /v2/mail/send wymienione są tylko\nzałączniki z requestu, te szablonowe idą obok.\n\n`downloadUrl` to podpisany link ważny 1 minutę; `id` to identyfikator\nzałącznika w szablonie (suma kontrolna treści) - nim się go kasuje.\nDostępne od wersji API 2.7.0.","operationId":"listMailTemplateAttachments","parameters":[{"name":"id","in":"path","description":"Id szablonu (GET /v2/mail/templates)","required":true,"schema":{"type":"integer","example":12}}],"responses":{"200":{"description":"Załączniki szablonu; szablon bez plików = pusta lista","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/MailTemplateAttachment"}}},"type":"object"}}}},"404":{"description":"Brak szablonu o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"post":{"tags":["Mail"],"summary":"Dopięcie pliku do szablonu maila (multipart)","description":"Dopina plik do szablonu maila - request **multipart/form-data** z plikiem\nw polu `file` (limit 10 MB, twardy limit CRM dla szablonów). Szablon\nzakłada się wcześniej (POST /v2/mail/templates), pliki dokłada się tym\nwywołaniem, po jednym.\n\nOd tej chwili każda wysyłka z tym `templateId` niesie ten plik.\n\nTen sam plik pod tą samą nazwą CRM odrzuca (422 `error.attachmentExist`) -\nrozpoznaje go po sumie kontrolnej treści. Dostępne od wersji API 2.7.0.","operationId":"uploadMailTemplateAttachment","parameters":[{"name":"id","in":"path","description":"Id szablonu (GET /v2/mail/templates)","required":true,"schema":{"type":"integer","example":12}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"required":["file"],"properties":{"file":{"description":"Plik do dopięcia (maksymalnie 10 MB)","type":"string","format":"binary"}},"type":"object"}}}},"responses":{"201":{"description":"Plik dopięty do szablonu","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/MailTemplateAttachment"}},"type":"object"}}}},"404":{"description":"Brak szablonu o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Brak pliku, plik za duży albo już dopięty do szablonu","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/mail/templates/{id}/attachments/{attachmentId}":{"delete":{"tags":["Mail"],"summary":"Usunięcie załącznika szablonu maila","description":"Odpina plik od szablonu i kasuje go z przestrzeni plików instancji.\nWysłane wcześniej wiadomości zostają bez zmian - dotyczy tylko kolejnych\nwysyłek z tym szablonem. Dostępne od wersji API 2.7.0.","operationId":"deleteMailTemplateAttachment","parameters":[{"name":"id","in":"path","description":"Id szablonu (GET /v2/mail/templates)","required":true,"schema":{"type":"integer","example":12}},{"name":"attachmentId","in":"path","description":"Id załącznika z GET /v2/mail/templates/{id}/attachments","required":true,"schema":{"type":"string","example":"b783a276c5e20580e5a6b7079dcbc219"}}],"responses":{"204":{"description":"Załącznik odpięty"},"404":{"description":"Brak szablonu albo załącznika o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/modules":{"get":{"tags":["System"],"summary":"Moduły aktywne na tej instancji","description":"Moduły tego workspace'u wraz z informacją, czy są aktywne. Zapisy do encji\nz nieaktywnego modułu kończą się `403 module.notActive` - tutaj sprawdzisz to\nz wyprzedzeniem, zamiast dowiadywać się przy pierwszym żądaniu.\n\n`active: false` oznacza, że klient modułu nie ma w planie ALBO ma go wyłączonego\nw Ustawieniach - z punktu widzenia integracji to ta sama sytuacja: ta część\nAPI nie zadziała.\n\n`accessUntil` to data, do której klient ma dostęp do modułu (jeśli plan ją\nokreśla; `null` = bezterminowo w ramach planu).\n\nMapowanie na endpointy: `tickets` → /v2/tickets, `leads` → /v2/leads,\n`pipeline` → /v2/pipeline/*, `task` → /v2/tasks, `project` → /v2/projects,\n`dms` → /v2/contractors/{id}/dms/*, `documents` → generator dokumentów,\n`webmail` → /v2/mail/*.","operationId":"modules","responses":{"200":{"description":"Lista modułów","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"properties":{"id":{"description":"Identyfikator modułu (bez prefiksu `_module.`)","type":"string","example":"tickets"},"name":{"type":"string","example":"Zgłoszenia"},"active":{"type":"boolean","example":false},"accessUntil":{"type":"string","format":"date","example":"2099-12-31","nullable":true}},"type":"object"}}},"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/note/templates":{"post":{"tags":["Notes"],"summary":"Nowy szablon notatki kontrahenta","description":"Zakłada szablon notatki kontrahenta tą samą ścieżką co panel Tillio\n(walidacje CRM, normalizacja i unikalność aliasu). Z szablonu użytkownik\nCRM tworzy notatkę jednym kliknięciem albo aliasem w edytorze.\n\n- `noteTypeId` - aktywny typ z GET /v2/note/types (wymagane); z tym typem\n  powstają notatki tworzone z szablonu.\n- `title` - tytuł notatki z szablonu, 3-180 znaków po odcięciu spacji.\n- `body` - treść notatki, surowy HTML (ląduje w edytorze WYSIWYG CRM).\n- `alias` - skrót do wywołania szablonu w edytorze (max 31 znaków); CRM\n  zapisuje go z prefiksem `!`, małymi literami i `_` zamiast spacji -\n  w tej postaci wraca w odpowiedzi. Musi być unikalny.\n- `categoryId` - kategoria w drzewku szablonów (konfiguracja CRM);\n  bez niej szablon ląduje na poziomie głównym.\n- `acl` - ograniczenie widoczności ({userIds, departmentIds, groupIds});\n  bez niego szablon widzą wszyscy z dostępem do typu notatki.\n\nWymaga uprawnienia `note.templateAdmin` użytkownika przypisanego\ndo klucza API - bez niego 403.\n\nDostępne od wersji API 2.4.0.","operationId":"createNoteTemplate","requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["noteTypeId","name","title"],"properties":{"categoryId":{"description":"Kategoria szablonu (drzewko w konfiguracji CRM); brak = poziom główny","type":"integer","example":3},"noteTypeId":{"description":"Typ notatki - aktywna pozycja z GET /v2/note/types","type":"integer","example":1},"name":{"description":"Nazwa szablonu widoczna na liście wyboru","type":"string","example":"Protokół rozmowy serwisowej"},"alias":{"description":"Skrót do wywołania w edytorze notatki (max 31 znaków, unikalny); prefiks `!` można pominąć","type":"string","example":"protokol"},"title":{"description":"Tytuł notatki tworzonej z szablonu (3-180 znaków)","type":"string","example":"Protokół rozmowy serwisowej"},"body":{"description":"Treść notatki - surowy HTML (WYSIWYG w CRM)","type":"string","example":"<p>Ustalenia z klientem: ...</p>"},"acl":{"description":"Ograniczenie widoczności szablonu; brak/pusty obiekt = widoczny dla wszystkich","properties":{"userIds":{"type":"array","items":{"type":"integer"},"example":[12345]},"departmentIds":{"type":"array","items":{"type":"integer"},"example":[]},"groupIds":{"type":"array","items":{"type":"integer"},"example":[]}},"type":"object"},"priority":{"description":"Priorytet na liście szablonów (wyższy = wyżej)","type":"integer","default":0},"active":{"type":"boolean","default":true}},"type":"object"}}}},"responses":{"201":{"description":"Szablon utworzony","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/NoteTemplate"},"info":{"$ref":"#/components/schemas/WriteInfo"}},"type":"object"}}}},"422":{"description":"Błędy walidacji - lista {field, code, message}","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnienia note.templateAdmin","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/note/template/categories":{"get":{"tags":["Notes"],"summary":"Kategorie szablonów notatek","description":"Słownik kategorii szablonów notatek - mapuje `categoryId`\nz POST /v2/note/templates. Lista jest PŁASKA: drzewo składa się\npo `parentId` (null = poziom główny).\n\nKolejność pozycji jak w panelu CRM: priorytet malejąco, potem\nnazwa rosnąco.\n\nDostępne od wersji API 2.4.0.","operationId":"listNoteTemplateCategories","responses":{"200":{"description":"Płaska lista kategorii (id, name, parentId, priority)","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/NoteTemplateCategory"}}},"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"post":{"tags":["Notes"],"summary":"Nowa kategoria szablonów notatek","description":"Nowa kategoria szablonów notatek tą samą ścieżką co panel Tillio.\nBez `parentId` kategoria ląduje na poziomie głównym drzewka;\nz `parentId` staje się podkategorią wskazanej pozycji.\n\nWymaga uprawnienia `note.templateAdmin` użytkownika przypisanego\ndo klucza API - bez niego 403.\n\nDostępne od wersji API 2.4.0.","operationId":"createNoteTemplateCategory","requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["name"],"properties":{"name":{"description":"Nazwa kategorii (max 128 znaków)","type":"string","example":"Serwis"},"parentId":{"description":"Kategoria nadrzędna z GET /v2/note/template/categories; brak/null = poziom główny","type":"integer","example":3,"nullable":true},"priority":{"description":"Priorytet na liście (wyższy = wyżej)","type":"integer","default":0}},"type":"object"}}}},"responses":{"201":{"description":"Kategoria utworzona","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/NoteTemplateCategory"},"info":{"$ref":"#/components/schemas/WriteInfo"}},"type":"object"}}}},"422":{"description":"Błędy walidacji - lista {field, code, message}","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnienia note.templateAdmin","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/note/template/categories/{id}":{"put":{"tags":["Notes"],"summary":"Aktualizacja kategorii szablonów notatek","description":"Częściowa aktualizacja kategorii szablonów notatek - wysyłasz tylko\npola do zmiany (name, parentId, priority). `parentId: null` przenosi\nkategorię na poziom główny; rodzicem nie może być sama kategoria ani\njej potomek (cykl w drzewku - 422).\n\nWymaga uprawnienia `note.templateAdmin` użytkownika przypisanego\ndo klucza API - bez niego 403.\n\nDostępne od wersji API 2.4.0.","operationId":"updateNoteTemplateCategory","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":7}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"properties":{"name":{"description":"Nazwa kategorii (max 128 znaków)","type":"string","example":"Serwis i reklamacje"},"parentId":{"description":"Nowy rodzic; null = poziom główny","type":"integer","example":3,"nullable":true},"priority":{"type":"integer","example":5}},"type":"object"}}}},"responses":{"200":{"description":"Kategoria po aktualizacji","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/NoteTemplateCategory"},"info":{"$ref":"#/components/schemas/WriteInfo"}},"type":"object"}}}},"404":{"description":"Brak kategorii o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Błędy walidacji - lista {field, code, message}","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnienia note.templateAdmin","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/notes":{"get":{"tags":["Notes"],"summary":"Lista notatek z filtrowaniem","description":"**Główny use-case - audit trail integracji:** wszystkie notatki kontrahenta\njednym zapytaniem: `?contractorId=121`. Integracja (ERP, automatyzacja), która\ndopisuje notatki przy zdarzeniach synchronizacji, może tu odczytać i zweryfikować\npełną historię.\n\nPowiązanie notatki jest polimorficzne: notatka należy do kontrahenta (`contractorId`)\nALBO leada (`leadId`); może dodatkowo wskazywać usługę (`serviceId`) i proces\nsprzedażowy (`pipelineId`). Każde z tych powiązań filtruje dokładnie.\nNotatki przypięte do osoby: `?contactId=3921` (pole `contactIds` w rekordzie).\nNotatki leada: `?leadId=659` (zapis: `POST /v2/leads/{leadId}/notes`).\n\n**Przyrost:** CRM nie przechowuje daty modyfikacji notatki - brak `updatedAt`\ni `updatedAfter`; synchronizację przyrostową oprzyj o `createdAfter`\n(notatki w praktyce nie są edytowane po utworzeniu, tworzą dziennik zdarzeń).\n\nPrzykład - nowe notatki kontrahenta od ostatniego syncu:\n```\nGET /v2/notes?contractorId=121&createdAfter=2026-08-01T00:00:00Z\n```","operationId":"listNotes","parameters":[{"name":"contractorId","in":"query","description":"Notatki kontrahenta - główny filtr (audit trail integracji).","required":false,"schema":{"type":"integer","example":121}},{"name":"contactId","in":"query","description":"Notatki przypięte do kontaktu (GET /v2/contacts) - dokładne dopasowanie po powiązaniu notatka-kontakt, niezależnie od tego, czy notatka wisi na kontrahencie, leadzie czy tylko na kontakcie. Na instalacji CRM bez tej funkcji: 501 `feature.notSupportedByCrmVersion`. Dostępne od wersji API 2.10.0.","required":false,"schema":{"type":"integer","example":3921}},{"name":"noteTypeId","in":"query","description":"Typ notatki (słownik w GET /v2/note/types) - dokładne dopasowanie. Analogicznie dokładne: leadId, serviceId, pipelineId, creatorUserId, pinned (0/1).","required":false,"schema":{"type":"integer"}},{"name":"title","in":"query","description":"Filtr częściowy (zawiera). Analogicznie: body.","required":false,"schema":{"type":"string"}},{"name":"createdAfter","in":"query","description":"Tylko notatki utworzone po tej chwili (ISO 8601). Fundament syncu przyrostowego - encja nie ma daty modyfikacji.","required":false,"schema":{"type":"string","format":"date-time","example":"2026-08-01T00:00:00Z"}},{"name":"createdBefore","in":"query","description":"Tylko notatki utworzone do tej chwili (ISO 8601, włącznie).","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"customField","in":"query","description":"Filtry pól niestandardowych notatek: `customField[<klucz>]=<wartość>` (dokładne; klucze w GET /v2/note/custom-fields). Pusta wartość = pole nieustawione.","required":false,"style":"deepObject","explode":true,"schema":{"type":"object","additionalProperties":{"type":"string"}}},{"name":"id","in":"query","required":false,"description":"Filtr po polu id - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"leadId","in":"query","required":false,"description":"Filtr po polu leadId - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"serviceId","in":"query","required":false,"description":"Filtr po polu serviceId - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"pipelineId","in":"query","required":false,"description":"Filtr po polu pipelineId - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"body","in":"query","required":false,"description":"Filtr po polu body - dopasowanie częściowe (zawiera).","schema":{"type":"string"}},{"name":"pinned","in":"query","required":false,"description":"Filtr po polu pinned - dopasowanie dokładne.","schema":{"type":"boolean"}},{"name":"creatorUserId","in":"query","required":false,"description":"Filtr po polu creatorUserId - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"sort","in":"query","description":"Pole sortowania: id, noteDate, createdAt.","required":false,"schema":{"type":"string","default":"createdAt"}},{"name":"sortDir","in":"query","required":false,"schema":{"type":"string","default":"asc","enum":["asc","desc"]}},{"name":"page","in":"query","description":"Strona wyników, od 1.","required":false,"schema":{"type":"integer","default":1,"maximum":1000000,"minimum":1}},{"name":"limit","in":"query","description":"Rozmiar strony (max 1000).","required":false,"schema":{"type":"integer","default":100,"maximum":1000,"minimum":1}}],"responses":{"200":{"description":"Lista notatek + metadane stronicowania","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Note"}},"pagination":{"$ref":"#/components/schemas/Pagination"}},"type":"object"}}}},"400":{"description":"Błąd walidacji zapytania - lista błędów w kontrakcie {field, code, message}","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"501":{"description":"Filtr contactId na instalacji CRM bez kontaktów przy notatce","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/notes/{id}":{"get":{"tags":["Notes"],"summary":"Pojedyncza notatka po id","description":"Pełne dane notatki wraz z polami niestandardowymi. Wystarczy id notatki, bez znajomości kontrahenta.","operationId":"getNote","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":2}}],"responses":{"200":{"description":"Notatka","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/Note"}},"type":"object"}}}},"404":{"description":"Brak notatki o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"put":{"tags":["Notes"],"summary":"Aktualizacja notatki (partial)","description":"Częściowa aktualizacja notatki - wysyłasz TYLKO pola do zmiany\n(`title`, `body`, `pinned`, `noteDate`, `customField`), reszta zostaje\nnietknięta; nie trzeba wcześniej pobierać rekordu. Ta sama ścieżka\nzapisu co edycja w aplikacji Tillio.\n\n`noteTypeId` i powiązania (kontrahent/lead/usługa/pipeline) nie podlegają\nedycji przez API - 422 `body.fieldNotUpdatable`, spójnie z kontraktem\nedycji CRM.","operationId":"updateNote","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":2}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"properties":{"title":{"type":"string"},"body":{"description":"Treść - surowy HTML","type":"string"},"pinned":{"type":"boolean"},"noteDate":{"type":"string","format":"date-time"},"customField":{"description":"Pola wielowartościowe (multiselect, produkty, szanse sprzedaży) przyjmują listę wartości, np. [\"12\",\"15\"] - pojedyncza wartość jest równoważna liście jednoelementowej. Lista dostępna od wersji API 2.9.0.","type":"object","additionalProperties":{"nullable":true}}},"type":"object"}}}},"responses":{"200":{"description":"Notatka po aktualizacji","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/Note"},"info":{"$ref":"#/components/schemas/WriteInfo"}},"type":"object"}}}},"404":{"description":"Brak notatki o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Błędy walidacji - lista {field, code, message}","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień do notatek","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/contractors/{contractorId}/notes":{"post":{"tags":["Notes"],"summary":"Nowa notatka na kontrahencie","description":"Tworzy notatkę na kontrahencie tą samą ścieżką zapisu, której używa\naplikacja Tillio (walidacje, historia, automatyzacje CRM). Typowy\nuse-case integracji: log zdarzeń synchronizacji / decyzji po stronie ERP.\n\n- `noteTypeId` - aktywny typ z GET /v2/note/types (wymagane).\n- `body` - surowy HTML (treść ląduje w edytorze WYSIWYG CRM).\n- `creatorUserId` - opcjonalne; bez niego autorem zostaje użytkownik\n  przypisany do klucza API.\n- `noteDate` - opcjonalna data notatki widoczna w CRM (antydatowanie,\n  np. przy migracji historii); bez niej = chwila utworzenia.\n- `customField` - obiekt klucz→wartość (klucze w GET /v2/note/custom-fields).\n- `contactIds` - kontakty przypinane od razu przy tworzeniu (osoby, których\n  dotyczy rozmowa czy spotkanie); tylko kontakty powiązane z tym kontrahentem.\n  Dostępne od wersji API 2.10.0; na starszym CRM 501.\n\nTo samo body przyjmują pozostałe kotwice notatki: pod kontaktem\n`POST /v2/contacts/{contactId}/notes` (2.10.0) i pod leadem\n`POST /v2/leads/{leadId}/notes` (2.13.0).","operationId":"createContractorNote","parameters":[{"name":"contractorId","in":"path","required":true,"schema":{"type":"integer","example":121}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["noteTypeId","title"],"properties":{"createdAt":{"description":"IMPORT DANYCH: data utworzenia rekordu (ISO 8601, strefa instancji). Pozwala zachować chronologię przy wciąganiu historii sprzed lat - bez niej rekord dostaje datę importu. Tylko przy tworzeniu: przy aktualizacji zwracamy 422 (pomyłkę w imporcie poprawia się kasując rekord i importując ponownie). Data z przyszłości = 422. Dostępne od wersji API 1.16.0.","type":"string","format":"date-time","example":"2023-05-17T09:30:00"},"creatorUserId":{"description":"Autor notatki (GET /v2/users); brak = użytkownik przypisany do klucza API. Zapis wykonuje się w kontekście tej osoby, więc obowiązują JEJ uprawnienia.","type":"integer","example":42},"noteTypeId":{"description":"Typ notatki - aktywna pozycja z GET /v2/note/types","type":"integer","example":1},"title":{"type":"string","example":"Synchronizacja z Optima - utworzono kartotekę"},"body":{"description":"Treść - surowy HTML (WYSIWYG w CRM)","type":"string","example":"<p>Kartoteka <b>OPT-8123</b> założona automatycznie.</p>"},"pinned":{"type":"boolean","default":false},"noteDate":{"description":"Data notatki widoczna w CRM (ISO 8601) - antydatowanie przy migracjach","type":"string","format":"date-time"},"customField":{"description":"Pola niestandardowe notatki (GET /v2/note/custom-fields). Pola wielowartościowe (multiselect, produkty, szanse sprzedaży) przyjmują listę wartości, np. [\"12\",\"15\"] - pojedyncza wartość jest równoważna liście jednoelementowej. Lista dostępna od wersji API 2.9.0.","type":"object","additionalProperties":{"nullable":true}},"contactIds":{"description":"Kontakty przypinane do notatki przy tworzeniu (id z GET /v2/contacts?contractorId=...). Każdy musi być powiązany z tym kontrahentem - inaczej 422 z polem contactIds. Dostępne od wersji API 2.10.0.","type":"array","items":{"type":"integer"},"example":[3921]}},"type":"object"}}}},"responses":{"201":{"description":"Notatka utworzona","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/Note"},"info":{"$ref":"#/components/schemas/WriteInfo"}},"type":"object"}}}},"404":{"description":"Brak kontrahenta o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Błędy walidacji - lista {field, code, message}","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień do notatek (albo do kontaktów przy contactIds)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"501":{"description":"contactIds na instalacji CRM bez kontaktów przy notatce","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/contacts/{contactId}/notes":{"post":{"tags":["Notes"],"summary":"Nowa notatka pod kontaktem","description":"Tworzy notatkę pod kontaktem (osobą) - np. po rozmowie telefonicznej z osobą,\nktóra nie ma jeszcze firmy w CRM. Body jak w `POST /v2/contractors/{id}/notes`,\nplus:\n\n- `contractorId` - opcjonalny; jeden z kontrahentów kontaktu (`contractorIds`\n  w GET /v2/contacts/{id}). Z nim notatka wisi na kontrahencie i jest przypięta\n  do kontaktu - dokładnie jak notatka kontrahenta z `contactIds`.\n- bez `contractorId` notatka trafia na **kontrahenta głównego kontaktu**\n  (pierwszy z `contractorIds` w GET /v2/contacts/{id}) - widać ją wtedy i na\n  karcie osoby, i na karcie firmy. Kontakt bez żadnego kontrahenta: notatka\n  wisi **wyłącznie na kontakcie** (`contractorId` i `leadId` w odpowiedzi\n  `null`), co jest właściwym zachowaniem dla rozmowy z kimś, kogo firmy nie ma\n  jeszcze w CRM. `serviceId` i `pipelineItemId` wymagają kontrahenta (422).\n- `contactIds` - dodatkowe kontakty do przypięcia obok tego z path.\n\n**Wymaga nowszego CRM** (501 `feature.notSupportedByCrmVersion` na starszym).\nDostępne od wersji API 2.10.0.","operationId":"createContactNote","parameters":[{"name":"contactId","in":"path","description":"Id kontaktu (GET /v2/contacts)","required":true,"schema":{"type":"integer","example":3921}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["noteTypeId","title"],"properties":{"contractorId":{"description":"Kontrahent, na którym ma wisieć notatka - jeden z kontrahentów kontaktu. Brak = kontrahent główny kontaktu (a gdy kontakt nie ma żadnego, notatka tylko pod kontaktem).","type":"integer","example":121,"nullable":true},"contactIds":{"description":"Dodatkowe kontakty do przypięcia (poza kontaktem z path); przy contractorId - tylko jego kontakty.","type":"array","items":{"type":"integer"}},"createdAt":{"description":"IMPORT DANYCH: data utworzenia rekordu (ISO 8601, strefa instancji) - jak w POST /v2/contractors/{id}/notes.","type":"string","format":"date-time"},"creatorUserId":{"description":"Autor notatki (GET /v2/users); brak = użytkownik przypisany do klucza API.","type":"integer"},"noteTypeId":{"description":"Typ notatki - aktywna pozycja z GET /v2/note/types","type":"integer","example":1},"title":{"type":"string","example":"Rozmowa telefoniczna - pytanie o ofertę"},"body":{"description":"Treść - HTML (WYSIWYG w CRM)","type":"string","example":"<p>Osoba prosi o kontakt w przyszłym tygodniu.</p>"},"pinned":{"type":"boolean","default":false},"noteDate":{"description":"Data notatki widoczna w CRM (ISO 8601)","type":"string","format":"date-time"},"serviceId":{"description":"Usługa kontrahenta - tylko razem z contractorId","type":"integer"},"pipelineItemId":{"description":"Szansa sprzedaży kontrahenta - tylko razem z contractorId","type":"integer"},"customField":{"description":"Pola niestandardowe notatki (GET /v2/note/custom-fields)","type":"object","additionalProperties":{"nullable":true}}},"type":"object"}}}},"responses":{"201":{"description":"Notatka utworzona","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/Note"},"info":{"$ref":"#/components/schemas/WriteInfo"}},"type":"object"}}}},"404":{"description":"Brak kontaktu o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Błędy walidacji - m.in. contractorId niepowiązany z kontaktem, serviceId/pipelineItemId bez kontrahenta","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień do notatek albo kontaktów","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"501":{"description":"Wersja CRM tej instalacji nie obsługuje kontaktów przy notatce","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/leads/{leadId}/notes":{"post":{"tags":["Notes"],"summary":"Nowa notatka pod leadem","description":"Tworzy notatkę pod leadem tą samą ścieżką zapisu, której używa aplikacja\nTillio na karcie leada (walidacje, historia, automatyzacje, odświeżenie\nostatniej aktywności leada - `lastActivityAt` w GET /v2/leads/{id}).\nBody jak w `POST /v2/contractors/{id}/notes` (noteTypeId, title, body, pinned,\nnoteDate, customField, createdAt, creatorUserId), z różnicami:\n\n- notatka należy do leada: w odpowiedzi `leadId` = lead z path, `contractorId`\n  = null - także wtedy, gdy lead ma już przypisanego kontrahenta. Przy\n  konwersji leada CRM sam przepina jego notatki na kontrahenta i szansę\n  sprzedaży, więc niczego nie trzeba dopisywać drugi raz;\n- `contactIds` - nieobsługiwane (422 z polem `contactIds`): kontakty przypina\n  się do notatek kontrahenta i kontaktu;\n- `serviceId` i `pipelineItemId` - nieobsługiwane (422 z tym polem): usługa\n  i szansa należą do kontrahenta, lead ich nie ma.\n\nWymaga uprawnień do notatek i do edycji leadów (jak `PUT /v2/leads/{id}`).\nOdczyt: `GET /v2/notes?leadId=...` albo `GET /v2/notes/{id}`; edycja:\n`PUT /v2/notes/{id}`. Dostępne od wersji API 2.13.0.","operationId":"createLeadNote","parameters":[{"name":"leadId","in":"path","description":"Id leada (GET /v2/leads)","required":true,"schema":{"type":"integer","example":659}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["noteTypeId","title"],"properties":{"createdAt":{"description":"IMPORT DANYCH: data utworzenia rekordu (ISO 8601, strefa instancji) - jak w POST /v2/contractors/{id}/notes.","type":"string","format":"date-time"},"creatorUserId":{"description":"Autor notatki (GET /v2/users); brak = użytkownik przypisany do klucza API. Zapis wykonuje się w kontekście tej osoby, więc obowiązują JEJ uprawnienia.","type":"integer"},"noteTypeId":{"description":"Typ notatki - aktywna pozycja z GET /v2/note/types","type":"integer","example":1},"title":{"type":"string","example":"Rozmowa kwalifikacyjna - zainteresowanie ofertą"},"body":{"description":"Treść - HTML (WYSIWYG w CRM)","type":"string","example":"<p>Lead prosi o wycenę na 10 stanowisk.</p>"},"pinned":{"type":"boolean","default":false},"noteDate":{"description":"Data notatki widoczna w CRM (ISO 8601) - antydatowanie przy migracjach","type":"string","format":"date-time"},"customField":{"description":"Pola niestandardowe notatki (GET /v2/note/custom-fields)","type":"object","additionalProperties":{"nullable":true}}},"type":"object"}}}},"responses":{"201":{"description":"Notatka utworzona (leadId = lead z path, contractorId = null)","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/Note"},"info":{"$ref":"#/components/schemas/WriteInfo"}},"type":"object"}}}},"404":{"description":"Brak leada o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Błędy walidacji - m.in. contactIds, serviceId, pipelineItemId na notatce leada","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień do notatek albo do edycji leadów","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/notes/{id}/attachments":{"get":{"tags":["Notes"],"summary":"Załączniki notatki z linkami do pobrania","description":"Załączniki notatki, chronologicznie - metadane plików plus tymczasowy\nlink do pobrania (`downloadUrl`, podpisany URL GCS ważny 1 minutę -\npobierz plik od razu po odczycie listy;\nnull = generowanie niedostępne w tym środowisku). Bez paginacji -\nzałączników per notatka jest mało.","operationId":"listNoteAttachments","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":2}}],"responses":{"200":{"description":"Załączniki notatki; brak = pusta lista","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/NoteAttachment"}}},"type":"object"}}}},"404":{"description":"Brak notatki o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"post":{"tags":["Notes"],"summary":"Upload załącznika notatki (multipart)","description":"Dodaje załącznik do notatki (np. notatki kontrahenta) - request\n**multipart/form-data** z plikiem w polu `file` (limit produktowy\n128 MB; core dodatkowo waliduje rozmiar). Plik ląduje w prywatnej\nprzestrzeni plików instancji (GCS) - ta sama ścieżka co aplikacja\nTillio. Odpowiedź: rekord załącznika z gotowym `downloadUrl`.","operationId":"uploadNoteAttachment","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":2}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"required":["file"],"properties":{"file":{"description":"Plik do załączenia","type":"string","format":"binary"}},"type":"object"}}}},"responses":{"201":{"description":"Załącznik dodany","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/NoteAttachment"}},"type":"object"}}}},"404":{"description":"Brak notatki o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Brak pliku / plik odrzucony przez CRM","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/notes/{id}/contacts":{"get":{"tags":["Notes"],"summary":"Kontakty przypięte do notatki","description":"Kontakty przypięte do notatki. Notatka wisi na kontrahencie albo leadzie,\na kontakty dochodzą **obok** - jedna notatka może mieć ich wiele (ustalenia\nze spotkania dotyczą kilku osób), tak samo jak w panelu.\n\n**Wymaga nowszego CRM.** Instalacja, która tej funkcji nie ma, dostaje 501\n`feature.notSupportedByCrmVersion` - reszta końcówek notatek działa normalnie.\nDostępne od wersji API 2.8.0.","operationId":"listNoteContacts","parameters":[{"name":"id","in":"path","description":"Id notatki (GET /v2/notes)","required":true,"schema":{"type":"integer","example":18270}}],"responses":{"200":{"description":"Przypięte kontakty; notatka bez kontaktów = pusta lista","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/NoteContact"}}},"type":"object"}}}},"404":{"description":"Brak notatki o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"501":{"description":"Wersja CRM tej instalacji nie obsługuje przypinania kontaktów","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"post":{"tags":["Notes"],"summary":"Przypięcie kontaktu do notatki","description":"Przypina kontakt do notatki. Powtórzenie tego samego przypięcia niczego nie\npsuje i nie tworzy duplikatu - odpowiedź zawsze niesie pełną, aktualną listę\nkontaktów notatki.\n\nNotatek mailowych CRM tą drogą nie zmienia - tam kontakty wynikają z adresów\nwiadomości (odpowiedź: 403).\n\n**Wymaga nowszego CRM** (501 `feature.notSupportedByCrmVersion` na starszym).\nDostępne od wersji API 2.8.0.","operationId":"pinNoteContact","parameters":[{"name":"id","in":"path","description":"Id notatki (GET /v2/notes)","required":true,"schema":{"type":"integer","example":18270}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["contactId"],"properties":{"contactId":{"description":"Id kontaktu (GET /v2/contacts)","type":"integer","example":3921}},"type":"object"}}}},"responses":{"201":{"description":"Kontakt przypięty - pełna lista po zmianie","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/NoteContact"}}},"type":"object"}}}},"404":{"description":"Brak notatki albo kontaktu o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Brak contactId albo odmowa CRM","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Notatka mailowa albo brak uprawnień do kontaktów","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"501":{"description":"Wersja CRM tej instalacji nie obsługuje przypinania kontaktów","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/notes/{id}/contacts/{contactId}":{"delete":{"tags":["Notes"],"summary":"Odpięcie kontaktu od notatki","description":"Odpina kontakt od notatki.\n\n**Uwaga - CRM może przy tym skasować notatkę.** Jeżeli po odpięciu notatka nie\njest przypięta do niczego (ani kontrahent, ani lead, ani inny kontakt), CRM\nusuwa ją w całości - tak działa też panel. Odpowiedź niesie wtedy\n`noteDeleted: true`, żeby integrator wiedział, że rekordu już nie ma.\n\n**Wymaga nowszego CRM** (501 `feature.notSupportedByCrmVersion` na starszym).\nDostępne od wersji API 2.8.0.","operationId":"unpinNoteContact","parameters":[{"name":"id","in":"path","description":"Id notatki (GET /v2/notes)","required":true,"schema":{"type":"integer","example":18270}},{"name":"contactId","in":"path","description":"Id kontaktu do odpięcia","required":true,"schema":{"type":"integer","example":3921}}],"responses":{"200":{"description":"Kontakt odpięty","content":{"application/json":{"schema":{"properties":{"data":{"properties":{"noteDeleted":{"description":"true = CRM usunął notatkę, bo nie była już do niczego przypięta","type":"boolean","example":false}},"type":"object"}},"type":"object"}}}},"404":{"description":"Brak notatki albo ten kontakt nie jest do niej przypięty","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Notatka mailowa albo brak uprawnień do kontaktów","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"501":{"description":"Wersja CRM tej instalacji nie obsługuje przypinania kontaktów","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/orders":{"get":{"tags":["Orders"],"summary":"Lista zamówień z filtrowaniem i pozycjami na żądanie","description":"Nagłówki zamówień dla integracji (ERP, fakturowanie, synchronizacja) i aplikacji.\n\n**Synchronizacja przyrostowa:** przekaż `updatedAfter` z datą ostatniego udanego syncu.\nUwaga: CRM nie śledzi modyfikacji zamówień (`updatedAt` == `createdAt`), więc filtr\nłapie wyłącznie zamówienia NOWO utworzone po tej chwili - edycje istniejących\nzamówień nie są wykrywalne przyrostowo.\n\n**Matchowanie rekordów:** `number` (pełny numer dokumentu, np. `PROF/CRM/20/08/2026`)\ni `foreignNumber` (numer sekwencyjny) filtrują dokładnie - pewne klucze dla ERP.\nZamówienia konkretnego kontrahenta: `contractorId=950102`.\n\n**Pozycje:** `include=products` dokleja listę pozycji do każdego zamówienia\n(jedno zapytanie batch - bez kary za rozmiar strony).\n\nPrzykład - nowe zamówienia od ostatniego syncu, z pozycjami, dla faktur w Optimie:\n```\nGET /v2/orders?updatedAfter=2026-08-01T00:00:00Z&include=products\n```","operationId":"listOrders","parameters":[{"name":"contractorId","in":"query","description":"Zamówienia jednego kontrahenta (dokładne).","required":false,"schema":{"type":"integer","example":950102}},{"name":"orderStatusId","in":"query","description":"Status zamówienia (dokładne) - słownik w GET /v2/order/statuses.","required":false,"schema":{"type":"integer"}},{"name":"number","in":"query","description":"Pełny numer dokumentu (dokładne dopasowanie).","required":false,"schema":{"type":"string","example":"PROF/CRM/20/08/2026"}},{"name":"foreignNumber","in":"query","description":"Numer sekwencyjny nadawany przy zapisie (dokładne).","required":false,"schema":{"type":"integer","example":20}},{"name":"updatedAfter","in":"query","description":"Tylko rekordy utworzone PO tej chwili (ISO 8601). Dla zamówień równoważne createdAfter - CRM nie śledzi ich modyfikacji.","required":false,"schema":{"type":"string","format":"date-time","example":"2026-08-01T00:00:00Z"}},{"name":"updatedBefore","in":"query","description":"Górna granica zakresu (ISO 8601, włącznie).","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"createdAfter","in":"query","description":"Tylko rekordy utworzone po tej chwili (ISO 8601).","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"createdBefore","in":"query","description":"Tylko rekordy utworzone do tej chwili (ISO 8601, włącznie).","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"customField","in":"query","description":"Filtry pól niestandardowych: `customField[<klucz>]=<wartość>`. Zamówienia obecnie NIE mają zdefiniowanych pól niestandardowych - każdy klucz zwróci 400.","required":false,"style":"deepObject","explode":true,"schema":{"type":"object","additionalProperties":{"type":"string"}}},{"name":"id","in":"query","required":false,"description":"Filtr po polu id - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"sort","in":"query","description":"Pole sortowania: id, number, orderDate, createdAt, updatedAt.","required":false,"schema":{"type":"string","default":"updatedAt"}},{"name":"sortDir","in":"query","required":false,"schema":{"type":"string","default":"asc","enum":["asc","desc"]}},{"name":"page","in":"query","description":"Strona wyników, od 1.","required":false,"schema":{"type":"integer","default":1,"maximum":1000000,"minimum":1}},{"name":"limit","in":"query","description":"Rozmiar strony (max 1000).","required":false,"schema":{"type":"integer","default":100,"maximum":1000,"minimum":1}},{"name":"include","in":"query","description":"Dane powiązane, CSV. Dostępne: `products` (pozycje zamówień). `customField` jest zawsze w odpowiedzi.","required":false,"schema":{"type":"string","example":"products"}}],"responses":{"200":{"description":"Lista zamówień + metadane stronicowania","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Order"}},"pagination":{"$ref":"#/components/schemas/Pagination"}},"type":"object"}}}},"400":{"description":"Błąd walidacji zapytania - lista błędów w kontrakcie {field, code, message}","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/orders/{id}":{"get":{"tags":["Orders"],"summary":"Pojedyncze zamówienie po id, z pozycjami","description":"Pełne dane zamówienia - nagłówek i pozycje (`products` zawsze w odpowiedzi).","operationId":"getOrder","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":366}}],"responses":{"200":{"description":"Zamówienie z pozycjami","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/Order"}},"type":"object"}}}},"404":{"description":"Brak zamówienia o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"put":{"tags":["Orders"],"summary":"Aktualizacja zamówienia (nagłówek partial, products = pełny stan)","description":"Aktualizacja zamówienia. **Nagłówek częściowo** - wysyłasz tylko pola\ndo zmiany. **Pozycje inaczej:** pole `products` to PEŁNY stan pozycji\n(mechanizm CRM robi wymianę) - pozycje z `id` są aktualizowane, bez `id`\ndodawane, a istniejące pozycje NIEOBECNE w liście są USUWANE.\nBrak pola `products` w body = pozycje zostają nietknięte.\n`totalAmount` jest przeliczany z pozycji.\n\n**`currency` jest create-only** - pozycje mają ceny w walucie z utworzenia,\nCRM nie zmienia jej w edycji. Body z `currency` = 422 `body.fieldNotUpdatable`\nPRZED zapisem (pozostałe pola z tego body też nie są zapisywane). Do wersji API\n2.13.0 pole było przyjmowane z odpowiedzią 200 i starą walutą w rekordzie.","operationId":"updateOrder","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":366}}],"requestBody":{"description":"Pola nagłówka do zmiany + opcjonalnie products (pełny stan pozycji; pozycje mogą mieć id)","required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Zamówienie po aktualizacji","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/Order"},"info":{"$ref":"#/components/schemas/WriteInfo"}},"type":"object"}}}},"404":{"description":"Brak zamówienia o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Błędy walidacji","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień do zamówień","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/contractors/{contractorId}/orders":{"post":{"tags":["Orders"],"summary":"Nowe zamówienie na kontrahencie (nagłówek + pozycje)","description":"Tworzy zamówienie (nagłówek + pozycje) na kontrahencie tą samą ścieżką,\nktórej używa aplikacja Tillio: transakcja, przeliczenie `totalAmount`\nz pozycji (cena × ilość × rabat), aktualizacja statusu kontrahenta\ni przeliczenie jego danych wygenerowanych w tle.\n\nWymagana co najmniej jedna pozycja w `products`; każda musi wskazywać\nprodukt z katalogu (`productId` - GET /v2/products). `customName`\ndomyślnie = nazwa produktu z katalogu, `quantity` = 1.\n\n**Numeracja.** Pominięcie `number` = Tillio składa numer ze schematu\nnumeracji instancji (np. `PROF/CRM/[NR]/[MM]/[YYYY]`). Przy imporcie\nz systemu zewnętrznego (Optima, wFirma, sklep) podaj **własny `number`** -\nzostanie zapisany dosłownie, razem z formatem źródła. Po numerze da się\npotem odnaleźć rekord: `GET /v2/orders?number=ZAM/OPTIMA/2024/00123`,\nco daje integracji klucz do sprawdzenia, czy zamówienie już wjechało.\n`foreignNumber` to osobna rzecz - numer KOLEJNY w schemacie Tillio.","operationId":"createContractorOrder","parameters":[{"name":"contractorId","in":"path","required":true,"schema":{"type":"integer","example":121}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["products"],"properties":{"createdAt":{"description":"IMPORT DANYCH: data utworzenia rekordu (ISO 8601, strefa instancji). Pozwala zachować chronologię przy wciąganiu historii sprzed lat - bez niej rekord dostaje datę importu. Tylko przy tworzeniu: przy aktualizacji zwracamy 422 (pomyłkę w imporcie poprawia się kasując rekord i importując ponownie). Data z przyszłości = 422. Dostępne od wersji API 1.16.0.","type":"string","format":"date-time","example":"2023-05-17T09:30:00"},"creatorUserId":{"description":"IMPORT DANYCH: użytkownik, który ma figurować jako twórca rekordu (GET /v2/users). Brak = użytkownik przypisany do klucza API. Zapis wykonuje się w kontekście tej osoby, więc obowiązują JEJ uprawnienia. Dostępne od wersji API 1.16.0.","type":"integer","example":42},"orderStatusId":{"description":"Status z GET /v2/order/statuses","type":"integer","example":1},"note":{"type":"string"},"currency":{"description":"Waluta zamówienia (ISO 4217). TYLKO przy tworzeniu: pozycje mają ceny w tej walucie, więc CRM nie zmienia jej w edycji - `currency` w PUT /v2/orders/{id} = 422 body.fieldNotUpdatable.","type":"string","example":"PLN"},"place":{"description":"Miejsce wystawienia","type":"string","example":"Warszawa"},"orderDate":{"type":"string","format":"date","example":"2026-08-17"},"validUntil":{"type":"string","format":"date"},"deliveryDate":{"type":"string","format":"date"},"number":{"description":"Pełny numer dokumentu z systemu źródłowego (dostępny od wersji API 1.25.0). Pominięty = Tillio nada własny ze schematu numeracji instancji. Filtrowalny w GET /v2/orders.","type":"string","example":"ZAM/OPTIMA/2024/00123"},"foreignNumber":{"description":"Numer KOLEJNY w schemacie numeracji Tillio (podstawiany pod [NR]) - domyślnie nadawany automatycznie. To nie jest numer z systemu zewnętrznego, na ten jest `number`.","type":"integer"},"sentDate":{"description":"Data wysłania zamówienia do klienta (dostępna od wersji API 1.24.0)","type":"string","format":"date"},"products":{"type":"array","items":{"properties":{"productId":{"description":"Produkt z katalogu (GET /v2/products) - to ALBO productSku wymagane","type":"integer","example":235},"productSku":{"description":"Alternatywa dla productId: wskazanie produktu po SKU (nieznane SKU = 422 - produkt musi już istnieć w katalogu, patrz POST /v2/products)","type":"string","example":"TIL-PRO-1Y"},"customName":{"description":"Nazwa pozycji; default = nazwa z katalogu","type":"string"},"quantity":{"type":"string","default":"1","example":"2"},"price":{"description":"Cena jednostkowa","type":"string","example":"499.00"},"discount":{"description":"Rabat w % (0-100)","type":"string"},"taxRate":{"description":"Stawka VAT w %","type":"string","example":"23"},"measureId":{"description":"Jednostka miary; default z produktu","type":"integer"}},"type":"object"}}},"type":"object"}}}},"responses":{"201":{"description":"Zamówienie utworzone (z nadanym numerem)","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/Order"},"info":{"$ref":"#/components/schemas/WriteInfo"}},"type":"object"}}}},"404":{"description":"Brak kontrahenta o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Błędy walidacji - lista {field, code, message}","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień do zamówień","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/phone-calls":{"get":{"tags":["Phone calls"],"summary":"Lista połączeń telefonicznych","description":"Połączenia z tabeli połączeń CRM - zarówno zapisane przez API (`provider:\nTillioCalls`), jak i zsynchronizowane z integracji VoIP (Focus, Play, Ringostat).\nKartoteka kontrahenta pokazuje je w zakładce połączeń, raport VoIP liczy z tej\nsamej tabeli.\n\nTypowe zapytania:\n```\nGET /v2/phone-calls?contractorId=121&sort=startedAt&sortDir=desc\nGET /v2/phone-calls?userId=7&startedAfter=2026-09-01T00:00:00Z&status=missed\nGET /v2/phone-calls?source=tillio-calls&sourceId=call_01J8ZK3M9Q\n```\n\n**Zakres czasu:** `startedAfter` / `startedBefore` po dacie rozmowy. CRM nie\nprzechowuje daty modyfikacji połączenia - `updatedAfter` nie jest wspierane\n(400), a `createdAt` nie istnieje: datą rekordu jest `startedAt`.\n\nPola niestandardowe nie dotyczą połączeń (`customField` = 400).\nDostępne od wersji API 2.10.0.","operationId":"listPhoneCalls","parameters":[{"name":"contractorId","in":"query","description":"Rozmowy przypięte do kontrahenta (po przypięciach, nie po numerze) - główny filtr kartoteki.","required":false,"schema":{"type":"integer","example":121}},{"name":"contactId","in":"query","description":"Rozmowy z tą osobą kontaktową. Analogicznie dokładne: userId (właściciel linii), direction (inbound/outbound), status, provider, source, sourceId, ownNumber, remoteNumber (format międzynarodowy, np. +48601123456), recordingCallId.","required":false,"schema":{"type":"integer","example":3921}},{"name":"startedAfter","in":"query","description":"Tylko rozmowy rozpoczęte po tej chwili (ISO 8601). Klucz przyrostowego odczytu - encja nie ma daty modyfikacji.","required":false,"schema":{"type":"string","format":"date-time","example":"2026-09-01T00:00:00Z"}},{"name":"startedBefore","in":"query","description":"Tylko rozmowy rozpoczęte do tej chwili (ISO 8601, włącznie).","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"id","in":"query","required":false,"description":"Filtr po polu id - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"provider","in":"query","required":false,"description":"Filtr po polu provider - dopasowanie dokładne.","schema":{"type":"string"}},{"name":"source","in":"query","required":false,"description":"Filtr po polu source - dopasowanie dokładne.","schema":{"type":"string"}},{"name":"sourceId","in":"query","required":false,"description":"Filtr po polu sourceId - dopasowanie dokładne.","schema":{"type":"string"}},{"name":"direction","in":"query","required":false,"description":"Filtr po polu direction - dopasowanie dokładne.","schema":{"type":"string"}},{"name":"status","in":"query","required":false,"description":"Filtr po polu status - dopasowanie dokładne.","schema":{"type":"string"}},{"name":"ownNumber","in":"query","required":false,"description":"Filtr po polu ownNumber - dopasowanie dokładne.","schema":{"type":"string"}},{"name":"remoteNumber","in":"query","required":false,"description":"Filtr po polu remoteNumber - dopasowanie dokładne.","schema":{"type":"string"}},{"name":"userId","in":"query","required":false,"description":"Filtr po polu userId - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"recordingCallId","in":"query","required":false,"description":"Filtr po polu recordingCallId - dopasowanie dokładne.","schema":{"type":"string"}},{"name":"sort","in":"query","description":"Pole sortowania: id, startedAt, duration.","required":false,"schema":{"type":"string","default":"startedAt"}},{"name":"sortDir","in":"query","required":false,"schema":{"type":"string","default":"asc","enum":["asc","desc"]}},{"name":"page","in":"query","description":"Strona wyników, od 1.","required":false,"schema":{"type":"integer","default":1,"maximum":1000000,"minimum":1}},{"name":"limit","in":"query","description":"Rozmiar strony (max 1000).","required":false,"schema":{"type":"integer","default":100,"maximum":1000,"minimum":1}}],"responses":{"200":{"description":"Lista połączeń + metadane stronicowania","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/PhoneCall"}},"pagination":{"$ref":"#/components/schemas/Pagination"}},"type":"object"}}}},"400":{"description":"Błąd walidacji zapytania - lista błędów w kontrakcie {field, code, message}","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"post":{"tags":["Phone calls"],"summary":"Nowe połączenie (idempotentne po source + sourceId)","description":"Dopisuje rozmowę do CRM **dokładnie tak, jak robi to integracja VoIP** (Focus,\nPlay, Ringostat): rekord w tabeli połączeń, przypięcie do kontrahenta i notatka\n„Telefon\" na jego osi czasu - tą samą klasą CRM, której używa panel. Kartoteka\npokazuje rozmowę w zakładce połączeń, raport VoIP liczy ją raz.\n\n**Idempotencja:** para `source` + `sourceId` identyfikuje rozmowę. Powtórne\nwysłanie tej samej pary nie tworzy duplikatu - odpowiedź **200** z istniejącym\nrekordem i `info.created: false` (nic nie jest zmieniane; zmiany robi PUT).\nSprawdzenie duplikatu idzie PRZED walidacją pozostałych pól (od wersji API\n2.12.0), więc powtórka wraca jako 200 także wtedy, gdy niesie dane, których\nsam zapis by nie przyjął - typowy wzorzec telefonii to wysłanie metadanych\nzaraz po rozmowie i podsumowania AI kilka minut później.\n\n**Wymagane:** `source`, `sourceId`, `direction`, `status`, `remoteNumber`,\n`startedAt` oraz co najmniej jedno z `contactId` / `contractorId`.\n- `contractorId` → przypięcie + notatka „Telefon\" (tytuł nadaje CRM: „Telefon\n  od/do <osoba albo numer>\"; własny tytuł przez `title`). Bez `contractorId`\n  rekord powstaje tylko z osobą kontaktową, bez notatki na osi czasu.\n- `duration` (sekundy) ma znaczenie przy `status: answered` - raport VoIP liczy\n  odebrane po czasie rozmowy. Rozmowa odebrana i natychmiast przerwana\n  (`duration: 0`) jest przyjmowana od wersji API 2.12.0, z ostrzeżeniem\n  `info.warnings.duration`; wcześniej kończyła się 422. Przy pozostałych\n  statusach zapisujemy 0 (podany czas wraca w `info.warnings.duration`).\n- `userId` (czyja rozmowa) - bez niego CRM szuka właściciela `ownNumber`\n  w numerach VoIP instancji (linie wszystkich dostawców). Gdy numeru tam nie ma,\n  od wersji API 2.12.0 rozmowa **zapisuje się bez pracownika**, a `ownNumber`\n  wraca w `info.warnings.userId` - integracja nie wie, które linie administrator\n  wpisał w CRM, a rozmowa nie powinna z tego powodu przepadać. 422 zostaje\n  wyłącznie wtedy, gdy nie ma ani `userId`, ani `ownNumber`.\n- Numery zapisujemy w formacie międzynarodowym (`+48...`), jak w całym API.\n- `summary` (HTML) trafia do treści notatki, `tldr`, `recordingCallId` i `callsUrl`\n  zostają przy rekordzie; nagranie nie jest kopiowane do CRM.\n- `creatorUserId` - autor notatki (brak = użytkownik klucza API).\n\nDostawca VoIP „Tillio Calls\" i jego konfiguracja w instancji powstają\nautomatycznie przy pierwszym zapisie. Wymaga aktywnego modułu VoIP\n(inaczej 403 `module.notActive`). Dostępne od wersji API 2.10.0.","operationId":"createPhoneCall","requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["source","sourceId","direction","status","remoteNumber","startedAt"],"properties":{"source":{"description":"System źródłowy (1-64 znaki: litery, cyfry, kropka, myślnik, podkreślenie).","type":"string","example":"tillio-calls"},"sourceId":{"description":"Identyfikator rozmowy w systemie źródłowym (do 128 znaków). Z `source` tworzy klucz idempotencji.","type":"string","example":"call_01J8ZK3M9Q"},"direction":{"type":"string","enum":["inbound","outbound"],"example":"inbound"},"status":{"description":"Status rozmowy. `answered` bez `duration` (albo z zerem) zapisuje się z ostrzeżeniem - raport VoIP liczy odebrane po czasie rozmowy.","type":"string","enum":["answered","missed","busy","voicemail","failed"],"example":"answered"},"remoteNumber":{"description":"Numer rozmówcy (co najmniej 9 cyfr; zapis w formacie międzynarodowym).","type":"string","example":"+48601123456"},"ownNumber":{"description":"Numer własny (linia użytkownika). Bez niego, gdy podano `userId`, bierzemy numer tego użytkownika z ustawień VoIP.","type":"string","example":"+48731427000"},"startedAt":{"description":"Początek rozmowy (ISO 8601).","type":"string","format":"date-time","example":"2026-09-05T10:15:00+02:00"},"duration":{"description":"Czas rozmowy w sekundach.","type":"integer","example":184},"userId":{"description":"Użytkownik CRM, którego jest rozmowa (GET /v2/users). Brak = właściciel `ownNumber` z ustawień VoIP.","type":"integer","example":7},"contactId":{"description":"Osoba kontaktowa (GET /v2/contacts).","type":"integer","example":3921},"contractorId":{"description":"Kontrahent, do którego przypinamy rozmowę (GET /v2/contractors) - powstaje notatka „Telefon\".","type":"integer","example":121},"title":{"description":"Tytuł notatki „Telefon\" (do 256 znaków); brak = tytuł nadany przez CRM.","type":"string","example":"Telefon od Jan Kowalski - oferta"},"summary":{"description":"Podsumowanie rozmowy (HTML) - treść notatki.","type":"string","example":"<p>Klient pyta o termin wdrożenia.</p>"},"tldr":{"description":"Jednozdaniowe streszczenie (do 2000 znaków).","type":"string"},"recordingCallId":{"description":"Identyfikator nagrania w systemie źródłowym (do 128 znaków). Obecność oznacza w CRM, że nagranie istnieje.","type":"string"},"callsUrl":{"description":"Link do rozmowy w systemie źródłowym.","type":"string","format":"uri"},"creatorUserId":{"description":"Autor notatki „Telefon\" (GET /v2/users); brak = użytkownik przypisany do klucza API.","type":"integer","example":42}},"type":"object"}}}},"responses":{"201":{"description":"Połączenie utworzone","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/PhoneCall"},"info":{"$ref":"#/components/schemas/WriteInfo"}},"type":"object"}}}},"200":{"description":"Rozmowa o tej parze source + sourceId już istnieje - zwrócony istniejący rekord, info.created = false","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/PhoneCall"},"info":{"$ref":"#/components/schemas/WriteInfo"}},"type":"object"}}}},"422":{"description":"Błędy walidacji - lista {field, code, message} (m.in. nieznany kontrahent, brak userId ORAZ ownNumber, zły format daty)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Moduł VoIP nieaktywny na instancji (module.notActive) albo klucz bez prawa do notatek (phoneCall.forbidden) - z zapisem rozmowy powstaje notatka na kartotece","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/phone-calls/{id}":{"get":{"tags":["Phone calls"],"summary":"Pojedyncze połączenie po id","description":"Pełne dane połączenia wraz z przypiętymi kontrahentami (`contractorIds`) i notatkami „Telefon\" (`noteIds`, `title`). Dostępne od wersji API 2.10.0.","operationId":"getPhoneCall","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":2766}}],"responses":{"200":{"description":"Połączenie","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/PhoneCall"}},"type":"object"}}}},"404":{"description":"Brak połączenia o tym id (phoneCall.notFound)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"put":{"tags":["Phone calls"],"summary":"Aktualizacja połączenia (partial)","description":"Częściowa aktualizacja - wysyłasz TYLKO pola do zmiany (`title`, `summary`,\n`tldr`, `recordingCallId`, `callsUrl`, `userId`, `contactId`, `contractorId`,\n`status`, `duration`), reszta zostaje nietknięta. Typowy use-case: dopisanie\npodsumowania po analizie AI.\n\n- `summary` → treść notatek „Telefon\" wszystkich przypiętych kontrahentów\n  (ta sama ścieżka co „zapisz podsumowanie do notatki\" w panelu).\n- `title` → tytuł tych notatek.\n- `contractorId` → przepięcie rozmowy: odpięcie od dotychczasowych kontrahentów\n  (CRM kasuje wtedy ich notatki „Telefon\") i przypięcie do wskazanego z nową\n  notatką. Kontrahent już przypięty = bez zmian.\n- `status` / `duration` → jak przy tworzeniu (`answered` z zerowym czasem przechodzi z ostrzeżeniem).\n\nPola tożsamości rozmowy (`source`, `sourceId`, `direction`, numery, `startedAt`)\nsą tylko do odczytu - 422 `body.fieldNotUpdatable`. Rekordy integracji natywnych\n(Focus, Play...) nadpisze kolejna synchronizacja z dostawcą.\nDostępne od wersji API 2.10.0.","operationId":"updatePhoneCall","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":2766}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"properties":{"title":{"type":"string"},"summary":{"description":"Podsumowanie rozmowy (HTML) - trafia do notatek „Telefon\"","type":"string"},"tldr":{"type":"string"},"recordingCallId":{"type":"string"},"callsUrl":{"type":"string","format":"uri"},"userId":{"type":"integer"},"contactId":{"type":"integer"},"contractorId":{"type":"integer"},"status":{"type":"string","enum":["answered","missed","busy","voicemail","failed"]},"duration":{"description":"Sekundy","type":"integer"}},"type":"object"}}}},"responses":{"200":{"description":"Połączenie po aktualizacji","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/PhoneCall"},"info":{"$ref":"#/components/schemas/WriteInfo"}},"type":"object"}}}},"404":{"description":"Brak połączenia o tym id (phoneCall.notFound)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Błędy walidacji - lista {field, code, message}","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Moduł VoIP nieaktywny na instancji (module.notActive) albo klucz bez prawa do notatek (phoneCall.forbidden) - z zapisem rozmowy powstaje notatka na kartotece","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/pipeline/items":{"get":{"tags":["PipelineItems"],"summary":"Lista szans sprzedaży z filtrowaniem","description":"Pełna lista szans sprzedaży z filtrowaniem, sortowaniem i paginacją.\n\n**Typowe zastosowania:**\n- pipeline kontrahenta: `?contractorId=121`,\n- szanse handlowca: `?ownerUserId=7`,\n- lejek per etap: `?pipelineStageId=122`,\n- synchronizacja przyrostowa: `?updatedAfter=<data ostatniego syncu>` -\n  wyłącznie szanse utworzone/zmienione po tej chwili (`updatedAt` rośnie\n  przy każdej modyfikacji oraz aktywności na szansie).\n\nIdentyfikatory (`*Id`) i klucze integracyjne (`externalId`, `currency`)\nfiltrują dokładnie, pola tekstowe (`name`, `note`) częściowo (zawiera).\nFiltry po polach niestandardowych: `customField[<klucz>]=<wartość>`\n(definicje w GET /v2/pipeline/custom-fields).\n\nSłownik etapów (`pipelineStageId`) znajdziesz w GET /v2/pipeline/funnels;\nstatusy (`pipelineStatusId`): 1 = aktywna, 2 = stracona, 3 = wygrana.","operationId":"listPipelineItems","parameters":[{"name":"contractorId","in":"query","description":"Szanse wskazanego kontrahenta (dokładne).","required":false,"schema":{"type":"integer","example":121}},{"name":"pipelineStageId","in":"query","description":"Szanse na wskazanym etapie pipeline (dokładne).","required":false,"schema":{"type":"integer"}},{"name":"ownerUserId","in":"query","description":"Szanse wskazanego opiekuna (dokładne). Analogicznie dokładne: pipelineStatusId, creatorUserId, closerUserId, leadId, probability, currency, externalId, id.","required":false,"schema":{"type":"integer"}},{"name":"name","in":"query","description":"Filtr częściowy (zawiera). Analogicznie: note.","required":false,"schema":{"type":"string"}},{"name":"updatedAfter","in":"query","description":"Tylko rekordy utworzone/zmienione PO tej chwili (ISO 8601). Fundament syncu przyrostowego.","required":false,"schema":{"type":"string","format":"date-time","example":"2026-08-01T00:00:00Z"}},{"name":"updatedBefore","in":"query","description":"Górna granica zakresu zmian (ISO 8601, włącznie).","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"createdAfter","in":"query","description":"Tylko rekordy utworzone po tej chwili (ISO 8601).","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"createdBefore","in":"query","description":"Tylko rekordy utworzone do tej chwili (ISO 8601, włącznie).","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"customField","in":"query","description":"Filtry pól niestandardowych: `customField[<klucz>]=<wartość>` (dokładne; klucze i typy w GET /v2/pipeline/custom-fields). Pusta wartość = pole nieustawione.","required":false,"style":"deepObject","explode":true,"schema":{"type":"object","additionalProperties":{"type":"string"}}},{"name":"id","in":"query","required":false,"description":"Filtr po polu id - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"pipelineStatusId","in":"query","required":false,"description":"Filtr po polu pipelineStatusId - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"creatorUserId","in":"query","required":false,"description":"Filtr po polu creatorUserId - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"closerUserId","in":"query","required":false,"description":"Filtr po polu closerUserId - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"currency","in":"query","required":false,"description":"Filtr po polu currency - dopasowanie dokładne.","schema":{"type":"string"}},{"name":"probability","in":"query","required":false,"description":"Filtr po polu probability - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"note","in":"query","required":false,"description":"Filtr po polu note - dopasowanie częściowe (zawiera).","schema":{"type":"string"}},{"name":"externalId","in":"query","required":false,"description":"Filtr po polu externalId - dopasowanie dokładne.","schema":{"type":"string"}},{"name":"leadId","in":"query","required":false,"description":"Filtr po polu leadId - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"sort","in":"query","description":"Pole sortowania: id, name, amount, closeDate, createdAt, updatedAt.","required":false,"schema":{"type":"string","default":"updatedAt"}},{"name":"sortDir","in":"query","required":false,"schema":{"type":"string","default":"asc","enum":["asc","desc"]}},{"name":"page","in":"query","description":"Strona wyników, od 1.","required":false,"schema":{"type":"integer","default":1,"maximum":1000000,"minimum":1}},{"name":"limit","in":"query","description":"Rozmiar strony (max 1000).","required":false,"schema":{"type":"integer","default":100,"maximum":1000,"minimum":1}}],"responses":{"200":{"description":"Lista szans sprzedaży + metadane stronicowania","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/PipelineItem"}},"pagination":{"$ref":"#/components/schemas/Pagination"}},"type":"object"}}}},"400":{"description":"Błąd walidacji zapytania - lista błędów w kontrakcie {field, code, message}","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"post":{"tags":["PipelineItems"],"summary":"Nowa szansa sprzedaży","description":"Tworzy szansę sprzedaży tą samą ścieżką co aplikacja Tillio. Wymagane:\n`name`, `pipelineStageId` (etap z GET /v2/pipeline/funnels - wyznacza też\nprawdopodobieństwo wygranej, gdy nie podasz probability, oraz grupę\nprocesów, do której przypisane są pola niestandardowe) i `contractorId`\n(szansa zawsze wisi na kontrahencie). Opcjonalne: note (HTML),\namount+currency (brak waluty = waluta instancji), closeDate,\nprobability (0-100), ownerUserId (default = użytkownik klucza),\npipelineStatusId (brak = otwarta), customField.","operationId":"createPipelineItem","requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["name","pipelineStageId","contractorId"],"properties":{"name":{"type":"string","example":"ACME - licencje 2027"},"pipelineStageId":{"description":"Etap z GET /v2/pipeline/funnels","type":"integer","example":1},"contractorId":{"type":"integer","example":121},"note":{"type":"string","description":"Treść w HTML (CRM czyści niebezpieczne znaczniki)."},"amount":{"type":"string","example":"25000.00"},"currency":{"type":"string"},"closeDate":{"type":"string","format":"date"},"probability":{"type":"integer"},"ownerUserId":{"type":"integer"},"pipelineStatusId":{"type":"integer","description":"Tylko przy tworzeniu."},"createdAt":{"description":"IMPORT DANYCH: data utworzenia rekordu (ISO 8601, strefa instancji). Pozwala zachować chronologię przy wciąganiu historii sprzed lat - bez niej rekord dostaje datę importu. Tylko przy tworzeniu: przy aktualizacji zwracamy 422 (pomyłkę w imporcie poprawia się kasując rekord i importując ponownie). Data z przyszłości = 422. Dostępne od wersji API 1.16.0.","type":"string","format":"date-time","example":"2023-05-17T09:30:00"},"creatorUserId":{"description":"IMPORT DANYCH: użytkownik, który ma figurować jako twórca rekordu (GET /v2/users). Brak = użytkownik przypisany do klucza API. Zapis wykonuje się w kontekście tej osoby, więc obowiązują JEJ uprawnienia. Dostępne od wersji API 1.16.0.","type":"integer","example":42},"customField":{"type":"object","additionalProperties":{"nullable":true}}},"type":"object"}}}},"responses":{"201":{"description":"Szansa utworzona","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/PipelineItem"},"info":{"$ref":"#/components/schemas/WriteInfo"}},"type":"object"}}}},"422":{"description":"Błędy walidacji - lista {field, code, message}","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień do procesów sprzedaży","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/pipeline/items/{id}":{"get":{"tags":["PipelineItems"],"summary":"Pojedyncza szansa sprzedaży po id","description":"Pełne dane szansy sprzedaży wraz z polami niestandardowymi. Wystarczy id szansy, bez znajomości kontrahenta.","operationId":"getPipelineItem","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":6}}],"responses":{"200":{"description":"Szansa sprzedaży","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/PipelineItem"}},"type":"object"}}}},"404":{"description":"Brak szansy o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"put":{"tags":["PipelineItems"],"summary":"Aktualizacja szansy (partial)","description":"Częściowa aktualizacja szansy - wysyłasz TYLKO pola do zmiany (name, note,\namount, currency, closeDate, probability, ownerUserId, customField).\n`pipelineStageId`/`pipelineStatusId`/`contractorId` nie podlegają edycji przez API\n(przejścia etapów i zamknięcia to proces CRM z historią zmian).","operationId":"updatePipelineItem","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":6}}],"requestBody":{"description":"Aktualizacja CZĘŚCIOWA - wysyłasz tylko pola do zmiany. Poza listą poniżej zapis przyjmuje `customField`. Pola ustawiane WYŁĄCZNIE przy tworzeniu (próba zmiany = 422 `body.fieldNotUpdatable`): pipelineStageId, contractorId, pipelineStatusId.","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"note":{"type":"string","description":"Treść w HTML (CRM czyści niebezpieczne znaczniki)."},"amount":{"type":"string"},"currency":{"type":"string"},"closeDate":{"type":"string","format":"date"},"probability":{"type":"integer"},"ownerUserId":{"type":"integer"},"customField":{"type":"object","description":"Pola niestandardowe: klucz → wartość (klucze i typy: GET /v2/<encja>/custom-fields). Pola wielowartościowe (multiselect, produkty, szanse sprzedaży) przyjmują listę wartości, np. [\"12\",\"15\"] - pojedyncza wartość jest równoważna liście jednoelementowej. Lista dostępna od wersji API 2.9.0.","additionalProperties":{"nullable":true}}}}}}},"responses":{"200":{"description":"Szansa po aktualizacji","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/PipelineItem"},"info":{"$ref":"#/components/schemas/WriteInfo"}},"type":"object"}}}},"404":{"description":"Brak szansy o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Błędy walidacji","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień do procesów sprzedaży","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/ticket/processes/{id}":{"put":{"tags":["Dictionaries"],"summary":"Aktualizacja lejka zgłoszeń","description":"Częściowa aktualizacja lejka zgłoszeń (bez etapów - te mają własne końcówki).","operationId":"updateTicketProcess","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":1}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Lejek zaktualizowany"},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/ticket/processes/{id}/stages":{"post":{"tags":["Dictionaries"],"summary":"Nowy etap lejka zgłoszeń","description":"Nowy etap w lejku zgłoszeń.","operationId":"createTicketProcessStage","parameters":[{"name":"id","in":"path","description":"Id lejka","required":true,"schema":{"type":"integer","example":1}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["name"],"properties":{"name":{"type":"string","example":"W realizacji"},"color":{"type":"string","example":"#FFA726"},"order":{"type":"integer","example":2}},"type":"object"}}}},"responses":{"201":{"description":"Etap utworzony"},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/ticket/stages/{id}":{"put":{"tags":["Dictionaries"],"summary":"Aktualizacja etapu lejka zgłoszeń","description":"Częściowa aktualizacja etapu lejka zgłoszeń.","operationId":"updateTicketProcessStage","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":3}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Etap zaktualizowany"},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/pipeline/funnels/{id}":{"put":{"tags":["Dictionaries"],"summary":"Aktualizacja lejka sprzedaży","description":"Częściowa aktualizacja lejka sprzedaży.","operationId":"updatePipelineFunnel","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":1}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Lejek zaktualizowany"},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/pipeline/funnels/{id}/stages":{"post":{"tags":["Dictionaries"],"summary":"Nowy etap lejka sprzedaży","description":"Nowy etap w lejku sprzedaży.","operationId":"createPipelineStage","parameters":[{"name":"id","in":"path","description":"Id lejka","required":true,"schema":{"type":"integer","example":1}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["name"],"properties":{"name":{"type":"string","example":"Negocjacje"},"color":{"type":"string","example":"#3398f0"},"probability":{"type":"integer","example":90},"order":{"type":"integer","example":4}},"type":"object"}}}},"responses":{"201":{"description":"Etap utworzony"},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/pipeline/stages/{id}":{"put":{"tags":["Dictionaries"],"summary":"Aktualizacja etapu lejka sprzedaży","description":"Częściowa aktualizacja etapu lejka sprzedaży.","operationId":"updatePipelineStage","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":4}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Etap zaktualizowany"},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/lead/processes":{"post":{"tags":["Dictionaries"],"summary":"Nowy proces leadowy","description":"Nowy proces leadowy (grupa statusów; odczyt: `GET /v2/lead/statuses`).\n\nUWAGA: CRM zakłada nowy proces razem z KOMPLETEM statusów startowych\n(kopiowanych z szablonu), więc statusy podane w `statuses` **dochodzą**\ndo tamtych, a nie zastępują ich.","operationId":"createLeadProcess","requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["name"],"properties":{"name":{"type":"string","example":"Proces leady partnerzy"},"order":{"type":"integer","example":2},"acl":{"oneOf":[{"$ref":"#/components/schemas/AclJson"}],"nullable":true,"description":"Ograniczenie widoczności: {userIds?, departmentIds?, groupIds?}. Pominięte albo puste = bez ograniczeń."},"active":{"type":"boolean","example":true},"stages":{"description":"Dodatkowe statusy procesu (pole `stages` - wspólna nazwa dla wszystkich procesów)","type":"array","items":{"properties":{"name":{"type":"string","example":"Do kontaktu"},"color":{"type":"string","example":"#4b78c5"},"order":{"type":"integer","example":2},"type":{"description":"Typ statusu w procesie (wg konwencji CRM)","type":"string","nullable":true}},"type":"object"}}},"type":"object"}}}},"responses":{"201":{"description":"Proces utworzony"},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/lead/processes/{id}":{"put":{"tags":["Dictionaries"],"summary":"Aktualizacja procesu leadowego","description":"Częściowa aktualizacja procesu leadowego.","operationId":"updateLeadProcess","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":400}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Proces zaktualizowany"},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/lead/processes/{id}/statuses":{"post":{"tags":["Dictionaries"],"summary":"Nowy status leada","description":"Nowy status w procesie leadowym.","operationId":"createLeadStatus","parameters":[{"name":"id","in":"path","description":"Id procesu leadowego","required":true,"schema":{"type":"integer","example":400}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["name"],"properties":{"name":{"type":"string","example":"Do kontaktu"},"color":{"type":"string","example":"#4b78c5"},"order":{"type":"integer","example":2},"type":{"type":"string","nullable":true}},"type":"object"}}}},"responses":{"201":{"description":"Status utworzony"},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/lead/statuses/{id}":{"put":{"tags":["Dictionaries"],"summary":"Aktualizacja statusu leada","description":"Częściowa aktualizacja statusu leada.","operationId":"updateLeadStatus","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":400}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Status zaktualizowany"},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/product/groups":{"get":{"tags":["Products"],"summary":"Lista grup produktów (drzewo przez parentId)","description":"Słownik grup produktów - do mapowania `groupId` z GET /v2/products na nazwy\ni do odtworzenia drzewa kategorii po stronie integracji (sklep, ERP).\n\nDrzewo składasz po `parentId`: null = grupa główna, wartość = id rodzica.\nGrup jest zwykle kilkadziesiąt - jedna strona wystarcza, paginacja jak wszędzie.\nGrupy główne (parentId = null) wyłuskaj po swojej stronie - filtr pustej\nwartości nie jest wspierany.\n\nPrzykład - aktywne podgrupy grupy \"Meble\" (id 66) w kolejności z CRM:\n```\nGET /v2/product/groups?parentId=66&status=1&sort=priority\n```","operationId":"listProductGroups","parameters":[{"name":"name","in":"query","description":"Filtr częściowy (zawiera).","required":false,"schema":{"type":"string"}},{"name":"parentId","in":"query","description":"Podgrupy wskazanej grupy - dopasowanie dokładne. Analogicznie dokładne: id, status.","required":false,"schema":{"type":"integer","example":66}},{"name":"id","in":"query","required":false,"description":"Filtr po polu id - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"status","in":"query","required":false,"description":"Filtr po polu status - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"sort","in":"query","description":"Pole sortowania: id, name, priority.","required":false,"schema":{"type":"string","default":"id"}},{"name":"sortDir","in":"query","required":false,"schema":{"type":"string","default":"asc","enum":["asc","desc"]}},{"name":"page","in":"query","description":"Strona wyników, od 1.","required":false,"schema":{"type":"integer","default":1,"maximum":1000000,"minimum":1}},{"name":"limit","in":"query","description":"Rozmiar strony (max 1000).","required":false,"schema":{"type":"integer","default":100,"maximum":1000,"minimum":1}}],"responses":{"200":{"description":"Lista grup produktów + metadane stronicowania","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ProductGroup"}},"pagination":{"$ref":"#/components/schemas/Pagination"}},"type":"object"}}}},"400":{"description":"Błąd walidacji zapytania - lista błędów w kontrakcie {field, code, message}","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"post":{"tags":["Products"],"summary":"Nowa grupa produktów","description":"Tworzy grupę produktów (drzewo: parentId = grupa nadrzędna, brak = grupa główna). Wymagane tylko name. Zły hex koloru CRM zamienia na domyślny.","operationId":"createProductGroup","requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["name"],"properties":{"createdAt":{"description":"IMPORT DANYCH: data utworzenia rekordu (ISO 8601, strefa instancji). Pozwala zachować chronologię przy wciąganiu historii sprzed lat - bez niej rekord dostaje datę importu. Tylko przy tworzeniu: przy aktualizacji zwracamy 422 (pomyłkę w imporcie poprawia się kasując rekord i importując ponownie). Data z przyszłości = 422. Dostępne od wersji API 1.16.0.","type":"string","format":"date-time","example":"2023-05-17T09:30:00"},"creatorUserId":{"description":"IMPORT DANYCH: użytkownik, który ma figurować jako twórca rekordu (GET /v2/users). Brak = użytkownik przypisany do klucza API. Zapis wykonuje się w kontekście tej osoby, więc obowiązują JEJ uprawnienia. Dostępne od wersji API 1.16.0.","type":"integer","example":42},"name":{"type":"string","example":"Licencje"},"parentId":{"description":"Grupa nadrzędna (drzewo); brak = grupa główna","type":"integer"},"color":{"description":"Kolor etykiety w CRM (hex). Pominięty = neutralny szary #cccccc.","type":"string","default":"#cccccc","example":"#4b78c5"},"priority":{"description":"Kolejność wyświetlania (rosnąco). Pominięty = 0.","type":"integer","default":0},"status":{"type":"integer","default":1}},"type":"object"}}}},"responses":{"201":{"description":"Grupa utworzona","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/ProductGroup"},"info":{"$ref":"#/components/schemas/WriteInfo"}},"type":"object"}}}},"422":{"description":"Błędy walidacji - lista {field, code, message}","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień do produktów","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/product/groups/{id}":{"get":{"tags":["Products"],"summary":"Grupa produktów po id","description":"Pojedyncza grupa produktów po id - adres, który zwraca nagłówek Location po utworzeniu grupy.","operationId":"getProductGroup","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":66}}],"responses":{"200":{"description":"Grupa produktów","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/ProductGroup"}},"type":"object"}}}},"404":{"description":"Brak grupy o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"put":{"tags":["Products"],"summary":"Aktualizacja grupy produktów (partial)","description":"Częściowa aktualizacja grupy - wysyłasz TYLKO pola do zmiany (name, color, priority, status). parentId nie podlega edycji (przenoszenie w drzewie to operacja CRM).","operationId":"updateProductGroup","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":66}}],"requestBody":{"description":"Pola do zmiany","required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Grupa po aktualizacji","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/ProductGroup"},"info":{"$ref":"#/components/schemas/WriteInfo"}},"type":"object"}}}},"404":{"description":"Brak grupy o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Błędy walidacji","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień do produktów","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/products":{"get":{"tags":["Products"],"summary":"Lista produktów z filtrowaniem po dowolnym polu","description":"Kartoteka produktów dla integracji (ERP, sklepy, synchronizacja cenników) i aplikacji.\n\n**Matchowanie rekordów integracji:** filtruj po swoim kluczu - `sku=KRZ-ERGO-01`\nalbo `ean=5901234567890` (dopasowanie dokładne). Typowy flow importu do ERP:\npobierz produkt po `sku`; brak wyniku = załóż kartotekę po swojej stronie.\n\n**Synchronizacja przyrostowa:** `updatedAfter` po dacie modyfikacji\nkartoteki (`updatedAt`, utrzymywana przez bazę CRM) - pobierasz tylko\nprodukty zmienione od ostatniego syncu.\n\nPola tekstowe (`name`, `description`) filtrują częściowo (zawiera), klucze\nintegracyjne (`sku`, `ean`) i identyfikatory (`id`, `groupId`, `measureId`,\n`status`, `currency`) - dokładnie.\n\nPrzykład - aktywne produkty z grupy 66 do wystawienia w sklepie:\n```\nGET /v2/products?groupId=66&status=1&sort=name\n```","operationId":"listProducts","parameters":[{"name":"updatedAfter","in":"query","description":"Tylko produkty zmodyfikowane po tej chwili (ISO 8601) - sync przyrostowy.","required":false,"schema":{"type":"string","format":"date-time","example":"2026-08-01T00:00:00Z"}},{"name":"updatedBefore","in":"query","description":"Górna granica zakresu modyfikacji (ISO 8601, włącznie).","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"customField","in":"query","description":"Filtry pól niestandardowych: `customField[<klucz>]=<wartość>` (dokładne). Obecnie CRM nie definiuje pól niestandardowych produktów - każdy klucz zwróci 400.","required":false,"style":"deepObject","explode":true,"schema":{"type":"object","additionalProperties":{"type":"string"}}},{"name":"name","in":"query","description":"Filtr częściowy (zawiera). Analogicznie: description.","required":false,"schema":{"type":"string"}},{"name":"sku","in":"query","description":"Kod magazynowy - dopasowanie dokładne. Analogicznie dokładne: ean, externalId, groupId, measureId, status, currency, id.","required":false,"schema":{"type":"string","example":"KRZ-ERGO-01"}},{"name":"externalId","in":"query","description":"Id produktu w systemie zewnętrznym - dopasowanie dokładne; bez unikalności w DB, może zwrócić wiele rekordów. Konwencja: własny prefix integracji, np. op1_10023.","required":false,"schema":{"type":"string","example":"op1_10023"}},{"name":"id","in":"query","required":false,"description":"Filtr po polu id - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"description","in":"query","required":false,"description":"Filtr po polu description - dopasowanie częściowe (zawiera).","schema":{"type":"string"}},{"name":"ean","in":"query","required":false,"description":"Filtr po polu ean - dopasowanie dokładne.","schema":{"type":"string"}},{"name":"groupId","in":"query","required":false,"description":"Filtr po polu groupId - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"measureId","in":"query","required":false,"description":"Filtr po polu measureId - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"currency","in":"query","required":false,"description":"Filtr po polu currency - dopasowanie dokładne.","schema":{"type":"string"}},{"name":"status","in":"query","required":false,"description":"Filtr po polu status - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"sort","in":"query","description":"Pole sortowania: id, name, sku, price.","required":false,"schema":{"type":"string","default":"id"}},{"name":"sortDir","in":"query","required":false,"schema":{"type":"string","default":"asc","enum":["asc","desc"]}},{"name":"page","in":"query","description":"Strona wyników, od 1.","required":false,"schema":{"type":"integer","default":1,"maximum":1000000,"minimum":1}},{"name":"limit","in":"query","description":"Rozmiar strony (max 1000).","required":false,"schema":{"type":"integer","default":100,"maximum":1000,"minimum":1}}],"responses":{"200":{"description":"Lista produktów + metadane stronicowania","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Product"}},"pagination":{"$ref":"#/components/schemas/Pagination"}},"type":"object"}}}},"400":{"description":"Błąd walidacji zapytania - lista błędów w kontrakcie {field, code, message}","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"post":{"tags":["Products"],"summary":"Nowy produkt w katalogu","description":"Dodaje produkt do katalogu tą samą ścieżką, której używa aplikacja Tillio\n(walidacje core; waluta domyślna instancji, gdy nie podasz `currency`).\nWymagane tylko `name` (min 3 znaki). `groupId` i `measureId` walidowane\nsłownikami (grupy: GET /v2/product/groups).\n\n**Ochrona przed duplikatami katalogu:** przed zapisem API samo sprawdza,\nczy taki produkt już istnieje - domyślnie po `externalId`, potem `sku`,\npotem `ean` (klucze matchowania z ERP; tablicą `duplicateCheck` z enum\nexternalId|sku|ean|name ustawiasz pola i kolejność). Znaleziony =\n**HTTP 200** z istniejącym rekordem i `info.duplicate = {matchedBy,\nproductId}` - żadne dane nie są zmieniane; zwrócone `productId` używaj\ndalej (np. PUT /v2/warehouses/{id}/stocks/{productId}).\n`allowDuplicates: true` wyłącza sprawdzanie. Odnalezienie istniejącego\nproduktu bez zapisu: filtr `GET /v2/products?externalId=...` / `?sku=...`.\n\n`externalId` = id produktu w systemie zewnętrznym (np. Optima). Celowo\nBEZ wymuszania unikalności - do instancji może być podpiętych kilka\nsystemów naraz i każdy numeruje od 1. **Konwencja: każda integracja\nużywa własnego prefixu**, np. `op1_[id-z-bazy-optima]` → `op1_10023`\n(druga instancja Optimy: `op2_...`, wFirma: `wf1_...`) - wtedy wartość\njest unikalna per integracja i duplicateCheck oraz filtr po externalId\ntrafiają zawsze we właściwy rekord. Dopasowanie duplikatu bierze\npierwszy znaleziony rekord.","operationId":"createProduct","requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["name"],"properties":{"createdAt":{"description":"IMPORT DANYCH: data utworzenia rekordu (ISO 8601, strefa instancji). Pozwala zachować chronologię przy wciąganiu historii sprzed lat - bez niej rekord dostaje datę importu. Tylko przy tworzeniu: przy aktualizacji zwracamy 422 (pomyłkę w imporcie poprawia się kasując rekord i importując ponownie). Data z przyszłości = 422. Dostępne od wersji API 1.16.0.","type":"string","format":"date-time","example":"2023-05-17T09:30:00"},"creatorUserId":{"description":"IMPORT DANYCH: użytkownik, który ma figurować jako twórca rekordu (GET /v2/users). Brak = użytkownik przypisany do klucza API. Zapis wykonuje się w kontekście tej osoby, więc obowiązują JEJ uprawnienia. Dostępne od wersji API 1.16.0.","type":"integer","example":42},"name":{"type":"string","example":"Licencja Tillio PRO"},"description":{"type":"string"},"sku":{"type":"string","example":"TIL-PRO-1Y"},"ean":{"type":"string"},"externalId":{"description":"Id produktu w systemie zewnętrznym - klucz matchowania z ERP, bez wymuszania unikalności. Konwencja: własny prefix integracji (unikalność per integracja), np. op1_10023.","type":"string","example":"op1_10023"},"groupId":{"description":"Grupa z GET /v2/product/groups","type":"integer"},"measureId":{"description":"Jednostka miary","type":"integer"},"price":{"description":"Cena bazowa (kropka dziesiętna)","type":"string","example":"499.00"},"currency":{"description":"ISO 4217; default = waluta instancji","type":"string","example":"PLN"},"taxRate":{"description":"Stawka VAT w %","type":"string","example":"23"},"status":{"type":"integer","default":1},"duplicateCheck":{"description":"Pola, po których szukamy istniejącego produktu, w kolejności priorytetu (default: [\"externalId\",\"sku\",\"ean\"]) Sprawdzane są wyłącznie pola, których wartość jest w tym samym żądaniu; pominięte wracają w info.warnings.duplicateCheck, a gdy żadne nie ma wartości - 422 body.duplicateCheckUnusable. Opcja `requireDuplicateCheck: true` (tryb dla importów, fail-closed) wymaga, by KAŻDE pole z listy miało wartość - brak choćby jednego (np. klucza integracji przy podanym NIP-ie) kończy się 422 wskazującym to konkretne pole, a nie samo duplicateCheck; działa też przy domyślnym zestawie. Opcja dostępna od wersji API 1.5.1 (wersja instancji: GET /v2/health).","type":"array","items":{"type":"string","enum":["externalId","sku","ean","name"]},"example":["externalId"]},"allowDuplicates":{"description":"true = bez sprawdzania duplikatów, zawsze insert","type":"boolean","default":false}},"type":"object"}}}},"responses":{"201":{"description":"Produkt utworzony","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/Product"},"info":{"$ref":"#/components/schemas/WriteInfo"}},"type":"object"}}}},"200":{"description":"Produkt już istnieje - zwrócony istniejący rekord, info.duplicate wskazuje pole, po którym go znaleziono; żadne dane nie zostały zmienione","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/Product"},"info":{"$ref":"#/components/schemas/WriteInfo"}},"type":"object"}}}},"422":{"description":"Błędy walidacji - lista {field, code, message}","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień do produktów","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/products/{id}":{"get":{"tags":["Products"],"summary":"Pojedynczy produkt po id","description":"Pełne dane produktu wraz z grupą, jednostką miary i polami niestandardowymi.","operationId":"getProduct","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":235}}],"responses":{"200":{"description":"Produkt","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/Product"}},"type":"object"}}}},"404":{"description":"Brak produktu o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"put":{"tags":["Products"],"summary":"Aktualizacja produktu (partial)","description":"Częściowa aktualizacja produktu - wysyłasz TYLKO pola do zmiany, reszta\nzostaje nietknięta; nie trzeba wcześniej pobierać rekordu. Ta sama\nścieżka zapisu co edycja w aplikacji Tillio.","operationId":"updateProduct","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":235}}],"requestBody":{"description":"Pola do zmiany (podzbiór pól z POST)","required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Produkt po aktualizacji","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/Product"},"info":{"$ref":"#/components/schemas/WriteInfo"}},"type":"object"}}}},"404":{"description":"Brak produktu o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Błędy walidacji","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień do produktów","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/projects":{"get":{"tags":["Projects"],"summary":"Lista projektów z filtrowaniem","description":"Projekty wraz z licznikami zadań (wszystkie / wykonane / niewykonane / po terminie) -\ndla integracji raportowych i synchronizacji portfela projektów.\n\n**Synchronizacja przyrostowa:** przekaż `updatedAfter` z datą ostatniego udanego\nsyncu - dostaniesz projekty utworzone lub zmienione po tej chwili (baza podbija\ndatę modyfikacji automatycznie przy każdej zmianie projektu).\n\nPrzykład - aktywne projekty kontrahenta zmienione od początku miesiąca:\n```\nGET /v2/projects?contractorId=121&archived=false&updatedAfter=2026-08-01T00:00:00Z\n```","operationId":"listProjects","parameters":[{"name":"contractorId","in":"query","description":"Projekty powiązane z kontrahentem.","required":false,"schema":{"type":"integer","example":121}},{"name":"projectStatusId","in":"query","description":"Status projektu wg słownika CRM.","required":false,"schema":{"type":"integer","example":1}},{"name":"name","in":"query","description":"Filtr częściowy (zawiera). Analogicznie: description.","required":false,"schema":{"type":"string","example":"ERP"}},{"name":"archived","in":"query","description":"false = tylko aktywne, true = tylko zarchiwizowane. Dokładne także: id, ownerUserId, creatorUserId.","required":false,"schema":{"type":"boolean"}},{"name":"updatedAfter","in":"query","description":"Tylko projekty utworzone/zmienione PO tej chwili (ISO 8601). Fundament syncu przyrostowego.","required":false,"schema":{"type":"string","format":"date-time","example":"2026-08-01T00:00:00Z"}},{"name":"updatedBefore","in":"query","description":"Górna granica zakresu zmian (ISO 8601, włącznie).","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"createdAfter","in":"query","description":"Tylko projekty utworzone po tej chwili (ISO 8601).","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"createdBefore","in":"query","description":"Tylko projekty utworzone do tej chwili (ISO 8601, włącznie).","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"customField","in":"query","description":"Filtry pól niestandardowych: `customField[<klucz>]=<wartość>` (dokładne; klucze w GET /v2/project/custom-fields). Pusta wartość = pole nieustawione.","required":false,"style":"deepObject","explode":true,"schema":{"type":"object","additionalProperties":{"type":"string"}}},{"name":"id","in":"query","required":false,"description":"Filtr po polu id - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"description","in":"query","required":false,"description":"Filtr po polu description - dopasowanie częściowe (zawiera).","schema":{"type":"string"}},{"name":"ownerUserId","in":"query","required":false,"description":"Filtr po polu ownerUserId - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"creatorUserId","in":"query","required":false,"description":"Filtr po polu creatorUserId - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"sort","in":"query","description":"Pole sortowania: id, name, startDate, dueDate, createdAt, updatedAt.","required":false,"schema":{"type":"string","default":"updatedAt"}},{"name":"sortDir","in":"query","required":false,"schema":{"type":"string","default":"asc","enum":["asc","desc"]}},{"name":"page","in":"query","description":"Strona wyników, od 1.","required":false,"schema":{"type":"integer","default":1,"maximum":1000000,"minimum":1}},{"name":"limit","in":"query","description":"Rozmiar strony (max 1000).","required":false,"schema":{"type":"integer","default":100,"maximum":1000,"minimum":1}}],"responses":{"200":{"description":"Lista projektów + metadane stronicowania","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Project"}},"pagination":{"$ref":"#/components/schemas/Pagination"}},"type":"object"}}}},"400":{"description":"Błąd walidacji zapytania - lista błędów w kontrakcie {field, code, message}","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"post":{"tags":["Projects"],"summary":"Nowy projekt","description":"Tworzy projekt tą samą ścieżką co aplikacja Tillio. Wymagane tylko `name`.\nPola: name, description (HTML), contractorId, ownerUserId (default =\nużytkownik klucza; CRM sam dodaje właściciela do uczestników), projectStatusId\n(GET /v2/project/statuses; brak = domyślny status CRM), color,\nstartDate+dueDate (para), customField (GET /v2/project/custom-fields).","operationId":"createProject","requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["name"],"properties":{"name":{"type":"string","example":"Wdrożenie ACME"},"description":{"type":"string","description":"Treść w HTML (CRM czyści niebezpieczne znaczniki)."},"contractorId":{"type":"integer","example":121},"ownerUserId":{"type":"integer"},"projectStatusId":{"type":"integer","description":"Wartość musi istnieć w słowniku CRM."},"color":{"type":"string"},"startDate":{"type":"string","format":"date"},"dueDate":{"type":"string","format":"date"},"createdAt":{"description":"IMPORT DANYCH: data utworzenia rekordu (ISO 8601, strefa instancji). Pozwala zachować chronologię przy wciąganiu historii sprzed lat - bez niej rekord dostaje datę importu. Tylko przy tworzeniu: przy aktualizacji zwracamy 422 (pomyłkę w imporcie poprawia się kasując rekord i importując ponownie). Data z przyszłości = 422. Dostępne od wersji API 1.16.0.","type":"string","format":"date-time","example":"2023-05-17T09:30:00"},"creatorUserId":{"description":"IMPORT DANYCH: użytkownik, który ma figurować jako twórca rekordu (GET /v2/users). Brak = użytkownik przypisany do klucza API. Zapis wykonuje się w kontekście tej osoby, więc obowiązują JEJ uprawnienia. Dostępne od wersji API 1.16.0.","type":"integer","example":42},"customField":{"type":"object","additionalProperties":{"nullable":true}}},"type":"object"}}}},"responses":{"201":{"description":"Projekt utworzony","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/Project"},"info":{"$ref":"#/components/schemas/WriteInfo"}},"type":"object"}}}},"422":{"description":"Błędy walidacji - lista {field, code, message}","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień do projektów","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/projects/{id}":{"get":{"tags":["Projects"],"summary":"Pojedynczy projekt po id","description":"Pełne dane projektu wraz z licznikami zadań i polami niestandardowymi.","operationId":"getProject","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":27}}],"responses":{"200":{"description":"Projekt","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/Project"}},"type":"object"}}}},"404":{"description":"Brak projektu o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"put":{"tags":["Projects"],"summary":"Aktualizacja projektu (partial)","description":"Częściowa aktualizacja projektu - wysyłasz TYLKO pola do zmiany (name, description, contractorId, ownerUserId, projectStatusId, color, startDate/dueDate, customField).","operationId":"updateProject","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":224}}],"requestBody":{"description":"Pola do zmiany (podzbiór pól z POST + customField)","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string","description":"Treść w HTML (CRM czyści niebezpieczne znaczniki)."},"contractorId":{"type":"integer","description":"Wartość musi istnieć w słowniku CRM."},"ownerUserId":{"type":"integer"},"projectStatusId":{"type":"integer","description":"Wartość musi istnieć w słowniku CRM."},"color":{"type":"string"},"startDate":{"type":"string","format":"date"},"dueDate":{"type":"string","format":"date"},"customField":{"type":"object","description":"Pola niestandardowe: klucz → wartość (klucze i typy: GET /v2/<encja>/custom-fields). Pola wielowartościowe (multiselect, produkty, szanse sprzedaży) przyjmują listę wartości, np. [\"12\",\"15\"] - pojedyncza wartość jest równoważna liście jednoelementowej. Lista dostępna od wersji API 2.9.0.","additionalProperties":{"nullable":true}}}}}}},"responses":{"200":{"description":"Projekt po aktualizacji","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/Project"},"info":{"$ref":"#/components/schemas/WriteInfo"}},"type":"object"}}}},"404":{"description":"Brak projektu o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Błędy walidacji","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień do projektów","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/selfcheck":{"get":{"tags":["System"],"summary":"Selfcheck zgodności z instancją CRM (szkielet core + schemat DB)","description":"Weryfikacja zgodności API v2 z instancją CRM - do odpytywania AUTOMATEM\n(np. raz dziennie oraz po każdym deployu core), żeby dryf między API\na CRM wychodził w monitoringu, a nie u klientów. Trzy warstwy:\n\n- **core** - szkielet: każda klasa i metoda core, którą API woła\n  in-process, istnieje pod tą nazwą na tej instancji,\n- **database** - odczyt: tabela każdego używanego modelu istnieje w DB\n  tenanta, wszystkie pola modeli są kolumnami tych tabel, a dodatkowo\n  każda kolumna użyta w mapach read API (kontrakty pól + słowniki)\n  istnieje w DB - nawet jeśli jeszcze nie ma jej w modelu core,\n- **acl** - uprawnienia (informacyjnie): czy każdy klucz, którym API rozstrzyga\n  403 przy zapisie, istnieje w tabeli uprawnień tenanta. Dostępne od wersji\n  API 2.14.0.\n\nZero zapytań do danych: jeden SELECT z `information_schema` + lista nazw\nkluczy uprawnień + refleksja - odpowiedź w ułamku sekundy. **`status: failed`\n= HTTP 500** (monitoring alarmuje po samym kodzie), szczegóły zawsze w body:\ndokładna lista brakujących klas/metod/tabel/kolumn ze wskazaniem, gdzie są\nużywane.\n\n`unmappedReadColumns` (tokeny z wyrażeń SQL nierozpoznane jako kolumny, np.\nfunkcje) i `acl.missingKeys` są informacyjne - nie wpływają na status.","operationId":"selfCheck","responses":{"200":{"description":"Zgodność potwierdzona (status: ok)","content":{"application/json":{"schema":{"properties":{"data":{"properties":{"status":{"type":"string","enum":["ok","failed"],"example":"ok"},"version":{"description":"Wersja API v2 (semver)","type":"string","example":"1.0.0"},"durationMs":{"type":"integer","example":180},"runtime":{"description":"Stan środowiska tej instalacji - do diagnostyki „działa, ale wolno” i przy zgłoszeniach. Za bramką auth, bo dokładna wersja PHP nie idzie anonimowo (dostępne od wersji API 2.1.0).","properties":{"env":{"description":"Tryb aplikacji (prod/dev)","type":"string","example":"prod"},"php":{"description":"Wersja PHP runtime instancji","type":"string","example":"8.2.33"},"routeCache":{"description":"Czy cache tras jest włączony","type":"boolean","example":true},"opcache":{"description":"Czy opcache jest aktywny","type":"boolean","example":true}},"type":"object"},"core":{"properties":{"classesChecked":{"type":"integer"},"methodsChecked":{"type":"integer"},"missingClasses":{"type":"array","items":{"type":"string"}},"missingMethods":{"type":"array","items":{"type":"string"}},"callbacksChecked":{"description":"Liczba pól modeli CRM, których konfigurację zapisu sprawdzono (dostępne od wersji API 2.14.1)","type":"integer"},"mismatchedCallbacks":{"description":"Pola modeli CRM, których zapis wskazuje inną metodę niż ta, na której polega API - zapis przeszedłby bez błędu i bez danych, np. lead bez adresów e-mail (dostępne od wersji API 2.14.1)","type":"array","items":{"type":"string"}}},"type":"object"},"database":{"properties":{"connectedTo":{"description":"Nazwa bazy, z którą sprawdzenie faktycznie rozmawiało - do potwierdzenia, że instalacja pracuje na własnych danych (dostępne od wersji API 2.1.2).","type":"string","example":"crm_klient_musq"},"modelsChecked":{"type":"integer"},"modelColumnsChecked":{"type":"integer"},"readColumnsChecked":{"type":"integer"},"missingTables":{"type":"array","items":{"type":"string"}},"missingModelColumns":{"type":"array","items":{"type":"string"}},"missingReadColumns":{"type":"array","items":{"type":"string"}},"unmappedReadColumns":{"description":"Informacyjne - nie wpływa na status","type":"array","items":{"type":"string"}}},"type":"object"},"acl":{"description":"Klucze uprawnień, na których opierają się bramki 403 API, vs tabela uprawnień tenanta (dostępne od wersji API 2.14.0)","properties":{"keysChecked":{"type":"integer"},"missingKeys":{"description":"Informacyjne - nie wpływa na status. Klucze nieznane tej instalacji, ze wskazaniem miejsc użycia (bramka na takim kluczu odmawia użytkownikom bez superadmina)","type":"array","items":{"type":"string"}}},"type":"object"}},"type":"object"}},"type":"object"}}}},"500":{"description":"Wykryto niezgodność - koperta błędu `_error` (kod selfcheck.failed) z listą braków w `errors` i pełnym raportem w `report`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/services":{"get":{"tags":["Services"],"summary":"Lista usług z filtrowaniem po kontrahencie, statusie i pozycji katalogu","description":"Usługi sprzedane/przypisane kontrahentom - podstawa integracji billingowych\n(fakturowanie cykliczne, kontrola umów) i raportowych.\n\n**Typowe filtry:** usługi kontrahenta `?contractorId=121`, usługi w danym statusie\n`?serviceStatusId=1`, wszystkie instancje pozycji katalogu `?catalogId=7`. Pola tekstowe\n(`customName`, `catalogName`, `place`, `note`) filtrują częściowo (zawiera),\nidentyfikatory i `currency` - dokładnie.\n\nDaty umowy (`agreement*`) przychodzą z powiązania 1:1 - null, gdy usługa nie ma\numowy (pozycje katalogu z `isAgreement=true` w GET /v2/service/catalog).\n\n**Przyrost:** `updatedAfter` po dacie modyfikacji (`updatedAt`,\nutrzymywana przez bazę CRM) - łapie nowe usługi i każdą zmianę rekordu.\n\nPrzykład - aktywne usługi kontrahenta:\n```\nGET /v2/services?contractorId=121&serviceStatusId=1\n```","operationId":"listServices","parameters":[{"name":"contractorId","in":"query","description":"Usługi kontrahenta - dokładne dopasowanie.","required":false,"schema":{"type":"integer","example":121}},{"name":"serviceStatusId","in":"query","description":"Status usługi (słownik CRM) - dokładne dopasowanie. Analogicznie dokładne: catalogId, billingPeriodId, paymentTermId, invoiceTypeId, salesUserId, ownerUserId.","required":false,"schema":{"type":"integer"}},{"name":"customName","in":"query","description":"Filtr częściowy (zawiera). Analogicznie: catalogName, place, note.","required":false,"schema":{"type":"string"}},{"name":"currency","in":"query","description":"Waluta - dokładne dopasowanie (ISO 4217).","required":false,"schema":{"type":"string","example":"PLN"}},{"name":"updatedAfter","in":"query","description":"Tylko usługi zmodyfikowane po tej chwili (ISO 8601) - fundament syncu przyrostowego.","required":false,"schema":{"type":"string","format":"date-time","example":"2026-08-01T00:00:00Z"}},{"name":"updatedBefore","in":"query","description":"Górna granica zakresu modyfikacji (ISO 8601, włącznie).","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"createdAfter","in":"query","description":"Tylko usługi utworzone po tej chwili (ISO 8601).","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"createdBefore","in":"query","description":"Tylko usługi utworzone do tej chwili (ISO 8601, włącznie).","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"customField","in":"query","description":"Filtry pól niestandardowych usług: `customField[<klucz>]=<wartość>` (dokładne; klucze w GET /v2/service/custom-fields). Pusta wartość = pole nieustawione.","required":false,"style":"deepObject","explode":true,"schema":{"type":"object","additionalProperties":{"type":"string"}}},{"name":"id","in":"query","required":false,"description":"Filtr po polu id - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"catalogId","in":"query","required":false,"description":"Filtr po polu catalogId - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"catalogName","in":"query","required":false,"description":"Filtr po polu catalogName - dopasowanie częściowe (zawiera).","schema":{"type":"string"}},{"name":"billingPeriodId","in":"query","required":false,"description":"Filtr po polu billingPeriodId - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"paymentTermId","in":"query","required":false,"description":"Filtr po polu paymentTermId - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"invoiceTypeId","in":"query","required":false,"description":"Filtr po polu invoiceTypeId - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"salesUserId","in":"query","required":false,"description":"Filtr po polu salesUserId - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"ownerUserId","in":"query","required":false,"description":"Filtr po polu ownerUserId - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"place","in":"query","required":false,"description":"Filtr po polu place - dopasowanie częściowe (zawiera).","schema":{"type":"string"}},{"name":"note","in":"query","required":false,"description":"Filtr po polu note - dopasowanie częściowe (zawiera).","schema":{"type":"string"}},{"name":"sort","in":"query","description":"Pole sortowania: id, customName, salesDate, createdAt.","required":false,"schema":{"type":"string","default":"createdAt"}},{"name":"sortDir","in":"query","required":false,"schema":{"type":"string","default":"asc","enum":["asc","desc"]}},{"name":"page","in":"query","description":"Strona wyników, od 1.","required":false,"schema":{"type":"integer","default":1,"maximum":1000000,"minimum":1}},{"name":"limit","in":"query","description":"Rozmiar strony (max 1000).","required":false,"schema":{"type":"integer","default":100,"maximum":1000,"minimum":1}}],"responses":{"200":{"description":"Lista usług + metadane stronicowania","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Service"}},"pagination":{"$ref":"#/components/schemas/Pagination"}},"type":"object"}}}},"400":{"description":"Błąd walidacji zapytania - lista błędów w kontrakcie {field, code, message}","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"post":{"tags":["Services"],"summary":"Nowa usługa na kontrahencie","description":"Tworzy usługę na kontrahencie tą samą ścieżką co aplikacja Tillio.\nWymagane: `catalogId` (typ usługi z GET /v2/service/catalog - z niego\nCRM bierze defaulty: cennik, okres rozliczeniowy, warunki umowy)\ni `contractorId`. Opcjonalne: customName (brak = nazwa typu), note, place,\nsalesDate, payValue+currency, costValue, payDay, salesUserId\n(sprzedawca; default = użytkownik klucza), ownerUserId (prowadzący),\ncustomField (pola przypisane do TYPÓW usług - nieprzypisane\ndo typu dostają 422).","operationId":"createService","requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["catalogId","contractorId"],"properties":{"catalogId":{"description":"Typ usługi z GET /v2/service/catalog","type":"integer","example":3},"contractorId":{"type":"integer","example":121},"customName":{"type":"string"},"note":{"type":"string"},"place":{"description":"Miejsce świadczenia usługi","type":"string"},"salesDate":{"description":"Data sprzedaży","type":"string","format":"date"},"payValue":{"type":"string","example":"299.00"},"currency":{"type":"string","example":"PLN"},"costValue":{"type":"string","example":"100.00"},"payDay":{"description":"Dzień miesiąca, w którym wypada płatność","type":"integer","example":10},"salesUserId":{"description":"Sprzedawca (kto sprzedał); default = użytkownik klucza API","type":"integer"},"ownerUserId":{"description":"Prowadzący / odpowiedzialny za usługę","type":"integer"},"agreementDate":{"description":"UMOWA (dostępne od wersji API 1.23.0): data zawarcia. Pola umowy przyjmuje wyłącznie usługa z pozycji katalogu oznaczonej jako umowa (`isAgreement: true` w GET /v2/service/catalog) - w przeciwnym razie 422 `service.notAgreementCatalogItem`.","type":"string","format":"date","example":"2024-01-02"},"agreementFrom":{"description":"UMOWA: obowiązywanie od","type":"string","format":"date","example":"2024-01-01"},"agreementTo":{"description":"UMOWA: obowiązywanie do (nie może być wcześniejsze niż agreementFrom)","type":"string","format":"date","example":"2024-12-31"},"agreementEnd":{"description":"UMOWA: data zakończenia","type":"string","format":"date"},"agreementTermination":{"description":"UMOWA: data wypowiedzenia","type":"string","format":"date"},"createdAt":{"description":"IMPORT DANYCH: data utworzenia rekordu (ISO 8601, strefa instancji). Pozwala zachować chronologię przy wciąganiu historii sprzed lat - bez niej rekord dostaje datę importu. Tylko przy tworzeniu: przy aktualizacji zwracamy 422 (pomyłkę w imporcie poprawia się kasując rekord i importując ponownie). Data z przyszłości = 422. Dostępne od wersji API 1.16.0.","type":"string","format":"date-time","example":"2023-05-17T09:30:00"},"creatorUserId":{"description":"IMPORT DANYCH: użytkownik, który ma figurować jako twórca rekordu (GET /v2/users). Brak = użytkownik przypisany do klucza API. Zapis wykonuje się w kontekście tej osoby, więc obowiązują JEJ uprawnienia. Dostępne od wersji API 1.16.0.","type":"integer","example":42},"customField":{"type":"object","additionalProperties":{"nullable":true}}},"type":"object"}}}},"responses":{"201":{"description":"Usługa utworzona","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/Service"},"info":{"$ref":"#/components/schemas/WriteInfo"}},"type":"object"}}}},"422":{"description":"Błędy walidacji - lista {field, code, message}","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień do usług","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/services/{id}":{"get":{"tags":["Services"],"summary":"Pojedyncza usługa po id","description":"Pełne dane usługi wraz z datami umowy i polami niestandardowymi.","operationId":"getService","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":412}}],"responses":{"200":{"description":"Usługa","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/Service"}},"type":"object"}}}},"404":{"description":"Brak usługi o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"put":{"tags":["Services"],"summary":"Aktualizacja usługi (partial)","description":"Częściowa aktualizacja usługi - wysyłasz TYLKO pola do zmiany. `catalogId` nie\npodlega edycji (typ usługi definiuje warunki) - 422 body.fieldNotUpdatable.\n\nDaty umowy (`agreementFrom`, `agreementTo`, `agreementDate`, `agreementEnd`,\n`agreementTermination`) żyją w osobnej tabeli obok usługi. Przy częściowej\nzmianie porównujemy je z zapisanym stanem: `agreementTo` wcześniejsze niż\nzapisane `agreementFrom` (albo odwrotnie) = 422 body.invalidValue przed zapisem;\n`null` czyści datę. Zmiana SAMYCH dat umowy nie odświeża `updatedAt` usługi,\nwięc filtr `updatedAfter` na GET /v2/services takiej zmiany nie wychwyci.","operationId":"updateService","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":77}}],"requestBody":{"description":"Aktualizacja CZĘŚCIOWA - wysyłasz tylko pola do zmiany. Poza listą poniżej zapis przyjmuje `customField`. Pola ustawiane WYŁĄCZNIE przy tworzeniu (próba zmiany = 422 `body.fieldNotUpdatable`): catalogId.","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"contractorId":{"type":"integer","description":"Wartość musi istnieć w słowniku CRM."},"customName":{"type":"string"},"note":{"type":"string"},"place":{"type":"string"},"salesDate":{"type":"string","format":"date"},"payValue":{"type":"string"},"currency":{"type":"string"},"costValue":{"type":"string"},"payDay":{"type":"integer"},"salesUserId":{"type":"integer"},"ownerUserId":{"type":"integer"},"agreementDate":{"type":"string","format":"date","description":"Umowa - przyjmuje ją wyłącznie usługa z pozycji katalogu oznaczonej jako umowa (`isAgreement` w GET /v2/service/catalog)."},"agreementFrom":{"type":"string","format":"date","description":"Umowa - przyjmuje ją wyłącznie usługa z pozycji katalogu oznaczonej jako umowa (`isAgreement` w GET /v2/service/catalog)."},"agreementTo":{"type":"string","format":"date","description":"Umowa - przyjmuje ją wyłącznie usługa z pozycji katalogu oznaczonej jako umowa (`isAgreement` w GET /v2/service/catalog)."},"agreementEnd":{"type":"string","format":"date","description":"Umowa - przyjmuje ją wyłącznie usługa z pozycji katalogu oznaczonej jako umowa (`isAgreement` w GET /v2/service/catalog)."},"agreementTermination":{"type":"string","format":"date","description":"Umowa - przyjmuje ją wyłącznie usługa z pozycji katalogu oznaczonej jako umowa (`isAgreement` w GET /v2/service/catalog)."},"customField":{"type":"object","description":"Pola niestandardowe: klucz → wartość (klucze i typy: GET /v2/<encja>/custom-fields). Pola wielowartościowe (multiselect, produkty, szanse sprzedaży) przyjmują listę wartości, np. [\"12\",\"15\"] - pojedyncza wartość jest równoważna liście jednoelementowej. Lista dostępna od wersji API 2.9.0.","additionalProperties":{"nullable":true}}}}}}},"responses":{"200":{"description":"Usługa po aktualizacji","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/Service"},"info":{"$ref":"#/components/schemas/WriteInfo"}},"type":"object"}}}},"404":{"description":"Brak usługi o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Błędy walidacji","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień do usług","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/stocks":{"get":{"tags":["Stocks"],"summary":"Stany magazynowe (magazyn + produkt + ilość)","description":"Stany magazynowe: jedna pozycja = jedna para magazyn+produkt (ewentualne\nzduplikowane wpisy w CRM są sumowane do jednego stanu). Tabela stanów nie ma\nznaczników czasu, więc końcówka nie oferuje syncu przyrostowego (`updatedAfter`) -\nstany synchronizuje się pełnym przebiegiem per magazyn.\n\n**Przykład - pełny sync stanów magazynu do ERP:**\n```\nGET /v2/warehouses                        -> mapowanie magazynów po symbol\nGET /v2/stocks?warehouseId=1&page=1&limit=1000\nGET /v2/stocks?warehouseId=1&page=2&limit=1000   (aż pagination.pages)\n```\nStabilna kolejność stron: sortowanie zawsze domyka się parą (warehouseId, productId),\nwięc przebieg stronami jest deterministyczny.\n\nStan pojedynczego produktu we wszystkich magazynach: `GET /v2/stocks?productId=307`.\nProdukty bez wpisu w magazynie nie występują w odpowiedzi (brak wpisu = stan nieprowadzony,\nNIE stan zerowy).","operationId":"listStocks","parameters":[{"name":"warehouseId","in":"query","description":"Filtr po magazynie - dopasowanie dokładne (id z GET /v2/warehouses).","required":false,"schema":{"type":"integer","example":1}},{"name":"productId","in":"query","description":"Filtr po produkcie - dopasowanie dokładne.","required":false,"schema":{"type":"integer","example":235}},{"name":"sort","in":"query","description":"Pole sortowania: warehouseId, productId, quantity.","required":false,"schema":{"type":"string","default":"warehouseId"}},{"name":"sortDir","in":"query","required":false,"schema":{"type":"string","default":"asc","enum":["asc","desc"]}},{"name":"page","in":"query","description":"Strona wyników, od 1.","required":false,"schema":{"type":"integer","default":1,"maximum":1000000,"minimum":1}},{"name":"limit","in":"query","description":"Rozmiar strony (max 1000).","required":false,"schema":{"type":"integer","default":100,"maximum":1000,"minimum":1}}],"responses":{"200":{"description":"Lista stanów magazynowych + metadane stronicowania","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Stock"}},"pagination":{"$ref":"#/components/schemas/Pagination"}},"type":"object"}}}},"400":{"description":"Błąd walidacji zapytania - lista błędów w kontrakcie {field, code, message}","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/warehouses/{warehouseId}/stocks/{productId}":{"put":{"tags":["Stocks"],"summary":"Ustawienie/korekta stanu magazynowego","description":"Zapis stanu magazynowego produktu w magazynie - przez publiczne metody\nmagazynowe CRM. Dokładnie jedno z:\n- `quantity` - ustawia stan bezwzględnie (inwentaryzacja),\n- `adjustBy` - koryguje stan o wartość, może być ujemna\n  (przyjęcie/wydanie).\n\n**`adjustBy` nie jest operacją atomową w CRM:** korekta to odczyt sumy\ni zapis PEŁNEJ ilości. Równoległe korekty tej samej pary produkt/magazyn\nmogą się nadpisać (dwie korekty -2 i -3 od stanu 10 mogą dać 7 zamiast 5) -\nintegracja serializuje korekty tej samej pary po swojej stronie do czasu\npoprawki CRM.\n\nBrak wpisu stanu dla pary produkt×magazyn = stan 0 - zapis zakłada\nwpis automatycznie (spójnie z odczytem GET /v2/stocks). Gdy CRM nie wykona\nzapisu, odpowiedź to 422 `stock.saveFailed` (odmowa uprawnień: 403) - nigdy\n200 z poprzednią ilością (do wersji API 2.13.0 wynik zapisu nie był sprawdzany).","operationId":"setStock","parameters":[{"name":"warehouseId","in":"path","required":true,"schema":{"type":"integer","example":1}},{"name":"productId","in":"path","required":true,"schema":{"type":"integer","example":235}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"properties":{"quantity":{"description":"Ustaw stan bezwzględnie (nieujemna, kropka dziesiętna)","type":"string","example":"120.5"},"adjustBy":{"description":"ALBO koryguj o wartość (może być ujemna)","type":"string","example":"-3"}},"type":"object"}}}},"responses":{"200":{"description":"Stan po zapisie","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/Stock"}},"type":"object"}}}},"404":{"description":"Brak magazynu albo produktu o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Błędy walidacji albo CRM nie wykonał zapisu (stock.saveFailed)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień do magazynów albo odmowa zapisu przez CRM","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/tasks/{id}/comments":{"get":{"tags":["Tasks"],"summary":"Komentarze zadania (chronologicznie)","description":"Pełny wątek komentarzy zadania, chronologicznie (najstarszy pierwszy) -\ndo podglądu historii ustaleń w integracjach i synchronizacji z zewnętrznymi\nnarzędziami (helpdesk, raporty).\n\n**Bez paginacji** - komentarzy per zadanie jest mało (wątek pod zadaniem\nw CRM), zawsze dostajesz komplet. Odpowiedzi w wątku rozpoznasz po\n`parentCommentId`. Załączniki dodane w komentarzu znajdziesz w\nGET /v2/tasks/{id}/attachments po polu `commentId`.\n\nPrzykład:\n```\nGET /v2/tasks/15321/comments\n```","operationId":"listTaskComments","parameters":[{"name":"id","in":"path","description":"Id zadania (GET /v2/tasks).","required":true,"schema":{"type":"integer","example":22}}],"responses":{"200":{"description":"Komentarze zadania, chronologicznie; zadanie bez komentarzy = pusta lista","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/TaskComment"}}},"type":"object"}}}},"404":{"description":"Brak zadania o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"post":{"tags":["Tasks"],"summary":"Nowy komentarz zadania","description":"Dopisuje komentarz do zadania - tą samą drogą co panel, więc działają\npowiadomienia obserwujących wątek i wzmianek (`@` w treści), a wpis trafia\ndo historii zadania.\n\n`body` to treść; przyjmuje HTML z edytora CRM (`<p>`, `<b>`, listy, linki) -\nCRM czyści go z niebezpiecznych znaczników. Wzmianka użytkownika to\n`<span data-user-id=\"7\">@Jan</span>`: taki zapis wysyła powiadomienie\ndo wskazanej osoby.\n\n`parentCommentId` robi z komentarza odpowiedź w wątku - musi wskazywać\nkomentarz TEGO zadania.\n\nZałącznik dokłada się drugim krokiem: POST /v2/tasks/{id}/attachments\nz polem `commentId` z odpowiedzi.\n\nDostęp wg reguł CRM dla użytkownika klucza API (właściciel, wykonawca,\nlider, projekt, kontrahent) - brak dostępu = 403.\nDostępne od wersji API 2.7.0.","operationId":"createTaskComment","parameters":[{"name":"id","in":"path","description":"Id zadania (GET /v2/tasks).","required":true,"schema":{"type":"integer","example":22}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["body"],"properties":{"body":{"description":"Treść komentarza (HTML dozwolony)","type":"string","example":"<p>Klient prosi o korektę oferty.</p>"},"parentCommentId":{"description":"Komentarz, na który odpowiadasz (z GET /v2/tasks/{id}/comments); pominięty = komentarz główny","type":"integer","nullable":true}},"type":"object"}}}},"responses":{"201":{"description":"Komentarz dodany","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/TaskComment"},"info":{"$ref":"#/components/schemas/WriteInfo"}},"type":"object"}}}},"404":{"description":"Brak zadania albo komentarza nadrzędnego o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Błędy walidacji (brak treści, nieznane pole)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Użytkownik klucza API bez dostępu do zadania","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/tasks/{id}/attachments":{"get":{"tags":["Tasks"],"summary":"Załączniki zadania z linkami do pobrania","description":"Pełna lista załączników zadania, chronologicznie (najstarszy pierwszy) -\nmetadane plików plus tymczasowy link do pobrania.\n\n**Pobieranie pliku:** pole `downloadUrl` to podpisany link prosto do\nGoogle Cloud Storage, ważny **1 minutę** od wygenerowania - pobierz plik\nod razu, a po wygaśnięciu odśwież listę. Gdy instancja nie trzyma plików\nw GCS albo środowisko nie ma kluczy podpisu, `downloadUrl` = null -\nzostają metadane i `storagePath` jako stały identyfikator pliku\n(endpoint binarny w kolejnej iteracji).\n\n**Bez paginacji** - załączników per zadanie jest mało, zawsze dostajesz\nkomplet. Zadania cykliczne współdzielą załączniki zadania-matki -\nlista jest wtedy wspólna dla całej serii (zachowanie CRM). Załącznik\ndodany w komentarzu ma ustawione `commentId`.\n\nPrzykład:\n```\nGET /v2/tasks/15321/attachments\n```","operationId":"listTaskAttachments","parameters":[{"name":"id","in":"path","description":"Id zadania (GET /v2/tasks).","required":true,"schema":{"type":"integer","example":22}}],"responses":{"200":{"description":"Załączniki zadania, chronologicznie; zadanie bez załączników = pusta lista","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/TaskAttachment"}}},"type":"object"}}}},"404":{"description":"Brak zadania o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"post":{"tags":["Tasks"],"summary":"Upload załącznika zadania albo komentarza (multipart)","description":"Dodaje załącznik do zadania - request **multipart/form-data** z plikiem\nw polu `file` (limit produktowy 128 MB; core dodatkowo waliduje rozmiar).\nPlik ląduje w prywatnej przestrzeni plików instancji (GCS), a dodanie\ntrafia do historii zadania - ta sama ścieżka co aplikacja Tillio.\n\nDostęp wg reguł CRM dla użytkownika klucza API (właściciel, wykonawca,\nlider, dostęp przez kontrahenta) - brak dostępu = 403.\n\n**Plik w komentarzu:** podaj `commentId` (z POST /v2/tasks/{id}/comments) -\nzałącznik zawiśnie pod tym komentarzem, jak przy dodawaniu pliku do wpisu\nw wątku w panelu. Bez `commentId` plik ląduje wprost na zadaniu.\nDostępne od wersji API 2.7.0.\n\nOdpowiedź: rekord załącznika jak w GET /v2/tasks/{id}/attachments,\nz gotowym `downloadUrl`.","operationId":"uploadTaskAttachment","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":22}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"required":["file"],"properties":{"file":{"description":"Plik do załączenia","type":"string","format":"binary"},"commentId":{"description":"Komentarz, pod którym ma zawisnąć plik (GET /v2/tasks/{id}/comments); pominięty = załącznik zadania","type":"integer","nullable":true}},"type":"object"}}}},"responses":{"201":{"description":"Załącznik dodany","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/TaskAttachment"}},"type":"object"}}}},"404":{"description":"Brak zadania o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Brak pliku / plik odrzucony przez CRM","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Użytkownik klucza API bez dostępu do zadania","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/tasks/{id}/comments/{commentId}":{"put":{"tags":["Tasks"],"summary":"Zmiana treści komentarza zadania","description":"Zmienia treść komentarza. Poprzednia wersja zostaje w historii komentarza\n(CRM pokazuje ją pod wpisem), a `updatedAt` i `editorUserId` w odpowiedzi\nmówią, kiedy i kto edytował.\n\nCRM pozwala edytować **własny** komentarz; cudzy wymaga uprawnienia\n`task.canEditAllComments` po stronie użytkownika klucza API - inaczej 403.\nKomentarza w zadaniu zarchiwizowanym nie da się zmienić.\nDostępne od wersji API 2.7.0.","operationId":"updateTaskComment","parameters":[{"name":"id","in":"path","description":"Id zadania (GET /v2/tasks).","required":true,"schema":{"type":"integer","example":22}},{"name":"commentId","in":"path","description":"Id komentarza (GET /v2/tasks/{id}/comments).","required":true,"schema":{"type":"integer","example":679}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["body"],"properties":{"body":{"description":"Nowa treść komentarza (HTML dozwolony)","type":"string","example":"<p>Klient prosi o korektę oferty - termin do piątku.</p>"}},"type":"object"}}}},"responses":{"200":{"description":"Komentarz po zmianie","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/TaskComment"},"info":{"$ref":"#/components/schemas/WriteInfo"}},"type":"object"}}}},"404":{"description":"Brak zadania albo komentarza o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Błędy walidacji (brak treści, nieznane pole)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Cudzy komentarz bez uprawnienia task.canEditAllComments albo zadanie zarchiwizowane","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/task/templates":{"post":{"tags":["TaskTemplates"],"summary":"Nowy szablon zadania","description":"Tworzy szablon zadania - gotowiec (temat, treść, wykonawcy, tagi, czas i termin),\nz którego użytkownik CRM tworzy zadanie jednym kliknięciem.\n\n- `alias` to skrót do szybkiego wstawiania szablonu w CRM; system normalizuje go sam\n  (małe litery, spacje na `_`, prefiks `!`) i pilnuje unikalności - zajęty alias = 422\n  `body.duplicateValue`. Brak aliasu jest OK.\n- `dueInDays` to termin wykonania zadania liczony w DNIACH od utworzenia zadania\n  z szablonu, `eta` - szacowany czas pracy w MINUTACH (formularz CRM podpowiada\n  wartości 5-480).\n- `taskPriority` to priorytet tworzonego zadania (0 = standard, 1 = wysoki,\n  2 = najwyższy), a `priority` - priorytet samego szablonu na liście (sortowanie,\n  wyższy = wyżej).\n- `acl` ogranicza, kto widzi szablon; brak pola = szablon widoczny dla wszystkich\n  z dostępem do modułu zadań.\n\nWymaga uprawnienia administratora szablonów zadań (`task.templateAdmin`) - jak\nw panelu CRM; klucz API bez niego dostaje 403.\n\nDostępne od wersji API 2.4.0.","operationId":"createTaskTemplate","requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["name"],"properties":{"categoryId":{"description":"Kategoria szablonu (drzewo kategorii z konfiguracji CRM). Brak = szablon na najwyższym poziomie.","type":"integer","example":7,"nullable":true},"name":{"description":"Nazwa szablonu widoczna na liście (max 255 znaków).","type":"string","example":"Follow-up po spotkaniu"},"alias":{"description":"Skrót do szybkiego wstawienia szablonu - z prefiksem `!` lub bez (system znormalizuje). Unikalny w instancji, max 31 znaków.","type":"string","example":"followup","nullable":true},"title":{"description":"Temat zadania tworzonego z szablonu (max 255 znaków).","type":"string","example":"Zadzwonić do klienta po spotkaniu","nullable":true},"body":{"description":"Treść zadania - HTML z WYSIWYG.","type":"string","example":"<p>Podsumować ustalenia i wysłać ofertę.</p>","nullable":true},"assignedUserIds":{"description":"Domyślni wykonawcy zadania (GET /v2/users).","type":"array","items":{"type":"integer"},"example":[12345]},"tagIds":{"description":"Domyślne tagi zadania (GET /v2/task/tags).","type":"array","items":{"type":"integer"},"example":[3]},"eta":{"description":"Szacowany czas wykonania w minutach.","type":"integer","example":30},"dueInDays":{"description":"Termin wykonania w dniach od utworzenia zadania z szablonu.","type":"integer","example":3},"taskPriority":{"description":"Priorytet tworzonego zadania: 0 = standard, 1 = wysoki, 2 = najwyższy.","type":"integer","example":0},"acl":{"description":"Ograniczenie widoczności szablonu - obiekt {userIds?, departmentIds?, groupIds?} z listami id. Brak/pusty = widoczny dla wszystkich.","properties":{"userIds":{"type":"array","items":{"type":"integer"},"example":[12345]},"departmentIds":{"type":"array","items":{"type":"integer"},"example":[2]},"groupIds":{"type":"array","items":{"type":"integer"},"example":[]}},"type":"object","nullable":true},"priority":{"description":"Priorytet szablonu na liście (sortowanie, wyższy = wyżej).","type":"integer","example":0},"active":{"description":"false = szablon nieaktywny (widoczny tylko dla adminów szablonów).","type":"boolean","default":true}},"type":"object"}}}},"responses":{"201":{"description":"Szablon utworzony","content":{"application/json":{"schema":{"properties":{"data":{"properties":{"id":{"type":"integer","example":456},"categoryId":{"type":"integer","example":7,"nullable":true},"name":{"type":"string","example":"Follow-up po spotkaniu"},"alias":{"description":"Alias po normalizacji (z prefiksem `!`).","type":"string","example":"!followup","nullable":true},"title":{"type":"string","example":"Zadzwonić do klienta po spotkaniu","nullable":true},"body":{"type":"string","example":"<p>Podsumować ustalenia i wysłać ofertę.</p>","nullable":true},"assignedUserIds":{"type":"array","items":{"type":"integer"},"example":[12345]},"tagIds":{"type":"array","items":{"type":"integer"},"example":[3]},"eta":{"type":"integer","example":30},"dueInDays":{"type":"integer","example":3},"taskPriority":{"type":"integer","example":0},"acl":{"description":"Ograniczenie widoczności ({userIds, departmentIds, groupIds}) albo null = bez ograniczeń.","type":"object","example":null,"nullable":true},"priority":{"type":"integer","example":0},"active":{"type":"boolean","example":true}},"type":"object"},"info":{"$ref":"#/components/schemas/WriteInfo"}},"type":"object"}}}},"422":{"description":"Błędy walidacji (w tym zajęty alias) - lista {field, code, message}","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnienia task.templateAdmin","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/task/template/categories":{"get":{"tags":["TaskTemplates"],"summary":"Kategorie szablonów zadań","description":"Słownik kategorii, w które CRM grupuje szablony zadań - stąd bierzesz\n`categoryId` do POST /v2/task/templates. Lista jest PŁASKA: każda pozycja\nniesie `parentId` (null = najwyższy poziom), drzewo budujesz po stronie\nklienta. Sortowanie: `priority` malejąco, potem `name` rosnąco - jak\npozycje jednego poziomu drzewa w panelu CRM.\n\nDostępne od wersji API 2.4.0.","operationId":"listTaskTemplateCategories","responses":{"200":{"description":"Płaska lista kategorii (drzewo przez parentId)","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"properties":{"id":{"type":"integer","example":7},"name":{"type":"string","example":"Sprzedaż"},"parentId":{"description":"Kategoria nadrzędna; null = najwyższy poziom.","type":"integer","example":null,"nullable":true},"priority":{"description":"Priorytet na liście (wyższy = wyżej w swoim poziomie drzewa).","type":"integer","example":0}},"type":"object"}}},"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"post":{"tags":["TaskTemplates"],"summary":"Nowa kategoria szablonów zadań","description":"Zakłada kategorię szablonów zadań - od razu użyteczną jako `categoryId`\nw POST /v2/task/templates. `parentId` zagnieżdża kategorię w istniejącej\n(null albo brak pola = najwyższy poziom drzewa).\n\nWymaga uprawnienia administratora szablonów zadań (`task.templateAdmin`) -\njak w panelu CRM; klucz API bez niego dostaje 403.\n\nDostępne od wersji API 2.4.0.","operationId":"createTaskTemplateCategory","requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["name"],"properties":{"name":{"description":"Nazwa kategorii (max 128 znaków).","type":"string","example":"Obsługa klienta"},"parentId":{"description":"Kategoria nadrzędna (id z GET /v2/task/template/categories); null/brak = najwyższy poziom.","type":"integer","example":7,"nullable":true},"priority":{"description":"Priorytet na liście (wyższy = wyżej w swoim poziomie drzewa).","type":"integer","default":0}},"type":"object"}}}},"responses":{"201":{"description":"Kategoria utworzona","content":{"application/json":{"schema":{"properties":{"data":{"properties":{"id":{"type":"integer","example":12},"name":{"type":"string","example":"Obsługa klienta"},"parentId":{"type":"integer","example":7,"nullable":true},"priority":{"type":"integer","example":0}},"type":"object"},"info":{"$ref":"#/components/schemas/WriteInfo"}},"type":"object"}}}},"422":{"description":"Błędy walidacji (brak nazwy, nieistniejący parentId) - lista {field, code, message}","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnienia task.templateAdmin","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/task/template/categories/{id}":{"put":{"tags":["TaskTemplates"],"summary":"Aktualizacja kategorii szablonów zadań","description":"Częściowa aktualizacja kategorii - wysyłasz tylko pola do zmiany\n(`name`, `parentId`, `priority`). `parentId: null` przenosi kategorię\nna najwyższy poziom; kategoria nie może zostać swoim własnym przodkiem\n(cykl = 422).\n\nWymaga uprawnienia administratora szablonów zadań (`task.templateAdmin`) -\njak w panelu CRM; klucz API bez niego dostaje 403.\n\nDostępne od wersji API 2.4.0.","operationId":"updateTaskTemplateCategory","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":12}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"properties":{"name":{"description":"Nowa nazwa (max 128 znaków).","type":"string","example":"Obsługa posprzedażowa"},"parentId":{"description":"Nowa kategoria nadrzędna; null = najwyższy poziom.","type":"integer","example":null,"nullable":true},"priority":{"description":"Priorytet na liście (wyższy = wyżej w swoim poziomie drzewa).","type":"integer","example":5}},"type":"object"}}}},"responses":{"200":{"description":"Kategoria po aktualizacji","content":{"application/json":{"schema":{"properties":{"data":{"properties":{"id":{"type":"integer","example":12},"name":{"type":"string","example":"Obsługa posprzedażowa"},"parentId":{"type":"integer","example":null,"nullable":true},"priority":{"type":"integer","example":5}},"type":"object"}},"type":"object"}}}},"404":{"description":"Brak kategorii o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Błędy walidacji (pusty update, nieistniejący parentId, cykl rodzica) - lista {field, code, message}","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnienia task.templateAdmin","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/tasks":{"get":{"tags":["Tasks"],"summary":"Lista zadań z filtrowaniem","description":"Zadania z całego systemu (w projektach i poza nimi) - dla integracji raportowych,\nsynchronizacji z zewnętrznymi narzędziami i automatyzacji.\n\n**Synchronizacja przyrostowa:** przekaż `updatedAfter` z datą ostatniego udanego\nsyncu - dostaniesz rekordy utworzone lub zmienione po tej chwili. Zmiany zadań\nśledzimy dziennikiem zmian CRM, więc łapią się też np. zmiany statusu i przypisań.\n\n**Wykonanie:** `done=false` - niewykonane, `done=true` - wykonane (uproszczony\nstatus całego zadania, wyliczany z daty wykonania `endDate`; szczegółowe statusy\nper wykonawca w GET /v2/task/statuses).\n\nPrzykład - niewykonane zadania kontrahenta przypisane do użytkownika 7:\n```\nGET /v2/tasks?contractorId=121&assignedUserId=7&done=false\n```","operationId":"listTasks","parameters":[{"name":"contractorId","in":"query","description":"Zadania powiązane z kontrahentem.","required":false,"schema":{"type":"integer","example":121}},{"name":"projectId","in":"query","description":"Zadania należące do projektu (id z GET /v2/projects).","required":false,"schema":{"type":"integer","example":27}},{"name":"pipelineItemId","in":"query","description":"Zadania powiązane z szansą sprzedaży (id z GET /v2/pipeline/items). Dostępne od wersji API 2.13.0.","required":false,"schema":{"type":"integer","example":2716}},{"name":"assignedUserId","in":"query","description":"Zadania, do których przypisany jest użytkownik (wykonawca).","required":false,"schema":{"type":"integer","example":7}},{"name":"done","in":"query","description":"false = niewykonane, true = wykonane.","required":false,"schema":{"type":"boolean"}},{"name":"updatedAfter","in":"query","description":"Tylko zadania utworzone/zmienione PO tej chwili (ISO 8601). Fundament syncu przyrostowego.","required":false,"schema":{"type":"string","format":"date-time","example":"2026-08-01T00:00:00Z"}},{"name":"updatedBefore","in":"query","description":"Górna granica zakresu zmian (ISO 8601, włącznie).","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"createdAfter","in":"query","description":"Tylko zadania utworzone po tej chwili (ISO 8601).","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"createdBefore","in":"query","description":"Tylko zadania utworzone do tej chwili (ISO 8601, włącznie).","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"customField","in":"query","description":"Filtry pól niestandardowych: `customField[<klucz>]=<wartość>` (dokładne; klucze w GET /v2/task/custom-fields). Pusta wartość = pole nieustawione.","required":false,"style":"deepObject","explode":true,"schema":{"type":"object","additionalProperties":{"type":"string"}}},{"name":"title","in":"query","description":"Filtr częściowy (zawiera). Analogicznie: description.","required":false,"schema":{"type":"string"}},{"name":"archived","in":"query","description":"false = tylko aktywne, true = tylko zarchiwizowane. Dokładne także: id, taskTypeId, priority, leadId, pipelineItemId, ownerUserId, creatorUserId.","required":false,"schema":{"type":"boolean"}},{"name":"id","in":"query","required":false,"description":"Filtr po polu id - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"description","in":"query","required":false,"description":"Filtr po polu description - dopasowanie częściowe (zawiera).","schema":{"type":"string"}},{"name":"taskTypeId","in":"query","required":false,"description":"Filtr po polu taskTypeId - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"priority","in":"query","required":false,"description":"Filtr po polu priority - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"leadId","in":"query","required":false,"description":"Filtr po polu leadId - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"ownerUserId","in":"query","required":false,"description":"Filtr po polu ownerUserId - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"creatorUserId","in":"query","required":false,"description":"Filtr po polu creatorUserId - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"contactId","in":"query","required":false,"description":"Filtr po polu contactId - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"sort","in":"query","description":"Pole sortowania: id, title, startDate, dueDate, endDate, createdAt.","required":false,"schema":{"type":"string","default":"id"}},{"name":"sortDir","in":"query","required":false,"schema":{"type":"string","default":"asc","enum":["asc","desc"]}},{"name":"page","in":"query","description":"Strona wyników, od 1.","required":false,"schema":{"type":"integer","default":1,"maximum":1000000,"minimum":1}},{"name":"limit","in":"query","description":"Rozmiar strony (max 1000).","required":false,"schema":{"type":"integer","default":100,"maximum":1000,"minimum":1}}],"responses":{"200":{"description":"Lista zadań + metadane stronicowania","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Task"}},"pagination":{"$ref":"#/components/schemas/Pagination"}},"type":"object"}}}},"400":{"description":"Błąd walidacji zapytania - lista błędów w kontrakcie {field, code, message}","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"post":{"tags":["Tasks"],"summary":"Nowe zadanie","description":"Tworzy zadanie tą samą ścieżką co aplikacja Tillio (zapis idzie przez\nmodel przypisania wykonawcy - stąd wymagane `assignedUserIds`).\nPola: title (wymagane), assignedUserIds (wymagane - wykonawca zadania; lista z jedną osobą),\ndescription (HTML), priority, ownerUserId (default = użytkownik klucza),\ncontractorId, pipelineItemId, leadId, startDate+dueDate (para - brak\ndueDate = zadanie \"bez terminu\"), taskStatusId (status WYKONAWCY ze\nsłownika GET /v2/task/statuses), customField (klucze w GET /v2/task/custom-fields).\n\n**Uwaga na `done`:** to pole jest wyłącznie do ODCZYTU (wyliczane,\n2 = wykonane - wyliczane z daty wykonania). Przy zapisie użyj\n`taskStatusId` (status per wykonawca) albo `endDate` (wykonanie zadania);\nwysłanie `done` kończy się 422 `body.fieldNotWritable`.","operationId":"createTask","requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["title","assignedUserIds"],"properties":{"title":{"type":"string","example":"Przygotować ofertę dla ACME"},"description":{"description":"Treść zadania w HTML (CRM czyści niebezpieczne znaczniki).","type":"string","example":"<p>Wysłać ofertę do końca tygodnia</p>"},"priority":{"description":"0 = standard, 1 = wysoki, 2 = najwyższy.","type":"integer","example":0},"ownerUserId":{"description":"Właściciel zadania (GET /v2/users). Brak = użytkownik przypisany do klucza API.","type":"integer","example":7},"assignedUserIds":{"description":"Wykonawca zadania - lista z JEDNĄ osobą (kolejnych wykonawców dopisuje się w panelu)","type":"array","items":{"type":"integer"},"example":[1]},"contractorId":{"description":"Kontrahent zadania (GET /v2/contractors). Razem z pipelineItemId musi być kontrahentem tej szansy - inaczej 422 na pipelineItemId.","type":"integer","example":121},"contactId":{"description":"Kontakt powiązany z zadaniem (GET /v2/contacts). Wymaga nowszej wersji CRM - na starszej instalacji zapis odrzuca pole jako nieznane. Dostępne od wersji API 2.8.0.","type":"integer","example":3921},"pipelineItemId":{"description":"Szansa sprzedaży (GET /v2/pipeline/items). Musi istnieć, a zadanie dostaje kontrahenta szansy: bez contractorId w body przypisujemy go automatycznie (odpowiedź zwraca contractorId szansy), contractorId innego kontrahenta = 422 `body.invalidValue` na tym polu. Dostępne w odczycie i filtrze od wersji API 2.13.0.","type":"integer","example":2716,"nullable":true},"leadId":{"description":"Lead powiązany z zadaniem (GET /v2/leads).","type":"integer","example":815},"startDate":{"description":"Początek zadania (ISO 8601). Para z dueDate: bez dueDate zadanie jest „bez terminu\" i startDate nie jest zapisywany.","type":"string","format":"date-time","example":"2026-08-10T09:00:00+02:00"},"dueDate":{"description":"Termin wykonania (ISO 8601). Sama data bez godziny = koniec dnia.","type":"string","format":"date-time","example":"2026-08-20T17:00:00+02:00"},"taskStatusId":{"description":"Status wykonawcy ze słownika GET /v2/task/statuses (nie mylić z polem done, które jest tylko do odczytu)","type":"integer","nullable":true},"createdAt":{"description":"IMPORT DANYCH: data utworzenia rekordu (ISO 8601, strefa instancji). Pozwala zachować chronologię przy wciąganiu historii sprzed lat - bez niej rekord dostaje datę importu. Tylko przy tworzeniu: przy aktualizacji zwracamy 422 (pomyłkę w imporcie poprawia się kasując rekord i importując ponownie). Data z przyszłości = 422. Dostępne od wersji API 1.16.0.","type":"string","format":"date-time","example":"2023-05-17T09:30:00"},"creatorUserId":{"description":"IMPORT DANYCH: użytkownik, który ma figurować jako twórca rekordu (GET /v2/users). Brak = użytkownik przypisany do klucza API. Zapis wykonuje się w kontekście tej osoby, więc obowiązują JEJ uprawnienia. Dostępne od wersji API 1.16.0.","type":"integer","example":42},"customField":{"type":"object","additionalProperties":{"nullable":true}}},"type":"object"}}}},"responses":{"201":{"description":"Zadanie utworzone","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/Task"},"info":{"$ref":"#/components/schemas/WriteInfo"}},"type":"object"}}}},"422":{"description":"Błędy walidacji - lista {field, code, message}","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień do zadań","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/tasks/{id}":{"get":{"tags":["Tasks"],"summary":"Pojedyncze zadanie po id","description":"Pełne dane zadania wraz z wykonawcami (`assignedUserIds`) i polami niestandardowymi.","operationId":"getTask","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":22}}],"responses":{"200":{"description":"Zadanie","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/Task"}},"type":"object"}}}},"404":{"description":"Brak zadania o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"put":{"tags":["Tasks"],"summary":"Aktualizacja zadania (partial)","description":"Częściowa aktualizacja zadania po jego id - wysyłasz TYLKO pola do zmiany\n(title, description, priority, ownerUserId, contractorId, pipelineItemId,\nleadId, contactId, startDate/dueDate, customField). `assignedUserIds` i `done`\nnie podlegają edycji przez API (statusy per wykonawca to dedykowany\nprzepływ CRM).\n\n**Szansa sprzedaży (`pipelineItemId`):** musi istnieć i należeć do kontrahenta\nzadania (`contractorId` z body, a bez niego - kontrahent zapisany na zadaniu,\ndla zadania projektowego bez własnego kontrahenta: kontrahent projektu);\ninaczej 422 `body.invalidValue` na `pipelineItemId`. Zadanie bez kontrahenta\nwymaga `contractorId` w tym samym żądaniu. Przejście do szansy innego\nkontrahenta = `contractorId` + `pipelineItemId` razem. `pipelineItemId: null`\nodpina szansę. Sama zmiana `contractorId` odpina dotychczasową szansę (CRM).\nDostępne od wersji API 2.13.0.\n\n**Priorytet razem z innymi polami:** `priority` CRM zapisuje osobną operacją,\nwięc żądanie `{priority, title, ...}` to dwa zapisy po stronie API - wszystkie\npola z body lądują w zadaniu. Gdyby drugi zapis nie przeszedł, odpowiedź to 422\n`task.partialUpdate` z listą pól już zapisanych i odrzuconych (zapis częściowy\nnigdy nie jest cichy). Do wersji API 2.13.0 takie żądanie zapisywało SAM priorytet\ni odpowiadało 200.","operationId":"updateTask","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":22}}],"requestBody":{"description":"Aktualizacja CZĘŚCIOWA - wysyłasz tylko pola do zmiany. Poza listą poniżej zapis przyjmuje `customField`. Pola ustawiane WYŁĄCZNIE przy tworzeniu (próba zmiany = 422 `body.fieldNotUpdatable`): assignedUserIds, taskStatusId.","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string","description":"Treść w HTML (CRM czyści niebezpieczne znaczniki)."},"priority":{"type":"integer"},"ownerUserId":{"type":"integer"},"contractorId":{"type":"integer"},"contactId":{"type":"integer"},"pipelineItemId":{"type":"integer","nullable":true,"description":"Null odpina powiązanie."},"leadId":{"type":"integer"},"startDate":{"type":"string","format":"date-time"},"dueDate":{"type":"string","format":"date-time"}}}}}},"responses":{"200":{"description":"Zadanie po aktualizacji","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/Task"},"info":{"$ref":"#/components/schemas/WriteInfo"}},"type":"object"}}}},"404":{"description":"Brak zadania o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Błędy walidacji","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień do zadań","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/text-messages":{"get":{"tags":["Text messages"],"summary":"Lista wiadomości SMS","description":"Wiadomości SMS z tabeli wiadomości CRM - zapisane przez API (`provider:\nTillioCalls`) i zsynchronizowane z integracji VoIP. Kartoteka kontrahenta\ni kontakt pokazują je przez notatki „SMS\" na osi czasu.\n\nTypowe zapytania:\n```\nGET /v2/text-messages?contractorId=121&sort=sentAt&sortDir=desc\nGET /v2/text-messages?direction=outbound&status=failed&sentAfter=2026-09-01T00:00:00Z\n```\n\n**Zakres czasu:** `sentAfter` / `sentBefore` po dacie wysłania. CRM nie\nprzechowuje daty modyfikacji wiadomości - `updatedAfter` nie jest wspierane (400).\nPola niestandardowe nie dotyczą wiadomości (`customField` = 400).\nDostępne od wersji API 2.10.0.","operationId":"listTextMessages","parameters":[{"name":"contractorId","in":"query","description":"Wiadomości przypięte do kontrahenta (po przypięciach, nie po numerze).","required":false,"schema":{"type":"integer","example":121}},{"name":"contactId","in":"query","description":"Wiadomości tej osoby kontaktowej. Analogicznie dokładne: userId, direction (inbound/outbound), status (received/sent/delivered/failed), provider, source, sourceId, ownNumber, remoteNumber (format międzynarodowy). Filtr `body` = dopasowanie częściowe.","required":false,"schema":{"type":"integer","example":3921}},{"name":"sentAfter","in":"query","description":"Tylko wiadomości wysłane/odebrane po tej chwili (ISO 8601). Klucz przyrostowego odczytu - encja nie ma daty modyfikacji.","required":false,"schema":{"type":"string","format":"date-time","example":"2026-09-01T00:00:00Z"}},{"name":"sentBefore","in":"query","description":"Tylko wiadomości do tej chwili (ISO 8601, włącznie).","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"id","in":"query","required":false,"description":"Filtr po polu id - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"provider","in":"query","required":false,"description":"Filtr po polu provider - dopasowanie dokładne.","schema":{"type":"string"}},{"name":"source","in":"query","required":false,"description":"Filtr po polu source - dopasowanie dokładne.","schema":{"type":"string"}},{"name":"sourceId","in":"query","required":false,"description":"Filtr po polu sourceId - dopasowanie dokładne.","schema":{"type":"string"}},{"name":"direction","in":"query","required":false,"description":"Filtr po polu direction - dopasowanie dokładne.","schema":{"type":"string"}},{"name":"status","in":"query","required":false,"description":"Filtr po polu status - dopasowanie dokładne.","schema":{"type":"string"}},{"name":"ownNumber","in":"query","required":false,"description":"Filtr po polu ownNumber - dopasowanie dokładne.","schema":{"type":"string"}},{"name":"remoteNumber","in":"query","required":false,"description":"Filtr po polu remoteNumber - dopasowanie dokładne.","schema":{"type":"string"}},{"name":"body","in":"query","required":false,"description":"Filtr po polu body - dopasowanie częściowe (zawiera).","schema":{"type":"string"}},{"name":"userId","in":"query","required":false,"description":"Filtr po polu userId - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"sort","in":"query","description":"Pole sortowania: id, sentAt.","required":false,"schema":{"type":"string","default":"sentAt"}},{"name":"sortDir","in":"query","required":false,"schema":{"type":"string","default":"asc","enum":["asc","desc"]}},{"name":"page","in":"query","description":"Strona wyników, od 1.","required":false,"schema":{"type":"integer","default":1,"maximum":1000000,"minimum":1}},{"name":"limit","in":"query","description":"Rozmiar strony (max 1000).","required":false,"schema":{"type":"integer","default":100,"maximum":1000,"minimum":1}}],"responses":{"200":{"description":"Lista wiadomości + metadane stronicowania","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/TextMessage"}},"pagination":{"$ref":"#/components/schemas/Pagination"}},"type":"object"}}}},"400":{"description":"Błąd walidacji zapytania - lista błędów w kontrakcie {field, code, message}","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"post":{"tags":["Text messages"],"summary":"Nowa wiadomość SMS (idempotentna po source + sourceId)","description":"Dopisuje SMS do CRM **dokładnie tak, jak robi to integracja VoIP**: rekord\nw tabeli wiadomości, przypięcie do kontrahenta i notatka „SMS\" z treścią\nwiadomości na jego osi czasu - tą samą klasą CRM, której używa panel.\n\n**Idempotencja:** para `source` + `sourceId`. Powtórne wysłanie tej samej pary\nnie tworzy duplikatu - odpowiedź **200** z istniejącym rekordem i\n`info.created: false` (zmiany robi PUT). Sprawdzenie duplikatu idzie PRZED\nwalidacją pozostałych pól (od wersji API 2.12.0), więc powtórka wraca jako 200\ntakże wtedy, gdy niesie dane, których sam zapis by nie przyjął.\n\n**Wymagane:** `source`, `sourceId`, `direction`, `status`, `remoteNumber`, `body`,\n`sentAt` oraz co najmniej jedno z `contactId` / `contractorId`.\n- `direction: inbound` przyjmuje wyłącznie `status: received`; `outbound` -\n  `sent`, `delivered` albo `failed` (doręczenie dopisuje się potem przez PUT).\n- `body` do 1024 znaków (limit CRM) - dłuższa treść to 422 `body.tooLong`,\n  nie ciche ucięcie.\n- `userId` (czyja wiadomość) - bez niego CRM szuka właściciela `ownNumber`\n  w numerach VoIP instancji (linie wszystkich dostawców). Gdy numeru tam nie ma,\n  od wersji API 2.12.0 wiadomość **zapisuje się bez pracownika**, a `ownNumber`\n  wraca w `info.warnings.userId`. 422 zostaje wyłącznie wtedy, gdy nie ma\n  ani `userId`, ani `ownNumber`.\n- `contractorId` → przypięcie + notatka „SMS\" (tytuł nadaje CRM: „SMS od/do\n  <osoba albo numer>\"). Bez `contractorId` rekord powstaje tylko z osobą\n  kontaktową, bez notatki.\n- `creatorUserId` - autor notatki (brak = użytkownik klucza API).\n\nDostawca VoIP „Tillio Calls\" powstaje automatycznie przy pierwszym zapisie.\nWymaga aktywnego modułu VoIP (inaczej 403 `module.notActive`).\nDostępne od wersji API 2.10.0.","operationId":"createTextMessage","requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["source","sourceId","direction","status","remoteNumber","body","sentAt"],"properties":{"source":{"description":"System źródłowy (1-64 znaki: litery, cyfry, kropka, myślnik, podkreślenie).","type":"string","example":"tillio-calls"},"sourceId":{"description":"Identyfikator wiadomości w systemie źródłowym (do 128 znaków).","type":"string","example":"sms_01J8ZK3M9Q"},"direction":{"type":"string","enum":["inbound","outbound"],"example":"inbound"},"status":{"description":"inbound: received; outbound: sent, delivered, failed.","type":"string","enum":["received","sent","delivered","failed"],"example":"received"},"remoteNumber":{"description":"Numer rozmówcy (zapis w formacie międzynarodowym).","type":"string","example":"+48601123456"},"ownNumber":{"description":"Numer własny (linia użytkownika). Bez niego, gdy podano `userId`, bierzemy numer tego użytkownika z ustawień VoIP.","type":"string","example":"+48731427000"},"body":{"description":"Treść wiadomości (do 1024 znaków).","type":"string","example":"Dzień dobry, proszę o kontakt w sprawie oferty."},"sentAt":{"description":"Data wysłania / odebrania (ISO 8601).","type":"string","format":"date-time","example":"2026-09-05T10:15:00+02:00"},"deliveredAt":{"description":"Data doręczenia (ISO 8601) - dla wychodzących ze statusem delivered.","type":"string","format":"date-time"},"userId":{"description":"Użytkownik CRM, którego jest wiadomość (GET /v2/users). Brak = właściciel `ownNumber` z ustawień VoIP.","type":"integer","example":7},"contactId":{"description":"Osoba kontaktowa (GET /v2/contacts).","type":"integer","example":3921},"contractorId":{"description":"Kontrahent, do którego przypinamy wiadomość - powstaje notatka „SMS\".","type":"integer","example":121},"callsUrl":{"description":"Link do wiadomości w systemie źródłowym.","type":"string","format":"uri"},"creatorUserId":{"description":"Autor notatki „SMS\" (GET /v2/users); brak = użytkownik przypisany do klucza API.","type":"integer","example":42}},"type":"object"}}}},"responses":{"201":{"description":"Wiadomość utworzona","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/TextMessage"},"info":{"$ref":"#/components/schemas/WriteInfo"}},"type":"object"}}}},"200":{"description":"Wiadomość o tej parze source + sourceId już istnieje - zwrócony istniejący rekord, info.created = false","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/TextMessage"},"info":{"$ref":"#/components/schemas/WriteInfo"}},"type":"object"}}}},"422":{"description":"Błędy walidacji - lista {field, code, message} (m.in. body.tooLong, status niezgodny z kierunkiem, nieznany kontrahent)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Moduł VoIP nieaktywny na instancji (module.notActive) albo klucz bez prawa do notatek (textMessage.forbidden) - z zapisem wiadomości powstaje notatka na kartotece","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/text-messages/{id}":{"get":{"tags":["Text messages"],"summary":"Pojedyncza wiadomość SMS po id","description":"Pełne dane wiadomości wraz z przypiętymi kontrahentami (`contractorIds`) i notatkami „SMS\" (`noteIds`, `title`). Dostępne od wersji API 2.10.0.","operationId":"getTextMessage","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":512}}],"responses":{"200":{"description":"Wiadomość","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/TextMessage"}},"type":"object"}}}},"404":{"description":"Brak wiadomości o tym id (textMessage.notFound)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"put":{"tags":["Text messages"],"summary":"Aktualizacja wiadomości SMS (partial)","description":"Częściowa aktualizacja - wysyłasz TYLKO pola do zmiany (`status`, `deliveredAt`,\n`contactId`, `contractorId`, `userId`, `callsUrl`). Typowy use-case: potwierdzenie\ndoręczenia wiadomości wychodzącej (`status: delivered` + `deliveredAt`).\n\n- `status` musi pasować do kierunku wiadomości (inbound: received; outbound:\n  sent/delivered/failed).\n- `contractorId` → przepięcie: odpięcie od dotychczasowych kontrahentów (CRM\n  kasuje ich notatki „SMS\") i przypięcie do wskazanego z nową notatką.\n\nTożsamość wiadomości (`source`, `sourceId`, `direction`, numery, `body`, `sentAt`)\njest tylko do odczytu - 422 `body.fieldNotUpdatable`. Dostępne od wersji API 2.10.0.","operationId":"updateTextMessage","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":512}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"properties":{"status":{"type":"string","enum":["received","sent","delivered","failed"]},"deliveredAt":{"type":"string","format":"date-time"},"contactId":{"type":"integer"},"contractorId":{"type":"integer"},"userId":{"type":"integer"},"callsUrl":{"type":"string","format":"uri"}},"type":"object"}}}},"responses":{"200":{"description":"Wiadomość po aktualizacji","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/TextMessage"},"info":{"$ref":"#/components/schemas/WriteInfo"}},"type":"object"}}}},"404":{"description":"Brak wiadomości o tym id (textMessage.notFound)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Błędy walidacji - lista {field, code, message}","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Moduł VoIP nieaktywny na instancji (module.notActive) albo klucz bez prawa do notatek (textMessage.forbidden) - z zapisem wiadomości powstaje notatka na kartotece","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/tickets":{"get":{"tags":["Tickets"],"summary":"Lista zgłoszeń z filtrowaniem po dowolnym polu i polach niestandardowych","description":"Zgłoszenia (helpdesk) dla integracji i raportowania: filtrowanie po dowolnym polu\nfiltrowalnym i polach niestandardowych, paginacja, sort.\n\n**Typowe scenariusze:**\n- otwarte zgłoszenia kontrahenta: `?contractorId=121&open=1`\n- kolejka użytkownika wg statusu: `?ownerUserId=7&ticketStatusId=1`\n- matchowanie po kluczu integracji: `customField[<klucz>]=<wartość>` (pusta wartość\n  = pole nieustawione, typowo \"jeszcze niezsynchronizowane\")\n\n**Sync przyrostowy (`updatedAfter`):** `updatedAt` to data modyfikacji rekordu\nutrzymywana przez bazę CRM - podbija ją każda zmiana zgłoszenia (status, priorytet,\nprzypisanie), a także nowa odpowiedź w wątku. `updatedAfter` łapie więc zarówno nowe\nzgłoszenia, jak i edycje. Osobne pole `lastResponseAt` niesie datę ostatniej\nodpowiedzi, jeśli potrzebujesz tylko ruchu w wątku.\n\nPola tekstowe (`title`, `email`) filtrują częściowo (zawiera), identyfikatory\n(`*Id`, `uuId`, `open`, `archived`, `priority`) - dokładnie.","operationId":"listTickets","parameters":[{"name":"contractorId","in":"query","description":"Zgłoszenia kontrahenta (dokładne).","required":false,"schema":{"type":"integer","example":121}},{"name":"ticketStatusId","in":"query","description":"Status wg słownika CRM (dokładne).","required":false,"schema":{"type":"integer","example":1}},{"name":"ownerUserId","in":"query","description":"Zgłoszenia prowadzone przez użytkownika (dokładne). Analogicznie dokładne: creatorUserId, lastResponseUserId, ticketSourceId, ticketStageId, priority, relatedTicketId, uuId, id.","required":false,"schema":{"type":"integer","example":7}},{"name":"open","in":"query","description":"true = tylko otwarte zgłoszenia, false = tylko zamknięte (przyjmuje też 1/0).","required":false,"schema":{"type":"boolean"}},{"name":"archived","in":"query","description":"false = tylko aktywne, true = tylko zarchiwizowane (przyjmuje też 1/0).","required":false,"schema":{"type":"boolean"}},{"name":"title","in":"query","description":"Filtr częściowy (zawiera). Analogicznie: email.","required":false,"schema":{"type":"string"}},{"name":"updatedAfter","in":"query","description":"Tylko zgłoszenia utworzone lub z odpowiedzią PO tej chwili (ISO 8601) - patrz uwaga o updatedAt w opisie.","required":false,"schema":{"type":"string","format":"date-time","example":"2026-08-01T00:00:00Z"}},{"name":"updatedBefore","in":"query","description":"Górna granica zakresu (ISO 8601, włącznie).","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"createdAfter","in":"query","description":"Tylko zgłoszenia utworzone po tej chwili (ISO 8601).","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"createdBefore","in":"query","description":"Tylko zgłoszenia utworzone do tej chwili (ISO 8601, włącznie).","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"customField","in":"query","description":"Filtry pól niestandardowych: `customField[<klucz>]=<wartość>` (dokładne; klucze i typy w GET /v2/<encja>/custom-fields). Pusta wartość = pole nieustawione.","required":false,"style":"deepObject","explode":true,"schema":{"type":"object","additionalProperties":{"type":"string"}}},{"name":"id","in":"query","required":false,"description":"Filtr po polu id - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"ticketSourceId","in":"query","required":false,"description":"Filtr po polu ticketSourceId - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"ticketStageId","in":"query","required":false,"description":"Filtr po polu ticketStageId - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"priority","in":"query","required":false,"description":"Filtr po polu priority - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"creatorUserId","in":"query","required":false,"description":"Filtr po polu creatorUserId - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"lastResponseUserId","in":"query","required":false,"description":"Filtr po polu lastResponseUserId - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"email","in":"query","required":false,"description":"Filtr po polu email - dopasowanie częściowe (zawiera).","schema":{"type":"string"}},{"name":"relatedTicketId","in":"query","required":false,"description":"Filtr po polu relatedTicketId - dopasowanie dokładne.","schema":{"type":"integer"}},{"name":"uuId","in":"query","required":false,"description":"Filtr po polu uuId - dopasowanie dokładne.","schema":{"type":"string"}},{"name":"sort","in":"query","description":"Pole sortowania: id, title, priority, resolutionAt, endedAt, lastResponseAt, createdAt, updatedAt.","required":false,"schema":{"type":"string","default":"updatedAt"}},{"name":"sortDir","in":"query","required":false,"schema":{"type":"string","default":"asc","enum":["asc","desc"]}},{"name":"page","in":"query","description":"Strona wyników, od 1.","required":false,"schema":{"type":"integer","default":1,"maximum":1000000,"minimum":1}},{"name":"limit","in":"query","description":"Rozmiar strony (max 1000).","required":false,"schema":{"type":"integer","default":100,"maximum":1000,"minimum":1}}],"responses":{"200":{"description":"Lista zgłoszeń + metadane stronicowania","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Ticket"}},"pagination":{"$ref":"#/components/schemas/Pagination"}},"type":"object"}}}},"400":{"description":"Błąd walidacji zapytania - lista błędów w kontrakcie {field, code, message}","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"post":{"tags":["Tickets"],"summary":"Nowe zgłoszenie","description":"Tworzy zgłoszenie tą samą ścieżką co aplikacja Tillio. Wymagane tylko\n`title`. Pola: title, body (HTML - sanityzowany przez CRM), priority,\nownerUserId, contractorId, contactId (osoba zgłaszająca - kontakt;\nprzy podanym contractorId musi do niego należeć), ticketStatusId\n(GET /v2/ticket/statuses; brak = domyślny), ticketStageId (etap procesu;\nbrak = domyślny; wyznacza też proces, do którego przypisane są pola\nniestandardowe), resolutionAt (brak = SLA procesu), open (true/false),\ncustomField (klucze w GET /v2/ticket/custom-fields; pola przypisane\ndo PROCESÓW - nieprzypisane do procesu etapu dostają 422).\nAutor = użytkownik klucza API.\n\n**Link online dla klienta:** flaga `createClientPanel: true` tworzy\npubliczny panel zgłoszenia w tickets.tillio.pl i zwraca gotowy\n`clientPanelUrl` w odpowiedzi. Wymaga `contactId` z ustawionym adresem\ne-mail (na ten adres CRM wysyła autoresponder z linkiem do panelu).\nNiepowodzenie utworzenia panelu (np. kontakt bez maila, serwis\nniedostępny) nie blokuje zgłoszenia - wraca w `info.warnings.clientPanel`.","operationId":"createTicket","requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["title"],"properties":{"title":{"type":"string","example":"Brak dostępu do panelu"},"description":{"type":"string","example":"<p>Klient zgłasza problem z logowaniem.</p>"},"priority":{"type":"integer"},"ownerUserId":{"type":"integer"},"email":{"description":"E-mail kontaktowy zgłaszającego. Tylko przy tworzeniu - CRM nie ma ścieżki edycji tego pola. Od wersji API 1.39.0.","type":"string","example":"klient@acme.pl","nullable":true},"contractorId":{"type":"integer","example":121},"resolutionAt":{"type":"string","format":"date-time"},"endedAt":{"type":"string","format":"date-time"},"ticketSourceId":{"type":"integer","description":"Wartość musi istnieć w słowniku CRM; tylko przy tworzeniu."},"open":{"type":"boolean"},"ticketStatusId":{"type":"integer","description":"Wartość musi istnieć w słowniku CRM; tylko przy tworzeniu."},"ticketStageId":{"description":"Etap procesu; brak = domyślny","type":"integer"},"createdAt":{"description":"IMPORT DANYCH: data utworzenia rekordu (ISO 8601, strefa instancji). Pozwala zachować chronologię przy wciąganiu historii sprzed lat - bez niej rekord dostaje datę importu. Tylko przy tworzeniu: przy aktualizacji zwracamy 422 (pomyłkę w imporcie poprawia się kasując rekord i importując ponownie). Data z przyszłości = 422. Dostępne od wersji API 1.16.0.","type":"string","format":"date-time","example":"2023-05-17T09:30:00"},"creatorUserId":{"description":"IMPORT DANYCH: użytkownik, który ma figurować jako twórca rekordu (GET /v2/users). Brak = użytkownik przypisany do klucza API. Zapis wykonuje się w kontekście tej osoby, więc obowiązują JEJ uprawnienia. Dostępne od wersji API 1.16.0.","type":"integer","example":42},"contactId":{"description":"Osoba zgłaszająca (kontakt z GET /v2/contacts)","type":"integer","example":50},"serviceId":{"description":"Usługa kontrahenta, której dotyczy zgłoszenie (GET /v2/services) - domyka raportowanie po placówkach/punktach. Usługa musi należeć do kontrahenta zgłoszenia. Dostępne od wersji API 1.19.0.","type":"integer","example":412,"nullable":true},"createClientPanel":{"description":"Utwórz publiczny panel zgłoszenia i zwróć clientPanelUrl (wymaga contactId z e-mailem)","type":"boolean","default":false},"customField":{"type":"object","additionalProperties":{"nullable":true}}},"type":"object"}}}},"responses":{"201":{"description":"Zgłoszenie utworzone","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/Ticket"},"info":{"$ref":"#/components/schemas/WriteInfo"}},"type":"object"}}}},"422":{"description":"Błędy walidacji - lista {field, code, message}","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień do zgłoszeń","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/tickets/{id}":{"get":{"tags":["Tickets"],"summary":"Pojedyncze zgłoszenie po id","description":"Pełne dane zgłoszenia wraz z polami niestandardowymi.","operationId":"getTicket","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":1}}],"responses":{"200":{"description":"Zgłoszenie","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/Ticket"}},"type":"object"}}}},"404":{"description":"Brak zgłoszenia o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"put":{"tags":["Tickets"],"summary":"Aktualizacja zgłoszenia (partial)","description":"Częściowa aktualizacja zgłoszenia - wysyłasz TYLKO pola do zmiany\n(komplet w schemacie poniżej). Import dwuetapowy działa: POST zakłada\nzgłoszenie, PUT uzupełnia daty rozwiązania/zakończenia i stan otwarcia.\n\n**Nazwy z odczytu działają też przy edycji** (od wersji API 1.39.0):\n`ownerUserId` = `ownerUserId`, `description` = `body`,\n`resolutionAt` = `resolutionAt` - wzorzec „pobierz rekord, zmień\npole, odeślij\" przechodzi bez mapowania nazw.\n\n**Powiązania (od wersji API 1.34.0):** `contactId` (osoba zgłaszająca)\ni `serviceId` (usługa/punkt kontrahenta) - id ustawia/podmienia\npowiązanie, `null` je wypina. `serviceId` wymaga, żeby zgłoszenie miało\nkontrahenta, a usługa do niego należała.\n\n`ticketStatusId`/`ticketStageId`/`ticketSourceId` nie podlegają edycji przez API\n(przepływ procesu w CRM; źródło opisuje kanał wpływu zgłoszenia) -\n422 body.fieldNotUpdatable.","operationId":"updateTicket","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":1}}],"requestBody":{"description":"Aktualizacja CZĘŚCIOWA - wysyłasz tylko pola do zmiany. Poza listą poniżej zapis przyjmuje `customField`. Pola ustawiane WYŁĄCZNIE przy tworzeniu (próba zmiany = 422 `body.fieldNotUpdatable`): email, contractorId, ticketSourceId, ticketStatusId, ticketStageId.","required":true,"content":{"application/json":{"schema":{"properties":{"title":{"type":"string","example":"Brak faktury za lipiec"},"description":{"description":"Treść zgłoszenia (HTML).","type":"string","nullable":true},"priority":{"description":"Priorytet (skala CRM)","type":"integer","nullable":true},"ownerUserId":{"description":"Prowadzący zgłoszenie (GET /v2/users).","type":"integer","example":7,"nullable":true},"resolutionAt":{"description":"Termin realizacji (ISO 8601).","type":"string","format":"date-time","nullable":true},"endedAt":{"description":"Data zakończenia zgłoszenia (ISO 8601)","type":"string","format":"date-time","nullable":true},"open":{"description":"true = otwarte, false = zamknięte","type":"boolean","nullable":true},"contactId":{"description":"Osoba zgłaszająca: id = ustaw/podmień, null = wypnij. Od wersji API 1.34.0.","type":"integer","example":50,"nullable":true},"serviceId":{"description":"Usługa/punkt kontrahenta: id = ustaw/podmień, null = wypnij. Usługa musi należeć do kontrahenta zgłoszenia. Od wersji API 1.34.0.","type":"integer","example":412,"nullable":true},"customField":{"description":"Pola niestandardowe: klucz → wartość (klucze w GET /v2/ticket/custom-fields). Pola wielowartościowe (multiselect, produkty, szanse sprzedaży) przyjmują listę wartości, np. [\"12\",\"15\"] - pojedyncza wartość jest równoważna liście jednoelementowej. Lista dostępna od wersji API 2.9.0.","type":"object","additionalProperties":{"nullable":true}}},"type":"object"}}}},"responses":{"200":{"description":"Zgłoszenie po aktualizacji","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/Ticket"},"info":{"$ref":"#/components/schemas/WriteInfo"}},"type":"object"}}}},"404":{"description":"Brak zgłoszenia o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Błędy walidacji","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień do zgłoszeń","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/tickets/{id}/messages":{"get":{"tags":["Tickets"],"summary":"Wiadomości zgłoszenia","description":"Wiadomości w wątku zgłoszenia (korespondencja), chronologicznie. Zwraca też autora, adresy i powiązanie odpowiedzi.","operationId":"listTicketMessages","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":29}}],"responses":{"200":{"description":"Lista wiadomości","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"properties":{"id":{"type":"integer"},"visibility":{"type":"string","example":"public"},"text":{"type":"string"},"subject":{"type":"string","nullable":true},"creatorUserId":{"type":"integer","nullable":true},"contactId":{"type":"integer","nullable":true},"fromName":{"type":"string","nullable":true},"fromEmail":{"type":"string","nullable":true},"toEmails":{"type":"array","items":{"type":"string"}},"replyToMessageId":{"type":"integer","nullable":true},"date":{"type":"string","format":"date-time"}},"type":"object"}}},"type":"object"}}}},"404":{"description":"Brak zgłoszenia o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"post":{"tags":["Tickets"],"summary":"Nowa wiadomość w zgłoszeniu (import historii)","description":"Dopisuje wiadomość do wątku zgłoszenia. Główne zastosowanie: **import historii\nobsługi** - `date` i `creatorUserId` pozwalają odtworzyć chronologię i autorstwo.\n\n**API niczego nie wysyła.** Wiadomość ląduje w wątku zgłoszenia, ale nie idzie\ndo nikogo mailem - import historii sprzed lat nie zasypie klientów pocztą.\nWysyłka maila to osobna, jawna operacja: `POST /v2/mail/send`.\n\n`visibility: public` (domyślne) = wiadomość w wątku zgłoszenia. `visibility: internal`\n(komentarz wewnętrzny) API JESZCZE nie zapisuje - zawsze 422\n`ticket.internalMessagesUnavailable`, na każdej instalacji; wsparcie pojawi się\nosobnym wydaniem, gdy CRM wystawi zapis komentarza.\n\n**Wiadomość od klienta (od wersji API 1.38.0):** gdy podasz `fromName`/`fromEmail`\nalbo `contactId` i NIE podasz `creatorUserId`, wiadomość zapisuje się jak\nkorespondencja przychodząca - w interfejsie podpisana nadawcą, nie użytkownikiem\nklucza API. Z `creatorUserId` wiadomość jest komentarzem wskazanego użytkownika,\na `fromName`/`fromEmail` zostają metadanymi.","operationId":"createTicketMessage","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":29}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["text"],"properties":{"text":{"description":"Treść (HTML dozwolony, sanityzowany przez CRM)","type":"string","example":"Przyjęliśmy zgłoszenie, kurier jutro."},"visibility":{"description":"public = wiadomość w wątku (domyślnie, działa). internal = komentarz wewnętrzny - jeszcze NIEobsługiwany, zawsze 422 ticket.internalMessagesUnavailable (wartość zarezerwowana pod przyszłe wydanie).","type":"string","default":"public","enum":["public","internal"]},"date":{"description":"Data wiadomości (ISO 8601) - do importu historii.","type":"string","format":"date-time","example":"2023-04-11T09:12:00"},"creatorUserId":{"description":"Autor wiadomości (GET /v2/users). Brak = wiadomość od klienta, gdy podasz fromName/fromEmail/contactId.","type":"integer","example":42},"subject":{"type":"string","nullable":true},"fromName":{"description":"Nadawca (gdy wiadomość pochodzi od klienta) - bez creatorUserId wiadomość jest podpisana nadawcą, jak korespondencja przychodząca (od wersji API 1.38.0)","type":"string","nullable":true},"fromEmail":{"type":"string","nullable":true},"contactId":{"description":"Osoba kontaktowa, od której pochodzi wiadomość (GET /v2/contacts) - wiąże wiadomość ze zgłaszającym. Od wersji API 1.38.0.","type":"integer","example":50,"nullable":true},"replyToMessageId":{"description":"Odpowiedź na wcześniejszą wiadomość wątku","type":"integer","nullable":true}},"type":"object"}}}},"responses":{"201":{"description":"Wiadomość dopisana"},"422":{"description":"Błąd walidacji","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/users":{"get":{"tags":["Users"],"summary":"Lista użytkowników systemu","description":"Użytkownicy systemu (bez kont technicznych/ukrytych). Kontrakt zawiera WYŁĄCZNIE\ndane niewrażliwe: id, imię, nazwisko, email i status konta.\n\nTypowe użycie w integracji: mapowanie opiekuna kontrahenta\n(`ownerUserId` z GET /v2/contractors) na osobę, albo znalezienie\nid użytkownika po loginie:\n```\nGET /v2/users?email=jan.kowalski@firma.pl\n```\nImię i nazwisko filtrują częściowo (zawiera) - przydatne, gdy ERP zna\nopiekuna tylko z imienia i nazwiska.","operationId":"listUsers","parameters":[{"name":"id","in":"query","description":"Filtr po id - dopasowanie dokładne.","required":false,"schema":{"type":"integer"}},{"name":"firstName","in":"query","description":"Filtr częściowy (zawiera).","required":false,"schema":{"type":"string","example":"Jan"}},{"name":"lastName","in":"query","description":"Filtr częściowy (zawiera).","required":false,"schema":{"type":"string","example":"Kowalski"}},{"name":"email","in":"query","description":"Login systemowy - dopasowanie dokładne.","required":false,"schema":{"type":"string","example":"jan.kowalski@firma.pl"}},{"name":"userStatusId","in":"query","description":"Status konta - dopasowanie dokładne (typowo: 1 aktywny, 2 nieaktywny, 3 zawieszony, 4 usunięty).","required":false,"schema":{"type":"integer","example":1}},{"name":"sort","in":"query","description":"Pole sortowania: id, firstName, lastName.","required":false,"schema":{"type":"string","default":"id"}},{"name":"sortDir","in":"query","required":false,"schema":{"type":"string","default":"asc","enum":["asc","desc"]}},{"name":"page","in":"query","description":"Strona wyników, od 1.","required":false,"schema":{"type":"integer","default":1,"maximum":1000000,"minimum":1}},{"name":"limit","in":"query","description":"Rozmiar strony (max 1000).","required":false,"schema":{"type":"integer","default":100,"maximum":1000,"minimum":1}}],"responses":{"200":{"description":"Lista użytkowników + metadane stronicowania","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/SystemUser"}},"pagination":{"$ref":"#/components/schemas/Pagination"}},"type":"object"}}}},"400":{"description":"Błąd walidacji zapytania - lista błędów w kontrakcie {field, code, message}","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"post":{"tags":["Users"],"summary":"Nowy użytkownik systemu (z hasłem startowym)","description":"Zakłada konto użytkownika systemu tą samą ścieżką co panel administracyjny\n(limit licencyjny instancji, unikalność loginu, domyślne ustawienia konta,\nkalendarz zakładany razem z kontem).\n\n**Hasło startowe generuje API** i zwraca je RAZ w `info.temporaryPassword` -\nnie da się go później odczytać. Jest krótkie i jednoznaczne w dyktowaniu\n(bez `0`/`O`, `1`/`l`/`I` i znaków interpunkcyjnych). Konto zawsze\nz wymuszoną zmianą hasła przy pierwszym logowaniu i bez 2FA.\n\n**Login:** `email`. **Rola:** `roleId` z GET /v2/user/roles decyduje\no uprawnieniach. **Dział:** `departmentId` z GET /v2/user/departments.\n\n**Import danych historycznych:** `userStatusId` (GET /v2/user/statuses) pozwala\nzałożyć konto od razu nieaktywne - takie konta NIE liczą się do limitu\naktywnych użytkowników instancji.\n\n**Limit licencyjny:** przy komplecie aktywnych kont odpowiedź to\n`409 user.limitReached` - konto nie powstaje. UWAGA, znane ograniczenie CRM\n(zgłoszone do naprawy): przy wyczerpanym limicie CRM odrzuca także konta\nzakładane jako NIEAKTYWNE, mimo że do limitu się nie liczą - limit jest\nsprawdzany przed uwzględnieniem statusu z żądania. Konta nieaktywne\nzakładaj, zanim limit się wyczerpie, albo zwolnij miejsce na czas importu.","operationId":"createUser","requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["firstName","email","userStatusId","roleId"],"properties":{"firstName":{"type":"string","example":"Anna"},"lastName":{"description":"Opcjonalne - konto funkcyjne (kolejka, skrzynka zespołu) zakłada się bez nazwiska. Naprawione w wersji API 1.36.0 (wcześniej brak odbijał 422 wbrew spec).","type":"string","example":"Kowalska","nullable":true},"email":{"description":"Login użytkownika do systemu","type":"string","example":"anna.kowalska@firma.pl"},"position":{"description":"Stanowisko","type":"string","example":"Specjalista ds. sprzedaży"},"phone":{"description":"Telefon służbowy - normalizowany do formatu międzynarodowego (9 cyfr = +48..., prefiks bez plusa dostaje +, mniej niż 9 cyfr = wartość odrzucana; od wersji API 1.32.0); wartość nie do znormalizowania nie blokuje założenia konta (wraca w info.warnings)","type":"string","example":"+48601234567"},"gender":{"description":"Płeć - używana m.in. w odmianie komunikatów systemu","type":"string","default":"unspecified","enum":["male","female","unspecified"]},"userStatusId":{"description":"Status konta z GET /v2/user/statuses (konto nieaktywne nie zajmuje miejsca w limicie)","type":"integer","example":1},"departmentId":{"description":"Dział z GET /v2/user/departments","type":"integer","example":1},"roleId":{"description":"Rola (grupa uprawnień) z GET /v2/user/roles","type":"integer","example":2}},"type":"object"}}}},"responses":{"201":{"description":"Konto utworzone - hasło startowe w info.temporaryPassword (widoczne tylko w tej odpowiedzi)","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/SystemUser"},"info":{"properties":{"created":{"type":"boolean","example":true},"ids":{"properties":{"userId":{"type":"integer","example":61200}},"type":"object"},"temporaryPassword":{"description":"Hasło startowe - przekaż użytkownikowi, system wymusi jego zmianę przy pierwszym logowaniu","type":"string","example":"Kx7mRt3npQwe"},"warnings":{"type":"object"}},"type":"object"}},"type":"object"}}}},"409":{"description":"Limit aktywnych użytkowników instancji wyczerpany (user.limitReached)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Błędy walidacji (m.in. body.emailNotUnique - login zajęty)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień do zarządzania użytkownikami","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/warehouses":{"get":{"tags":["Warehouses"],"summary":"Lista magazynów","description":"Słownik magazynów instancji. Integracja ERP typowo pobiera go raz na początku\nsyncu stanów i mapuje swoje magazyny po `symbol` (unikalny, dopasowanie dokładne):\n\n```\nGET /v2/warehouses?symbol=MAIN\n```\n\nMagazynów jest zwykle kilka - paginacja jest raczej formalnością (spójny kontrakt list).","operationId":"listWarehouses","parameters":[{"name":"id","in":"query","description":"Filtr po id - dopasowanie dokładne.","required":false,"schema":{"type":"integer"}},{"name":"name","in":"query","description":"Filtr częściowy (zawiera).","required":false,"schema":{"type":"string","example":"Centralny"}},{"name":"symbol","in":"query","description":"Symbol magazynu - dopasowanie dokładne (klucz naturalny do matchowania z ERP).","required":false,"schema":{"type":"string","example":"MAIN"}},{"name":"status","in":"query","description":"1 aktywny, 0 nieaktywny.","required":false,"schema":{"type":"integer","enum":[0,1]}},{"name":"sort","in":"query","description":"Pole sortowania: id, name, symbol, createdAt.","required":false,"schema":{"type":"string","default":"id"}},{"name":"sortDir","in":"query","required":false,"schema":{"type":"string","default":"asc","enum":["asc","desc"]}},{"name":"page","in":"query","description":"Strona wyników, od 1.","required":false,"schema":{"type":"integer","default":1,"maximum":1000000,"minimum":1}},{"name":"limit","in":"query","description":"Rozmiar strony (max 1000).","required":false,"schema":{"type":"integer","default":100,"maximum":1000,"minimum":1}}],"responses":{"200":{"description":"Lista magazynów + metadane stronicowania","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Warehouse"}},"pagination":{"$ref":"#/components/schemas/Pagination"}},"type":"object"}}}},"400":{"description":"Błąd walidacji zapytania - lista błędów w kontrakcie {field, code, message}","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"post":{"tags":["Warehouses"],"summary":"Nowy magazyn","description":"Tworzy magazyn. Wymagane name i symbol (symbol unikalny w instancji - duplikat = 422 body.duplicateValue).","operationId":"createWarehouse","requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["name","symbol"],"properties":{"createdAt":{"description":"IMPORT DANYCH: data utworzenia rekordu (ISO 8601, strefa instancji). Pozwala zachować chronologię przy wciąganiu historii sprzed lat - bez niej rekord dostaje datę importu. Tylko przy tworzeniu: przy aktualizacji zwracamy 422 (pomyłkę w imporcie poprawia się kasując rekord i importując ponownie). Data z przyszłości = 422. Dostępne od wersji API 1.16.0.","type":"string","format":"date-time","example":"2023-05-17T09:30:00"},"creatorUserId":{"description":"IMPORT DANYCH: użytkownik, który ma figurować jako twórca rekordu (GET /v2/users). Brak = użytkownik przypisany do klucza API. Zapis wykonuje się w kontekście tej osoby, więc obowiązują JEJ uprawnienia. Dostępne od wersji API 1.16.0.","type":"integer","example":42},"name":{"type":"string","example":"Magazyn główny"},"symbol":{"description":"Unikalny symbol magazynu","type":"string","example":"MG-01"},"status":{"type":"integer","default":1}},"type":"object"}}}},"responses":{"201":{"description":"Magazyn utworzony","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/Warehouse"},"info":{"$ref":"#/components/schemas/WriteInfo"}},"type":"object"}}}},"422":{"description":"Błędy walidacji - lista {field, code, message}","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień do magazynów","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/warehouses/{id}":{"get":{"tags":["Warehouses"],"summary":"Magazyn po id","description":"Pojedynczy magazyn po id.","operationId":"getWarehouse","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":1}}],"responses":{"200":{"description":"Magazyn","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/Warehouse"}},"type":"object"}}}},"404":{"description":"Brak magazynu o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"put":{"tags":["Warehouses"],"summary":"Aktualizacja magazynu (partial)","description":"Częściowa aktualizacja magazynu - wysyłasz TYLKO pola do zmiany (name, symbol, status).","operationId":"updateWarehouse","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":1}}],"requestBody":{"description":"Pola do zmiany","required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Magazyn po aktualizacji","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/Warehouse"},"info":{"$ref":"#/components/schemas/WriteInfo"}},"type":"object"}}}},"404":{"description":"Brak magazynu o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Błędy walidacji","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Klucz API bez uprawnień do magazynów","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/whoami":{"get":{"tags":["System"],"summary":"Tożsamość klucza API (test poprawności auth)","description":"Zwraca tożsamość uwierzytelnionego klucza: tenant i nazwę klucza. Najprostszy sposób na zweryfikowanie poprawności nagłówków auth przy podpinaniu integracji.","operationId":"whoami","responses":{"200":{"description":"Kontekst uwierzytelnienia","content":{"application/json":{"schema":{"properties":{"authenticated":{"type":"boolean","example":true},"tenantDomain":{"type":"string","example":"crm.przyklad.pl","nullable":true},"tenantId":{"type":"string","example":"crm-przyklad-pl-a1b2c3","nullable":true},"keyName":{"type":"string","example":"Integracja Optima","nullable":true},"userId":{"description":"Użytkownik przypisany do klucza (autor zapisów przez API)","type":"integer","example":12345,"nullable":true}},"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/wiki/bases":{"get":{"tags":["Wiki"],"summary":"Bazy wiedzy","description":"Bazy wiedzy widoczne dla użytkownika klucza. `type` rozróżnia artykuły od procedur.","operationId":"listWikiBases","parameters":[{"name":"type","in":"query","description":"Rodzaj bazy: article (artykuły) albo procedure (procedury)","required":false,"schema":{"type":"string","enum":["article","procedure"]}},{"name":"archived","in":"query","description":"true = bazy zarchiwizowane, false = bieżące, brak = wszystkie","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"description":"Lista baz wiedzy","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/WikiBase"}}},"type":"object"}}}},"403":{"description":"Moduł Baza wiedzy nieaktywny","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"post":{"tags":["Wiki"],"summary":"Nowa baza wiedzy","description":"Nowa baza wiedzy. `type: procedure` zakłada bazę procedur, `article` - bazę artykułów; w CRM to dwa osobne zbiory, dlatego typ podaje się na bazie, a nie na wpisie. Tytuł musi być unikalny w obrębie typu.","operationId":"createWikiBase","requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["name"],"properties":{"name":{"description":"Tytuł bazy (min. 3 znaki)","type":"string","example":"Procedury obsługi zgłoszeń"},"type":{"type":"string","default":"article","enum":["article","procedure"]},"subtitle":{"type":"string","example":"Jak obsługujemy zgłoszenia serwisowe"},"icon":{"description":"Ikona FontAwesome, jak w panelu","type":"string","default":"fa-book","example":"fa-wrench"},"color":{"description":"Kolor w formacie #rrggbb","type":"string","default":"#2196f3","example":"#77d44e"}},"type":"object"}}}},"responses":{"201":{"description":"Baza wiedzy utworzona"},"422":{"description":"Błędy walidacji (m.in. duplikat tytułu w tym samym typie)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/wiki/bases/{id}":{"put":{"tags":["Wiki"],"summary":"Aktualizacja bazy wiedzy","description":"Częściowa aktualizacja bazy wiedzy (pola nieprzysłane zostają bez zmian). `archived: true` archiwizuje bazę razem z jej kategoriami i wpisami, `false` przywraca - to w CRM osobna operacja, dlatego nie da się jej pomylić ze zwykłą edycją.","operationId":"updateWikiBase","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":415}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"properties":{"name":{"type":"string"},"subtitle":{"type":"string"},"type":{"type":"string","enum":["article","procedure"]},"icon":{"type":"string"},"color":{"type":"string"},"archived":{"description":"Archiwizacja bazy wraz z kategoriami i wpisami","type":"boolean"}},"type":"object"}}}},"responses":{"200":{"description":"Baza wiedzy zaktualizowana"},"404":{"description":"Brak bazy o tym id albo brak do niej dostępu","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/wiki/bases/{id}/categories":{"get":{"tags":["Wiki"],"summary":"Kategorie bazy wiedzy","description":"Kategorie w bazie wiedzy - wpis zawsze należy do jednej z nich.","operationId":"listWikiCategories","parameters":[{"name":"id","in":"path","description":"Id bazy wiedzy","required":true,"schema":{"type":"integer","example":415}}],"responses":{"200":{"description":"Lista kategorii","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/WikiCategory"}}},"type":"object"}}}},"404":{"description":"Brak bazy o tym id albo brak do niej dostępu","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"post":{"tags":["Wiki"],"summary":"Nowa kategoria","description":"Nowa kategoria w bazie wiedzy.","operationId":"createWikiCategory","parameters":[{"name":"id","in":"path","description":"Id bazy wiedzy","required":true,"schema":{"type":"integer","example":415}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["name"],"properties":{"name":{"description":"Nazwa kategorii (min. 3 znaki)","type":"string","example":"Reklamacje"}},"type":"object"}}}},"responses":{"201":{"description":"Kategoria utworzona"},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/wiki/categories/{id}":{"put":{"tags":["Wiki"],"summary":"Aktualizacja kategorii","description":"Zmiana nazwy kategorii.","operationId":"updateWikiCategory","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":220}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"properties":{"name":{"type":"string","example":"Reklamacje i zwroty"},"archived":{"description":"Archiwizacja kategorii wraz z jej wpisami","type":"boolean"}},"type":"object"}}}},"responses":{"200":{"description":"Kategoria zaktualizowana"},"404":{"description":"Brak kategorii o tym id albo brak do niej dostępu","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/wiki/entries":{"get":{"tags":["Wiki"],"summary":"Wpisy bazy wiedzy","description":"Wpisy bazy wiedzy. Lista nie zawiera treści (`content`) - bywa długa; pełny wpis: GET /v2/wiki/entries/{id}.","operationId":"listWikiEntries","parameters":[{"name":"baseId","in":"query","required":false,"schema":{"type":"integer"}},{"name":"categoryId","in":"query","required":false,"schema":{"type":"integer"}},{"name":"published","in":"query","description":"true = tylko opublikowane","required":false,"schema":{"type":"boolean"}},{"name":"archived","in":"query","description":"Domyślnie false - wpisy zarchiwizowane trzeba poprosić jawnie","required":false,"schema":{"type":"boolean","default":false}},{"name":"search","in":"query","description":"Fragment tytułu albo treści","required":false,"schema":{"type":"string"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","default":1,"maximum":1000000,"minimum":1}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":100,"maximum":1000,"minimum":1}}],"responses":{"200":{"description":"Lista wpisów + metadane stronicowania","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/WikiEntry"}},"pagination":{"$ref":"#/components/schemas/Pagination"}},"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"post":{"tags":["Wiki"],"summary":"Nowy wpis bazy wiedzy","description":"Nowy wpis w kategorii bazy wiedzy. `content` to HTML - CRM sam go czyści\n(usuwa niebezpieczne znaczniki), więc treść z edytora można wysłać wprost.\n\nWpis powstaje domyślnie OPUBLIKOWANY (`published: true`) - integracja siejąca\nbazę wiedzy chce ją widzieć od razu. `published: false` zostawia go w szkicach.\n\n`createdAt` i `creatorUserId` (import danych archiwalnych) działają tak samo\njak przy pozostałych encjach: ustawia się je wyłącznie przy tworzeniu.","operationId":"createWikiEntry","requestBody":{"required":true,"content":{"application/json":{"schema":{"required":["categoryId","title"],"properties":{"categoryId":{"description":"Kategoria z GET /v2/wiki/bases/{id}/categories","type":"integer","example":220},"title":{"description":"Tytuł (min. 3 znaki)","type":"string","example":"Zgłoszenie reklamacyjne - krok po kroku"},"content":{"description":"Treść w HTML","type":"string","example":"<p>1. Sprawdź numer zamówienia...</p>"},"published":{"type":"boolean","default":true},"alias":{"description":"Własny adres wpisu (musi być unikalny)","type":"string","example":"reklamacje-krok-po-kroku"},"createdAt":{"description":"Data utworzenia przy imporcie historii (ISO 8601)","type":"string","format":"date-time"},"creatorUserId":{"description":"Twórca wpisu; brak = użytkownik klucza API","type":"integer"}},"type":"object"}}}},"responses":{"201":{"description":"Wpis utworzony"},"422":{"description":"Błędy walidacji","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}},"/v2/wiki/entries/{id}":{"get":{"tags":["Wiki"],"summary":"Wpis bazy wiedzy","description":"Pojedynczy wpis wraz z treścią (HTML).","operationId":"getWikiEntry","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":1201}}],"responses":{"200":{"description":"Wpis","content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/WikiEntry"}},"type":"object"}}}},"404":{"description":"Brak wpisu o tym id albo brak do niego dostępu","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"put":{"tags":["Wiki"],"summary":"Aktualizacja wpisu","description":"Częściowa aktualizacja wpisu. Zmiana `categoryId` przenosi wpis do innej kategorii (w tej samej bazie wiedzy). `archived: true` zdejmuje wpis z bazy wiedzy bez kasowania go.","operationId":"updateWikiEntry","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":1201}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"properties":{"categoryId":{"type":"integer"},"title":{"type":"string"},"content":{"description":"Treść w HTML","type":"string"},"published":{"type":"boolean"},"alias":{"type":"string"},"archived":{"description":"Zdjęcie wpisu z bazy wiedzy bez kasowania","type":"boolean"}},"type":"object"}}}},"responses":{"200":{"description":"Wpis zaktualizowany"},"404":{"description":"Brak wpisu o tym id albo brak do niego dostępu","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]},"delete":{"tags":["Wiki"],"summary":"Usunięcie wpisu","description":"Usuwa wpis wraz z jego załącznikami. Operacja nieodwracalna; CRM pozwala na nią administratorowi bazy wiedzy albo twórcy wpisu z odpowiednim uprawnieniem.","operationId":"deleteWikiEntry","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","example":1201}}],"responses":{"200":{"description":"Wpis usunięty"},"403":{"description":"Brak uprawnień do usuwania wpisów","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Brak wpisu o tym id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"tenantAuth":[],"tenantDomain":[],"tenantId":[]},{"tenantAuth":[],"tenantDomain":[],"tenantId":[],"instanceName":[]}]}}},"components":{"schemas":{"Contractor":{"description":"Kontrahent. `customField` to obiekt klucz→wartość surowa (select = id opcji - mapowanie w GET /v2/<encja>/custom-fields). `address` tylko przy include=address.","properties":{"id":{"description":"Korzeń dokumentacji OpenAPI (swagger-php skanuje app/ - atrybuty na tej klasie\ndefiniują metadane API, schematy współdzielone i security). Endpointy dokumentują\nsię same atrybutami przy kontrolerach.\n\nNamed arguments ZAWSZE w kolejności parametrów konstruktora atrybutu\n(inspekcja \"Named arguments order does not match parameters order\").","type":"integer","example":121},"name":{"description":"Nazwa skrócona","type":"string","example":"Acme"},"alias":{"description":"Alias wyświetlany w CRM","type":"string","example":"acme"},"fullName":{"description":"Pełna nazwa rejestrowa","type":"string","example":"Acme Sp. z o.o.","nullable":true},"taxId":{"description":"NIP (bez separatorów)","type":"string","example":"5252344078","nullable":true},"regon":{"type":"string","nullable":true},"pesel":{"type":"string","nullable":true},"email":{"type":"string","example":"biuro@acme.pl","nullable":true},"phone":{"type":"string","example":"+48221234567","nullable":true},"domain":{"type":"string","example":"acme.pl","nullable":true},"country":{"description":"Kod kraju ISO 3166-1 alpha-2","type":"string","example":"PL","nullable":true},"externalId":{"description":"Historyczny klucz zewnętrzny starszych integracji. Dla nowych integracji zalecane dedykowane pole niestandardowe per system.","type":"string","nullable":true},"note":{"type":"string","nullable":true},"contractorTypeId":{"description":"Typ kontrahenta (GET /v2/contractor/types)","type":"integer","nullable":true},"contractorStatusId":{"type":"integer","nullable":true},"industryId":{"type":"integer","nullable":true},"contractorSourceId":{"type":"integer","nullable":true},"contractorPriorityId":{"type":"integer","nullable":true},"paymentTypeId":{"type":"integer","nullable":true},"legalFormId":{"type":"integer","nullable":true},"ownerUserId":{"description":"Opiekun handlowy (id użytkownika systemu)","type":"integer","nullable":true},"parentId":{"description":"Kontrahent nadrzędny (struktury kapitałowe)","type":"integer","nullable":true},"employeesCount":{"type":"integer","nullable":true},"revenue":{"description":"Przychód - string dla precyzji dziesiętnej","type":"string","example":"1250000.00","nullable":true},"revenueCurrency":{"type":"string","example":"PLN","nullable":true},"acl":{"oneOf":[{"$ref":"#/components/schemas/AclJson"}],"nullable":true,"description":"ACL rekordu z CRM: dostęp ograniczony do wskazanych użytkowników/działów/grup; null = bez ograniczeń. Do respektowania ograniczeń dostępu po stronie konsumenta."},"createdAt":{"type":"string","format":"date-time","example":"2026-08-01T09:30:00+02:00"},"updatedAt":{"description":"Ostatnia modyfikacja; dla rekordów nigdy niemodyfikowanych = createdAt. Klucz syncu przyrostowego.","type":"string","format":"date-time","example":"2026-08-12T14:05:11+02:00"},"lastActivityAt":{"description":"Ostatnia aktywność handlowa w CRM","type":"string","format":"date-time","nullable":true},"customField":{"description":"Pola niestandardowe: klucz → wartość. Kluczem jest ZAWSZE `key` z GET /v2/{entity}/custom-fields (np. `contractor_str_2`), nigdy etykieta pola - ta sama zasada w odczycie, filtrach i zapisie. Pola wielowartościowe (multiselect, produkty, szanse sprzedaży) niosą listę wartości, np. [\"12\",\"15\"]; filtr `customField[<klucz>]=12` znajduje rekordy zawierające podaną wartość.","type":"object","example":{"contractor_str_2":"8123","contractor_select_1":5},"additionalProperties":{"nullable":true}},"address":{"description":"Tylko przy include=address","type":"array","items":{"$ref":"#/components/schemas/Address"}},"url":{"description":"Adres kartoteki w interfejsie CRM (dla ludzi - do powiadomień i raportów). Od wersji API 1.38.0.","type":"string","example":"https://twoja-instancja.tillio.app/crm/contractors/121","nullable":true}},"type":"object"},"Address":{"description":"Adres fizyczny. Należy w CRM do FIRMY (wspólnej dla kontrahentów o tym samym NIP-ie), nie do pojedynczego kontrahenta - kontrahenci dzielący firmę widzą ten sam zestaw adresów. `latitude`/`longitude`/`placeId` uzupełnia geokoder przy aktywnym module Mapy.","properties":{"id":{"type":"integer","example":15},"addressTypeId":{"description":"Typ adresu (siedziba, korespondencyjny...)","type":"integer","example":1},"street":{"type":"string","example":"Marszałkowska 1","nullable":true},"street2":{"type":"string","nullable":true},"postCode":{"type":"string","example":"00-624","nullable":true},"city":{"type":"string","example":"Warszawa","nullable":true},"region":{"type":"string","example":"mazowieckie","nullable":true},"district":{"description":"Powiat","type":"string","nullable":true},"country":{"type":"string","example":"PL","nullable":true},"placeId":{"description":"Identyfikator miejsca z geokodera (dostępny od wersji API 1.21.0)","type":"string","example":"ChIJLZGhOufMHkcRwTsg9eKcn2M","nullable":true},"latitude":{"description":"Szerokość geograficzna; null = adres nie został zgeokodowany (dostępna od wersji API 1.21.0)","type":"number","format":"float","example":52.2155351,"nullable":true},"longitude":{"description":"Długość geograficzna (dostępna od wersji API 1.21.0)","type":"number","format":"float","example":21.0213232,"nullable":true}},"type":"object"},"AddressLookupResult":{"description":"Wynik weryfikacji adresu w geokoderze (parametr `addressLookup`).","properties":{"status":{"description":"`skipped` = nie pytaliśmy, `matched` = geokoder zna adres, `notFound` = nie rozpoznał, `unavailable` = serwis nie odpowiedział (zapis i tak przechodzi)","type":"string","enum":["skipped","matched","notFound","unavailable"],"example":"matched"},"matchLevel":{"description":"`exact` = ulica z numerem i miejscowość, `approximate` = trafienie tylko w miejscowość/region","type":"string","enum":["exact","approximate"],"example":"exact","nullable":true},"formattedAddress":{"description":"Adres w postaci z geokodera","type":"string","example":"Marszałkowska 10, 00-590 Warszawa, Polska","nullable":true},"latitude":{"type":"number","format":"float","example":52.2155351,"nullable":true},"longitude":{"type":"number","format":"float","example":21.0213232,"nullable":true},"placeId":{"type":"string","example":"ChIJLZGhOufMHkcRwTsg9eKcn2M","nullable":true},"normalized":{"description":"Czy dane adresu zostały nadpisane postacią z geokodera","type":"boolean","example":true},"message":{"description":"Wyjaśnienie, gdy adres nie został potwierdzony","type":"string","nullable":true}},"type":"object"},"CustomFieldDefinition":{"description":"Definicja pola niestandardowego. Identyfikatorem operacyjnym jest ZAWSZE `key` - nim odczytujesz, filtrujesz i zapisujesz wartości. `name` i `displayName` to etykiety dla ludzi i nie działają jako klucz.","properties":{"key":{"description":"JEDYNY identyfikator operacyjny pola. Tym kluczem odczytujesz wartość (`customField.<key>`), filtrujesz (`customField[<key>]=`) i zapisujesz przy POST/PUT encji. Nadaje go CRM przy tworzeniu pola - nie jest to etykieta, którą podałeś.","type":"string","example":"contractor_str_2"},"name":{"description":"Etykieta pola dla ludzi (mapowanie, wizardy); gdy pole nie ma etykiety, powtarza `key`. NIE używaj jej jako klucza - odczyt i zapis wartości działają wyłącznie na `key`.","type":"string","example":"Optima ID"},"displayName":{"description":"Etykieta nadana w CRM (null, gdy pole jej nie ma). Jak wyżej: do operacji służy `key`.","type":"string","example":"Optima ID","nullable":true},"type":{"description":"Nazwa typu pola: INT, DECIMAL, DATETIME, STR, TEXT, SELECT, MULTISELECT, USERS, VARCHAR, CONTRACTOR, FILE, PRODUCT, PROJECT","type":"string","example":"STR"},"fieldTypeId":{"description":"Id typu pola wg słownika CRM","type":"integer","example":4},"required":{"type":"boolean","example":false},"config":{"description":"Konfiguracja pola, m.in. opcje selectów (id → etykieta)","type":"object","nullable":true},"options":{"description":"Opcje pól SELECT/MULTISELECT wprost - {value, name, color}; value to identyfikator zapisywany w customField. Symetryczne z options w POST /v2/custom-fields; null dla pól bez statycznych opcji. Dostępne od wersji API 1.36.0.","type":"array","items":{"properties":{"value":{"description":"Identyfikator opcji (to zapisuje się w customField)","example":2},"name":{"description":"Etykieta opcji","type":"string","example":"Telefon"},"color":{"type":"string","example":"#ffffff","nullable":true}},"type":"object"},"nullable":true},"editableBy":{"oneOf":[{"$ref":"#/components/schemas/AclJson"}],"nullable":true,"description":"ACL pola: kto je widzi i edytuje ({userIds, departmentIds, groupIds}); null = wszyscy. Symetryczne z editableBy w POST /v2/custom-fields. Dostępne od wersji API 1.29.0."},"assignedTo":{"description":"Id podtypów rekordów, w których pole działa - tylko encje z przypisaniami (note, ticket, service, lead, pipeline). Zmiana: PUT /v2/{entity}/custom-fields/{key}. Dostępne od wersji API 1.28.0.","type":"array","items":{"type":"integer"}}},"type":"object"},"NoteContact":{"description":"Kontakt przypięty do notatki (relacja wiele-do-wielu: notatka może mieć kilka kontaktów). Dane osoby pochodzą z kartoteki kontaktu - pełny rekord odczytasz przez GET /v2/contacts/{id}.","properties":{"id":{"description":"Id kontaktu (GET /v2/contacts)","type":"integer","example":3921},"firstName":{"type":"string","example":"Anna","nullable":true},"lastName":{"type":"string","example":"Kowalska","nullable":true},"fullName":{"description":"Imię i nazwisko złożone przez CRM","type":"string","example":"Anna Kowalska"},"position":{"description":"Stanowisko","type":"string","example":"Kierownik zakupów","nullable":true},"email":{"description":"Główny adres e-mail kontaktu","type":"string","nullable":true},"phone":{"type":"string","nullable":true}},"type":"object"},"MailTemplateAttachment":{"description":"Plik dopięty do szablonu maila - CRM dokłada go do każdej wysyłki z tym szablonem. `downloadUrl` to podpisany, tymczasowy link (ważny 1 minutę); null = generowanie linków niedostępne w tym środowisku.","properties":{"id":{"description":"Identyfikator załącznika w szablonie (suma kontrolna treści) - używany przy usuwaniu","type":"string","example":"b783a276c5e20580e5a6b7079dcbc219"},"fileName":{"type":"string","example":"regulamin.pdf","nullable":true},"mimeType":{"type":"string","example":"application/pdf","nullable":true},"sizeBytes":{"type":"integer","example":171605,"nullable":true},"storagePath":{"description":"Ścieżka pliku w prywatnej przestrzeni plików instancji","type":"string","example":"webmail/templates/12/b783a276c5e20580e5a6b7079dcbc219.file"},"downloadUrl":{"description":"Podpisany, tymczasowy link do pobrania (ważny 1 minutę)","type":"string","nullable":true}},"type":"object"},"CustomFieldFile":{"description":"Plik w polu niestandardowym typu FILE - wartość takiego pola w kluczu\n`customField` przy odczycie rekordu (pole bez pliku ma null zamiast tego\nobiektu) i odpowiedź końcówek `/v2/{entity}/{id}/custom-fields/{key}/file`.\n\nPo plik idzie się przez `fileUrl`: `GET <fileUrl>` daje podpisany `downloadUrl`\n(ważny 1 minutę), a `GET <fileUrl>?download=1` przekierowuje prosto na plik.\nOdczyt rekordu `downloadUrl` NIE zawiera - podpisanie każdego pliku na stronie\nlisty kosztowałoby tyle, co sama lista.","properties":{"fileName":{"description":"Nazwa pliku nadana przy wgrywaniu","type":"string","example":"protokol.pdf","nullable":true},"mimeType":{"type":"string","example":"application/pdf","nullable":true},"sizeBytes":{"type":"integer","example":171605,"nullable":true},"storagePath":{"description":"Ścieżka pliku w prywatnej przestrzeni plików instancji","type":"string","example":"visibleFields/attachments/18270/9bcb5d.pdf"},"fileUrl":{"description":"Adres końcówki pliku tego pola - stamtąd bierze się link do pobrania. null dla encji bez końcówki plikowej (np. zadania)","type":"string","example":"/v2/note/18270/custom-fields/note_file_1/file","nullable":true},"downloadUrl":{"description":"Podpisany, tymczasowy link do pobrania (ważny 1 minutę). Wyłącznie w odpowiedzi końcówki pliku; null = generowanie linków niedostępne w tym środowisku","type":"string","nullable":true}},"type":"object"},"WikiBase":{"description":"Baza wiedzy. `type` rozróżnia artykuły od procedur - w CRM to dwa osobne zbiory.","properties":{"id":{"type":"integer","example":415},"name":{"type":"string","example":"Procedury obsługi zgłoszeń"},"subtitle":{"type":"string","nullable":true},"type":{"type":"string","enum":["article","procedure"],"example":"procedure"},"icon":{"description":"Ikona FontAwesome","type":"string","example":"fa-book","nullable":true},"color":{"type":"string","example":"#2196f3","nullable":true},"archived":{"type":"boolean","example":false},"order":{"type":"integer","example":1},"creatorUserId":{"type":"integer","example":1},"createdAt":{"type":"string","format":"date-time","nullable":true}},"type":"object"},"WikiCategory":{"description":"Kategoria w bazie wiedzy - każdy wpis należy do dokładnie jednej.","properties":{"id":{"type":"integer","example":220},"baseId":{"type":"integer","example":415},"name":{"type":"string","example":"Reklamacje"},"archived":{"type":"boolean","example":false},"order":{"type":"integer","example":1},"creatorUserId":{"type":"integer","example":1},"createdAt":{"type":"string","format":"date-time","nullable":true}},"type":"object"},"WikiEntry":{"description":"Wpis bazy wiedzy. `content` (HTML) występuje tylko w GET pojedynczego wpisu.","properties":{"id":{"type":"integer","example":1201},"baseId":{"type":"integer","example":415},"categoryId":{"type":"integer","example":220},"title":{"type":"string","example":"Zgłoszenie reklamacyjne - krok po kroku"},"content":{"description":"Treść w HTML (tylko w GET /v2/wiki/entries/{id})","type":"string","nullable":true},"alias":{"description":"Własny adres wpisu","type":"string","nullable":true},"published":{"type":"boolean","example":true},"archived":{"type":"boolean","example":false},"views":{"description":"Licznik odsłon w panelu","type":"integer","example":12},"helpful":{"description":"Licznik ocen „pomocne\"","type":"integer","example":3},"order":{"type":"integer","example":1},"creatorUserId":{"type":"integer","example":1},"createdAt":{"type":"string","format":"date-time","nullable":true},"updatedAt":{"type":"string","format":"date-time","nullable":true},"publishedAt":{"type":"string","format":"date-time","nullable":true}},"type":"object"},"Pagination":{"properties":{"page":{"type":"integer","example":1},"limit":{"type":"integer","example":100},"total":{"description":"Łączna liczba rekordów dla zapytania","type":"integer","example":5598},"pages":{"type":"integer","example":56}},"type":"object"},"Error":{"description":"Kontrakt błędu. `errors` (przy walidacji) wskazuje każde pole osobno.","properties":{"_error":{"properties":{"code":{"type":"integer","example":400},"message":{"type":"string","example":"validation.error"},"errors":{"type":"array","items":{"properties":{"field":{"type":"string","example":"updatedAfter"},"code":{"type":"string","example":"query.invalidDate"},"message":{"type":"string","example":"Parametr updatedAfter musi być datą ISO 8601."}},"type":"object"}}},"type":"object"}},"type":"object"},"WriteInfo":{"description":"Metadane operacji zapisu: utworzone id, warningi normalizacji (wartości niezapisane, bo nie dały się sprowadzić do standardu - z oryginalnym wejściem) i informacja o znalezionym duplikacie.","properties":{"created":{"description":"false = znaleziono istniejący rekord, nowy nie został utworzony","type":"boolean","example":true},"ids":{"description":"Identyfikatory rekordów dotkniętych zapisem. Klucz GŁÓWNY zależy od encji\nendpointu: `contractorId`, `contactId`, `noteId`, `leadId`, `ticketId`,\n`taskId`, `projectId`, `serviceId`, `pipelineItemId`, `productId`,\n`groupId`, `orderId`, `warehouseId` albo `key` (stany magazynowe - para\nmagazyn/produkt). Obok niego mogą pojawić się identyfikatory rekordów\nutworzonych przy okazji (np. `noteId` notatki systemowej i `contactId`\nosoby kontaktowej przy zakładaniu kontrahenta). Czytaj klucz właściwy\ndla wołanego endpointu - nie zakładaj stałego zestawu pól.","properties":{"contractorId":{"type":"integer","example":950201},"ownerUserId":{"type":"integer","example":12345},"noteId":{"description":"Id notatki systemowej (gdy createSystemNote)","type":"integer","example":4021},"contactId":{"description":"Id kontaktu (gdy createContractorContacts)","type":"integer","example":3701}},"type":"object"},"duplicate":{"description":"Znaleziony duplikat (null, gdy brak)","properties":{"matchedBy":{"description":"Pole, po którym znaleziono istniejący rekord - jedno z podanych w duplicateCheck. Poza wartościami z enuma także \"custom:<klucz>\" (pole niestandardowe) oraz klucze produktowe: externalId, sku, ean; companyName dotyczy leada.","type":"string","enum":["taxId","phone","email","name","domain","companyName","externalId","sku","ean","custom:<klucz>"],"example":"taxId"},"contractorId":{"description":"Id znalezionego kontrahenta (POST /v2/contractors). Przy leadzie: kontrahent, na którego znaleziony lead został już skonwertowany - tylko wtedy obecne.","type":"integer","example":121},"contactId":{"description":"Id znalezionego kontaktu (POST /v2/contacts)","type":"integer","example":3921},"leadId":{"description":"Id znalezionego leada (POST /v2/leads, od wersji API 2.13.0)","type":"integer","example":1532}},"type":"object","nullable":true},"warnings":{"description":"Pole → {valid: false, input: oryginalna wartość} dla wartości niezapisanych po normalizacji. Klucz `duplicateCheck` oznacza pola, po których NIE sprawdzono duplikatu, bo żądanie nie niosło ich wartości (lista w `input`).","type":"object","example":{"phone":{"valid":false,"input":"brak telefonu"}},"additionalProperties":{"type":"object"}}},"type":"object"},"CalendarEvent":{"description":"Wydarzenie w kalendarzu. Trzymane w usłudze kalendarzowej, powiązania (kontrahent, typ) po stronie CRM.","properties":{"id":{"description":"Identyfikator wydarzenia w usłudze kalendarzowej (UID).","type":"string","example":"00924a96-ebc2-45b9-84b1-a994bc1d2947"},"title":{"type":"string","example":"Spotkanie handlowe - ACME","nullable":true},"description":{"type":"string","example":"Omówienie oferty na 2027 rok.","nullable":true},"location":{"type":"string","example":"Warszawa, ul. Marszałkowska 10","nullable":true},"url":{"type":"string","nullable":true},"startAt":{"type":"string","format":"date-time","example":"2026-09-01T10:00:00+02:00","nullable":true},"endAt":{"type":"string","format":"date-time","example":"2026-09-01T11:00:00+02:00","nullable":true},"recurrenceRule":{"description":"Reguła powtarzania (FREQ, INTERVAL, BYDAY, UNTIL) - null dla wydarzeń jednorazowych.","type":"object","nullable":true},"organizer":{"description":"Organizator wydarzenia.","properties":{"name":{"type":"string","example":"Jan Kowalski"},"email":{"type":"string","example":"jan.kowalski@firma.pl"},"status":{"type":"string","example":"ACCEPTED","nullable":true}},"type":"object","nullable":true},"attendees":{"type":"array","items":{"properties":{"name":{"type":"string","example":"Anna Nowak"},"email":{"type":"string","example":"anna.nowak@acme.pl"},"status":{"description":"Odpowiedź uczestnika (ACCEPTED, DECLINED, TENTATIVE, NEEDS-ACTION).","type":"string","example":"NEEDS-ACTION","nullable":true}},"type":"object"}},"contractorId":{"description":"Kontrahent powiązany ze spotkaniem (GET /v2/contractors).","type":"integer","example":12345,"nullable":true},"contactId":{"description":"Kontakt powiązany ze spotkaniem (GET /v2/contacts). Wymaga nowszej wersji CRM - na starszej zawsze null, a zapis kończy się 501. Dostępne od wersji API 2.8.0.","type":"integer","example":3921,"nullable":true},"eventTypeId":{"type":"integer","example":3,"nullable":true},"eventTypeName":{"type":"string","example":"Spotkanie","nullable":true},"completed":{"description":"Czy spotkanie zostało odhaczone jako odbyte.","type":"boolean","example":false},"participationStatus":{"description":"Status uczestnictwa właściciela kalendarza.","type":"string","example":"ACCEPTED","nullable":true}},"type":"object"},"Calendar":{"description":"Kalendarz użytkownika albo zespołu. Typ mapuj słownikiem GET /v2/calendar/types.","properties":{"id":{"description":"Kontrakt kalendarza na modelu CRM App\\Model\\Calendar\\Calendar (tabela `Calendar`).\n\nPodział wart zapamiętania: SAM KALENDARZ (nazwa, typ, właściciel, dostępy, powiązana\nskrzynka) to wiersz w bazie tenanta i czytamy go modelem, jak każdą inną encję. WYDARZENIA\nżyją poza CRM - w usłudze kalendarzowej Tillio, do której core chodzi klientem HTTP\n(`TillioCalendarCore::createClient`), a w bazie zostaje tylko metadana wydarzenia\n(`Calendar__EventData`: powiązanie z kontrahentem, zakres dat, reguła powtarzania).\nDlatego listy wydarzeń NIE da się zbudować zapytaniem - patrz CalendarEventRepository.\n\nNie wystawiamy `c_tillio_cal_id` (wewnętrzny identyfikator kalendarza w usłudze - klient\nAPI operuje naszym `id`) ani `c_oauth_data` (dane autoryzacji, w modelu core oznaczone\njako pole chronione).","type":"integer","example":400},"name":{"type":"string","example":"Jan Kowalski"},"calendarTypeId":{"description":"Rodzaj kalendarza (Tillio, Microsoft, Google) - słownik GET /v2/calendar/types.","type":"integer","example":1},"ownerUserId":{"description":"Użytkownik, do którego kalendarz należy.","type":"integer","example":12345,"nullable":true},"mailAccountId":{"description":"Konto pocztowe powiązane z kalendarzem (GET /v2/mail/accounts). Wypełnione dla kalendarzy spiętych ze skrzynką - stamtąd biorą się zaproszenia przychodzące mailem. Null = kalendarz bez powiązanej skrzynki.","type":"integer","example":101,"nullable":true},"email":{"description":"Adres kalendarza - można na niego wysłać zaproszenie, trafi do tego kalendarza.","type":"string","example":"9aa36091-5907-4b27-ba13-87fd4ae4359f@calendar.tillio.app","nullable":true},"oauthEmail":{"description":"Konto zewnętrzne (Microsoft/Google), z którego kalendarz jest synchronizowany.","type":"string","example":"jan.kowalski@firma.pl","nullable":true},"oauthAuthorized":{"description":"Czy synchronizacja z kontem zewnętrznym ma ważną autoryzację. False przy kalendarzu Tillio i przy wygasłej zgodzie.","type":"boolean","example":false},"color":{"type":"string","example":"#0e6cbe","nullable":true},"allowExternalEvents":{"description":"Czy kalendarz przyjmuje zaproszenia z zewnątrz.","type":"boolean","example":true},"active":{"description":"Kalendarz aktywny (wyłączone zostają w bazie, ale nie są używane).","type":"boolean","example":true},"users":{"description":"Użytkownicy mający dostęp do kalendarza. `main: true` oznacza, że to GŁÓWNY kalendarz tego użytkownika - czyli ten, w którym domyślnie lądują jego spotkania.","type":"array","items":{"properties":{"userId":{"type":"integer","example":12345},"main":{"description":"Czy to główny kalendarz tego użytkownika","type":"boolean","example":true},"admin":{"description":"Poziom uprawnień: 0 podgląd, 1 edycja, 2 administrator","type":"integer","example":2},"accessTo":{"description":"Data, do której dostęp obowiązuje (null = bezterminowo)","type":"string","format":"date","example":null,"nullable":true}},"type":"object"}}},"type":"object"},"Lead":{"description":"Lead (szansa sprzedażowa przed konwersją na kontrahenta). `customField` to obiekt\nklucz→wartość surowa (select = id opcji - mapowanie w GET /v2/<encja>/custom-fields).\n`emails` to lista adresów e-mail leada (główny adres pierwszy).\n\n`contractorId`/`contactId` wypełnione oznaczają, że lead został powiązany\n(skonwertowany) z kontrahentem/kontaktem w CRM.","properties":{"id":{"description":"Jedno źródło kontraktu leada: pole API (camelCase, angielskie) ↔ wyrażenie SQL\nna modelu CRM App\\Model\\Leads\\Leads. Używane przez select, filtry, sort\ni dokumentację - kontrakt NIE zna nazw kolumn DB.\n\nfilter: 'exact' (=) | 'like' (%v%) | null (pole tylko w odpowiedzi).\nKlucze integracyjne (taxId, regon, postCode, country) celowo 'exact'.","type":"integer","example":659},"title":{"description":"Tytuł leada","type":"string","example":"Wdrożenie CRM - Acme"},"note":{"description":"Notatka (HTML z edytora CRM)","type":"string","nullable":true},"leadStatusId":{"description":"Status leada wg słownika CRM","type":"integer","example":6},"leadStageId":{"description":"Etap procesu (grupa statusów)","type":"integer","example":2},"statusChangeReasonId":{"description":"Powód ostatniej zmiany statusu","type":"integer","nullable":true},"categoryId":{"type":"integer","nullable":true},"contractorSourceId":{"description":"Źródło pozyskania (wspólny słownik ze źródłami kontrahenta)","type":"integer","example":1},"priority":{"description":"Priorytet (skala CRM, wyższa liczba = pilniejsze)","type":"integer","nullable":true},"ownerUserId":{"description":"Opiekun leada (id użytkownika systemu)","type":"integer","nullable":true},"creatorUserId":{"type":"integer","nullable":true},"contractorId":{"description":"Kontrahent po konwersji (id z GET /v2/contractors)","type":"integer","nullable":true},"contactId":{"description":"Kontakt po konwersji","type":"integer","nullable":true},"salesPipelineId":{"description":"Powiązana szansa w lejku sprzedaży","type":"integer","nullable":true},"companyName":{"type":"string","example":"Acme Sp. z o.o.","nullable":true},"taxId":{"description":"NIP (bez separatorów)","type":"string","example":"5252344078","nullable":true},"regon":{"type":"string","nullable":true},"domain":{"type":"string","example":"acme.pl","nullable":true},"firstName":{"description":"Imię osoby kontaktowej","type":"string","nullable":true},"lastName":{"description":"Nazwisko osoby kontaktowej. Przy zapisie musi przejść walidację CRM: 2-65 znaków - litery, spacje, myślnik, apostrof, kropka, przecinek; cyfra dozwolona tylko jako pierwszy znak (od wersji API 2.12.0 tak samo przy tworzeniu i aktualizacji).","type":"string","nullable":true},"position":{"description":"Stanowisko osoby kontaktowej","type":"string","nullable":true},"phone":{"type":"string","example":"+48221234567","nullable":true},"phoneAlternative":{"type":"string","nullable":true},"street":{"type":"string","nullable":true},"street2":{"type":"string","nullable":true},"postCode":{"type":"string","example":"00-624","nullable":true},"city":{"type":"string","example":"Warszawa","nullable":true},"region":{"type":"string","nullable":true},"district":{"type":"string","nullable":true},"country":{"description":"Kod kraju ISO 3166-1 alpha-2","type":"string","example":"PL","nullable":true},"acl":{"description":"ZNACZNIK PRYWATNOŚCI rekordu (inny mechanizm niż obiektowy ACL {userIds...} reszty API): 0/null = publiczny, >0 = prywatny dla użytkownika o tym id. Do respektowania ograniczeń dostępu po stronie konsumenta.","type":"integer","example":0,"nullable":true},"createdAt":{"type":"string","format":"date-time","example":"2026-08-01T09:30:00+02:00"},"updatedAt":{"description":"Ostatnia modyfikacja; dla rekordów nigdy niemodyfikowanych = createdAt. Klucz syncu przyrostowego.","type":"string","format":"date-time","example":"2026-08-12T14:05:11+02:00"},"closedAt":{"description":"Data zamknięcia leada","type":"string","format":"date-time","nullable":true},"lastActivityAt":{"description":"Ostatnia aktywność handlowa w CRM","type":"string","format":"date-time","nullable":true},"customField":{"type":"object","additionalProperties":{"nullable":true}},"emails":{"description":"Adresy e-mail leada: główny pierwszy, pozostałe w kolejności dodania. To samo pole przyjmuje zapis (od wersji API 2.13.0): w POST lista adresów do założenia, w PUT KOMPLETNA lista docelowa (zastępuje dotychczasową, `[]` usuwa wszystkie); pierwszy element = adres główny.","type":"array","items":{"type":"string","example":"jan.kowalski@acme.pl"}}},"type":"object"},"NoteAttachment":{"description":"Załącznik notatki. `downloadUrl` to podpisany, tymczasowy link do pobrania\npliku prosto z Google Cloud Storage - ważny 1 minutę od wygenerowania, potem\npobierz listę ponownie; null = generowanie niedostępne w tym środowisku\n(zostają metadane i `storagePath`).","properties":{"id":{"description":"Kontrakt załącznika notatki: pole API ↔ kolumna modelu CRM\nCRM_Note__NoteAttachments (prefix na_). Podzasób bez filtrów i sortowania\nz zewnątrz - lista zawsze chronologicznie. downloadUrl wyliczany\nw kontrolerze (StorageUrlSigner) - jak przy załącznikach zadań.","type":"integer","example":91},"fileName":{"description":"Oryginalna nazwa pliku nadana przy dodawaniu","type":"string","example":"protokol.pdf"},"mimeType":{"type":"string","example":"application/pdf","nullable":true},"sizeBytes":{"type":"integer","example":171605,"nullable":true},"creatorUserId":{"description":"Użytkownik, który dodał załącznik (GET /v2/users)","type":"integer","nullable":true},"createdAt":{"type":"string","format":"date-time","example":"2026-08-10T09:12:33+02:00"},"storagePath":{"description":"Ścieżka pliku w prywatnej przestrzeni plików instancji","type":"string","example":"note/attachments/2/cc752e.pdf","nullable":true},"downloadUrl":{"description":"Podpisany, tymczasowy link do pobrania (ważny 1 minutę)","type":"string","nullable":true}},"type":"object"},"Note":{"description":"Notatka CRM. Powiązanie polimorficzne: notatka należy do kontrahenta (`contractorId`)\nALBO do leada (`leadId`); opcjonalnie wskazuje usługę (`serviceId`) i proces\nsprzedażowy (`pipelineId`). Niezależnie od tego może być przypięta do kontaktów\n(`contactIds`, wiele-do-wielu) - notatka utworzona przez `POST /v2/contacts/{id}/notes`\nbez `contractorId` wisi WYŁĄCZNIE na kontakcie (`contractorId` i `leadId` = null).\nNotatka utworzona przez `POST /v2/leads/{leadId}/notes` (od wersji API 2.13.0) ma\n`leadId` = lead i `contractorId` = null - także gdy lead ma już przypisanego\nkontrahenta; przy konwersji leada CRM sam przepina jego notatki na kontrahenta\ni szansę sprzedaży.\n\n`customField` to obiekt klucz→wartość surowa (select = id opcji - mapowanie\nw GET /v2/note/custom-fields).\n\nCRM nie przechowuje daty modyfikacji notatki - brak pola `updatedAt`;\nsynchronizację przyrostową oprzyj o `createdAfter`.","properties":{"id":{"description":"Jedno źródło kontraktu notatki: pole API (camelCase, angielskie) ↔ wyrażenie SQL\nna modelu CRM (CRM_Note, prefiks n_). Używane przez select, filtry, sort\ni dokumentację - kontrakt NIE zna nazw kolumn DB.\n\nPowiązanie notatki jest POLIMORFICZNE: notatka wisi na kontrahencie (contractorId)\nALBO na leadzie (leadId) - core wymaga jednego z nich (req n_cc_id|n_ld_id).\nDodatkowo może wskazywać usługę (serviceId) i proces sprzedażowy (pipelineId).\nCRM wiąże notatki także z kalendarzem/VoIP/sentymentem - to celowo poza kontraktem v2.\nZapis pod każdą z trzech kotwic: POST /v2/contractors/{id}/notes, /v2/contacts/{id}/notes\n(2.10.0) i /v2/leads/{id}/notes (2.13.0, task-26 - notatka leada ma leadId, contractorId\nnull; konwersja leada przepina ją na kontrahenta po stronie core).\n\nCRM_Note NIE ma kolumny daty modyfikacji - stąd brak updatedAt/updatedAfter;\nprzyrost oprzyj o createdAfter. noteDate = data notatki widoczna w CRM\n(użytkownik może ją ręcznie ustawić - antydatowanie; brak ustawienia = createdAt).\n\nfilter: 'exact' (=) | 'like' (%v%) | null (pole tylko w odpowiedzi).","type":"integer","example":2},"contractorId":{"description":"Kontrahent, do którego należy notatka. Null, gdy notatka należy do leada.","type":"integer","example":121,"nullable":true},"leadId":{"description":"Lead, do którego należy notatka (zapis: POST /v2/leads/{leadId}/notes, filtr: GET /v2/notes?leadId=). Null, gdy notatka należy do kontrahenta.","type":"integer","example":659,"nullable":true},"serviceId":{"description":"Usługa powiązana z notatką (GET /v2/services)","type":"integer","nullable":true},"pipelineId":{"description":"Proces sprzedażowy powiązany z notatką","type":"integer","nullable":true},"noteTypeId":{"description":"Typ notatki (telefon, e-mail, spotkanie...) - słownik w GET /v2/note/types","type":"integer","example":1,"nullable":true},"url":{"description":"Adres notatki na osi czasu kartoteki firmy w CRM (dla ludzi - do powiadomień i linków „otwórz w CRM”). Notatka nie ma własnej strony, więc notatka bez kontrahenta (wisząca na samym kontakcie albo na leadzie) ma tu null. Od wersji API 2.12.0.","type":"string","example":"https://twoja-instancja.tillio.app/crm/contractors/121/#/tab=company/activities&noteId=904","nullable":true},"title":{"type":"string","example":"Rozmowa telefoniczna - oferta"},"body":{"description":"Treść notatki - surowy HTML z edytora WYSIWYG w CRM","type":"string","example":"<p>Klient prosi o ofertę na 20 stanowisk.</p>","nullable":true},"pinned":{"description":"Notatka przypięta na górze listy w CRM","type":"boolean","example":false},"creatorUserId":{"description":"Autor notatki (id użytkownika systemu)","type":"integer","example":1,"nullable":true},"noteDate":{"description":"Data notatki widoczna w CRM - użytkownik może ją ustawić ręcznie (antydatowanie); bez ręcznej zmiany = createdAt","type":"string","format":"date-time","example":"2026-08-10T11:00:00+02:00"},"createdAt":{"description":"Data utworzenia rekordu. Klucz syncu przyrostowego (createdAfter).","type":"string","format":"date-time","example":"2026-08-10T11:02:31+02:00"},"customField":{"type":"object","additionalProperties":{"nullable":true}},"contactIds":{"description":"Kontakty przypięte do notatki (GET /v2/contacts), w kolejności przypięcia; brak = pusta lista. Na instalacji CRM bez tej funkcji zawsze pusta lista (szczegóły osób: GET /v2/notes/{id}/contacts). Dostępne od wersji API 2.10.0.","type":"array","items":{"type":"integer"},"example":[3921]}},"type":"object"},"NoteTemplateCategory":{"description":"Kategoria szablonów notatek. Lista jest PŁASKA - drzewo składa się po\n`parentId` (null = poziom główny). Kategorię wskazuje pole `categoryId`\nw POST /v2/note/templates.","properties":{"id":{"description":"Kategorie szablonów notatek (drzewko przez parentId) - słownik `categoryId`\nz POST /v2/note/templates.\n\nREAD = direct modelem CRM `App\\Model\\CRM\\Note\\NoteTemplateCategory` (płaska\nlista, drzewo składa klient po parentId). WRITE przez klasę core\n`App\\Core\\Musq\\CRM\\Note\\NoteTemplate` (createCategory/updateCategory) - ta\nsama ścieżka co panel CRM.\n\nWalidacje przeniesione na naszą stronę, bo core ich nie ma albo zwraca gołe\n`error.formDataError` bez wskazania pola:\n  - name max 128 znaków (createCategory core wstawia BEZ validateData modelu),\n  - parentId musi istnieć (inaczej wisząca gałąź drzewa),\n  - cykl rodzica przy PUT (core nie sprawdza - kategoria mogłaby zostać\n    swoim własnym przodkiem i zniknąć z drzewka panelu).\n\nUprawnienie zapisu jak trasy panelu (Route::globalAcl): `note.templateAdmin`.","type":"integer","example":7},"name":{"description":"Nazwa kategorii (max 128 znaków)","type":"string","example":"Serwis"},"parentId":{"description":"Kategoria nadrzędna; null = poziom główny drzewka","type":"integer","example":3,"nullable":true},"priority":{"description":"Priorytet na liście (wyższy = wyżej)","type":"integer","example":0}},"type":"object"},"NoteTemplate":{"description":"Szablon notatki kontrahenta - gotowa treść (tytuł + HTML), z której użytkownik\nCRM tworzy notatkę jednym kliknięciem. `alias` jest zwracany w postaci zapisanej\nprzez CRM: prefiks `!`, małe litery, spacje zamienione na `_` (tak wpisuje się go\nw edytorze notatki).","properties":{"id":{"description":"Kontrakt szablonu notatki kontrahenta: pole API ↔ kolumna modelu CRM\nCRM_Note__NoteTemplate (prefix ntpl_). Na razie tylko serializacja\nodpowiedzi POST - bez listy, filtrów i sortowania.","type":"integer","example":17},"categoryId":{"description":"Kategoria szablonu w drzewku CRM; null = poziom główny","type":"integer","example":3,"nullable":true},"noteTypeId":{"description":"Typ notatki tworzonej z szablonu (GET /v2/note/types)","type":"integer","example":1},"name":{"description":"Nazwa szablonu widoczna na liście wyboru","type":"string","example":"Protokół rozmowy serwisowej"},"alias":{"description":"Skrót do wywołania szablonu w edytorze notatki (np. `!protokol`); null = bez aliasu","type":"string","example":"!protokol","nullable":true},"title":{"description":"Tytuł notatki tworzonej z szablonu","type":"string","example":"Protokół rozmowy serwisowej"},"body":{"description":"Treść notatki - surowy HTML (WYSIWYG w CRM)","type":"string","example":"<p>Ustalenia: ...</p>","nullable":true},"acl":{"description":"Ograniczenie widoczności szablonu ({userIds, departmentIds, groupIds}); null = widoczny dla wszystkich","properties":{"userIds":{"type":"array","items":{"type":"integer"},"example":[12345]},"departmentIds":{"type":"array","items":{"type":"integer"},"example":[]},"groupIds":{"type":"array","items":{"type":"integer"},"example":[]}},"type":"object","nullable":true},"priority":{"description":"Priorytet na liście szablonów (wyższy = wyżej)","type":"integer","example":0},"active":{"type":"boolean","example":true},"updatedAt":{"description":"Ostatnia zmiana szablonu (ISO 8601)","type":"string","format":"date-time","example":"2026-08-31T12:00:00+02:00","nullable":true},"updatedByUserId":{"description":"Użytkownik ostatniej zmiany (GET /v2/users)","type":"integer","example":12345,"nullable":true}},"type":"object"},"Order":{"description":"Nagłówek zamówienia. `products` tylko przy include=products (na liście) - w GET /v2/orders/{id} zawsze.\n\n`customField` to obiekt klucz→wartość; zamówienia obecnie NIE mają definicji pól niestandardowych\nw CRM, więc obiekt jest pusty - pole zostaje w kontrakcie dla spójności między encjami.\n\n`updatedAt` jest zawsze równe `createdAt` - CRM nie śledzi modyfikacji zamówień.","properties":{"id":{"description":"Jedno źródło kontraktu zamówienia: pole API (camelCase, angielskie) ↔ kolumna\nmodelu CRM Document__Order (prefix dco_). Używane przez select, filtry, sort\ni dokumentację - kontrakt NIE zna nazw kolumn DB (mapowane kontrakty I/O).\n\nMapowanie numerów (decyzja kontraktowa, patrz docs/journal.md):\n - number = dco_name - pełny numer dokumentu generowany ze schematu numeracji\n   CRM (np. \"PROF/CRM/20/08/2026\").\n - foreignNumber = dco_number - numer sekwencyjny INT, ustawialny przez\n   integrację przy zapisie - pełni rolę numeru obcego.\n\nupdatedAt = dco_add_date: tabela Document__Order NIE ma kolumny last_modified\n(CRM nie śledzi modyfikacji zamówień), więc updatedAt == createdAt, a filtr\nupdatedAfter łapie wyłącznie rekordy nowo utworzone. Pole zostaje w kontrakcie\ndla spójności syncu przyrostowego między encjami.\n\nfilter: 'exact' (=) | null (pole tylko w odpowiedzi). Zamknięta lista filtrów\nwg spec §B: id, number, foreignNumber, contractorId, orderStatusId + zakresy dat.","type":"integer","example":366},"number":{"description":"Pełny numer dokumentu generowany przez CRM ze schematu numeracji. Unikalny w praktyce - dobry klucz matchowania po stronie ERP.","type":"string","example":"PROF/CRM/20/08/2026"},"foreignNumber":{"description":"Numer sekwencyjny - może być nadany przez integrację przy zapisie (klucz wyszukiwania duplikatów zamówień).","type":"integer","example":20,"nullable":true},"contractorId":{"description":"Kontrahent zamówienia - szczegóły w GET /v2/contractors/{id}","type":"integer","example":950102},"ownerUserId":{"description":"Użytkownik prowadzący zamówienie (id użytkownika systemu)","type":"integer","nullable":true},"orderStatusId":{"description":"Status zamówienia - słownik w GET /v2/order/statuses","type":"integer","nullable":true},"note":{"type":"string","nullable":true},"totalAmount":{"description":"Suma zamówienia liczona przez CRM (pozycje minus rabaty) - string dla precyzji dziesiętnej","type":"string","example":"400.00","nullable":true},"currency":{"description":"Waluta ISO 4217","type":"string","example":"PLN","nullable":true},"place":{"description":"Miejsce wystawienia","type":"string","example":"Warszawa","nullable":true},"orderDate":{"description":"Data zamówienia","type":"string","format":"date","example":"2026-08-12"},"validUntil":{"description":"Data ważności oferty/zamówienia","type":"string","format":"date","nullable":true},"deliveryDate":{"description":"Data realizacji/dostawy","type":"string","format":"date","nullable":true},"sentDate":{"description":"Data wysłania zamówienia do klienta (dostępna od wersji API 1.24.0)","type":"string","format":"date","nullable":true},"createdAt":{"type":"string","format":"date-time","example":"2026-08-12T02:47:58+02:00"},"updatedAt":{"description":"Zawsze równe createdAt - CRM nie śledzi modyfikacji zamówień. updatedAfter łapie wyłącznie nowe rekordy.","type":"string","format":"date-time","example":"2026-08-12T02:47:58+02:00"},"customField":{"description":"Obecnie zawsze pusty obiekt - zamówienia nie mają pól niestandardowych w CRM","type":"object","example":[]},"products":{"description":"Pozycje zamówienia - na liście tylko przy include=products, w GET /v2/orders/{id} zawsze","type":"array","items":{"$ref":"#/components/schemas/OrderProduct"}}},"type":"object"},"OrderProduct":{"description":"Pozycja zamówienia. Wartości liczbowe jako stringi dla precyzji dziesiętnej.","properties":{"id":{"type":"integer","example":424},"productId":{"description":"Produkt z katalogu (szczegóły w GET /v2/products/{id}); null = pozycja z nazwą własną bez powiązania z katalogiem","type":"integer","example":235,"nullable":true},"name":{"description":"Nazwa pozycji: nazwa własna z zamówienia, a gdy brak - nazwa produktu z katalogu","type":"string","example":"Wdrożenie CRM","nullable":true},"price":{"description":"Cena jednostkowa","type":"string","example":"200.00"},"quantity":{"type":"string","example":"10.00"},"taxRate":{"description":"Stawka VAT w procentach","type":"string","example":"23.00"},"discount":{"description":"Rabat pozycji w procentach","type":"string","example":"10.00","nullable":true},"measure":{"description":"Jednostka miary (szt., kg, m2...)","type":"string","example":"szt.","nullable":true}},"type":"object"},"PhoneCall":{"description":"Połączenie telefoniczne w tabeli połączeń CRM - ten sam rekord, który zakładają\nintegracje VoIP (Focus, Play, Ringostat) i który liczy raport VoIP. `provider` mówi,\nskąd rekord pochodzi (`TillioCalls` = zapisany przez API); `source` + `sourceId` to\nidentyfikacja w systemie źródłowym (klucz idempotencji zapisu).\n\n`contractorIds` to kontrahenci, do których rozmowa jest przypięta (każde przypięcie\nma w CRM notatkę „Telefon\" na osi czasu kontrahenta - jej id w `noteIds`, tytuł\npierwszej w `title`). `summary` to podsumowanie rozmowy (HTML), które CRM pokazuje\nw treści tej notatki.\n\nCRM nie przechowuje daty modyfikacji połączenia - brak `updatedAt`; zakres czasu\nwybieraj po `startedAfter`/`startedBefore` (data rozmowy).","properties":{"id":{"description":"Jedno źródło kontraktu połączenia telefonicznego: pole API ↔ wyrażenie SQL na modelu\nCRM `VoIP__Call` (prefiks vcl_) z joinami słownika kodu statusu (`VoIP__CallStatusExternal`)\ni dostawcy (`VoIP__Provider`). Kontrakt NIE zna nazw kolumn DB.\n\nKierunek i status to w core PARY id (typ + kod zewnętrzny dostawcy) - w kontrakcie\nsą jednym słowem (`inbound`/`outbound`, `answered`/`missed`/...), liczonym z tych samych\nid, po których core rysuje kartotekę i raport VoIP (1 = przychodzące, 2 = wychodzące;\nstatusy 1-4 = odebrane/nieodebrane/zajęte/poczta, `failed` po kodzie FAILED).\n\n`ownNumber`/`remoteNumber` liczone jak `vcl_user_number`/`vcl_outer_number` w modelu core:\nprzychodzące = od rozmówcy (from) do nas (to), wychodzące odwrotnie.\n\nRekord NIE ma daty modyfikacji (model core jej nie trzyma - findings task-18), stąd brak\n`updatedAt`/`updatedAfter`; zakres czasu to `startedAfter`/`startedBefore` po dacie rozmowy.\n`contractorIds`, `noteIds` i `title` dokleja repozytorium (pivot i notatki, batch).\n\nfilter: 'exact' (=) | 'like' (%v%) | null (pole tylko w odpowiedzi).","type":"integer","example":2766},"provider":{"description":"Dostawca VoIP w CRM, który założył rekord: `TillioCalls` dla zapisów przez API, nazwy integracji (Focus, P4, Ringostat, Plus) dla synchronizacji natywnej.","type":"string","example":"TillioCalls"},"source":{"description":"System źródłowy podany przy zapisie (np. `tillio-calls`); null dla rekordów integracji natywnych.","type":"string","example":"tillio-calls","nullable":true},"sourceId":{"description":"Identyfikator rozmowy w systemie źródłowym. Razem z `source` tworzy klucz idempotencji POST.","type":"string","example":"call_01J8ZK3M9Q"},"direction":{"type":"string","enum":["inbound","outbound"],"example":"inbound"},"status":{"description":"`answered` = rozmowa odbyła się (wymaga `duration` > 0), `missed` = nieodebrane, `busy` = zajęte, `voicemail` = poczta głosowa, `failed` = nieudane (po stronie sieci).","type":"string","enum":["answered","missed","busy","voicemail","failed"],"example":"answered"},"ownNumber":{"description":"Numer własny (linia użytkownika CRM), format międzynarodowy.","type":"string","example":"+48731427000","nullable":true},"remoteNumber":{"description":"Numer rozmówcy, format międzynarodowy.","type":"string","example":"+48601123456"},"startedAt":{"description":"Początek rozmowy (strefa instancji).","type":"string","format":"date-time","example":"2026-09-05T10:15:00+02:00"},"duration":{"description":"Czas rozmowy w sekundach; 0 dla nieodebranych. Raport VoIP liczy rozmowę jako odebraną, gdy > 0.","type":"integer","example":184},"contactId":{"description":"Osoba kontaktowa (GET /v2/contacts), z którą prowadzono rozmowę.","type":"integer","example":3921,"nullable":true},"contractorIds":{"description":"Kontrahenci, do których rozmowa jest przypięta (kartoteka → zakładka połączeń, notatka na osi czasu).","type":"array","items":{"type":"integer"},"example":[121]},"userId":{"description":"Użytkownik CRM, którego jest rozmowa (właściciel linii).","type":"integer","example":7,"nullable":true},"title":{"description":"Tytuł notatki „Telefon\" (pierwszej z `noteIds`); domyślnie nadaje go CRM: „Telefon od/do <osoba albo numer>\".","type":"string","example":"Telefon od Jan Kowalski","nullable":true},"summary":{"description":"Podsumowanie rozmowy (HTML) - CRM pokazuje je w treści notatki.","type":"string","example":"<p>Klient pyta o termin wdrożenia.</p>","nullable":true},"tldr":{"description":"Jednozdaniowe streszczenie (tekst).","type":"string","nullable":true},"recordingCallId":{"description":"Identyfikator nagrania w systemie źródłowym (plik zostaje po jego stronie; CRM oznacza, że nagranie istnieje).","type":"string","nullable":true},"callsUrl":{"description":"Link do rozmowy w systemie źródłowym.","type":"string","format":"uri","nullable":true},"noteIds":{"description":"Notatki „Telefon\" spięte z rozmową (jedna na przypiętego kontrahenta).","type":"array","items":{"type":"integer"},"example":[18301]}},"type":"object"},"Project":{"description":"Projekt. `customField` to obiekt klucz→wartość surowa (select = id opcji - mapowanie\nw GET /v2/project/custom-fields). Liczniki `tasks*Count` pochodzą z danych\ngenerowanych CRM (odświeżane przy zmianach zadań); null = projekt bez policzonych danych.","properties":{"id":{"description":"Jedno źródło kontraktu projektu: pole API (camelCase, angielskie) ↔ wyrażenie SQL\nna modelach CRM (Project + join Project__ProjectGeneratedData dla liczników zadań).\nKontrakt NIE zna nazw kolumn DB (mapowane kontrakty I/O).\n\nupdatedAt = p_last_activity: kolumna timestamp ON UPDATE CURRENT_TIMESTAMP -\nbaza sama podbija ją przy KAŻDEJ modyfikacji wiersza projektu, a przy insercie\ninicjuje datą utworzenia. NOT NULL, więc bez gałęzi NULL→createdAt (inaczej\nniż kontrahenci, gdzie cc_last_modified bywa NULL).\n\nfilter: 'exact' (=) | 'like' (%v%) | null (pole tylko w odpowiedzi).","type":"integer","example":27},"name":{"type":"string","example":"Wdrożenie ERP - Acme"},"description":{"description":"Opis projektu (może zawierać HTML z edytora CRM)","type":"string","nullable":true},"contractorId":{"description":"Kontrahent powiązany (GET /v2/contractors/{id})","type":"integer","example":121,"nullable":true},"projectStatusId":{"description":"Status projektu wg słownika CRM (Project__ProjectStatus)","type":"integer","example":1,"nullable":true},"ownerUserId":{"description":"Właściciel/lider projektu (id użytkownika systemu)","type":"integer","example":7},"creatorUserId":{"description":"Twórca projektu","type":"integer","example":3},"archived":{"description":"Czy projekt zarchiwizowany","type":"boolean","example":false},"startDate":{"description":"Data startu (bez czasu)","type":"string","format":"date","example":"2026-08-01","nullable":true},"dueDate":{"description":"Planowany termin zakończenia (bez czasu)","type":"string","format":"date","example":"2026-12-31","nullable":true},"createdAt":{"type":"string","format":"date-time","example":"2026-08-01T09:30:00+02:00"},"updatedAt":{"description":"Ostatnia modyfikacja projektu (baza podbija automatycznie). Klucz syncu przyrostowego.","type":"string","format":"date-time","example":"2026-08-12T14:05:11+02:00"},"tasksCount":{"description":"Liczba zadań w projekcie","type":"integer","example":24,"nullable":true},"tasksDoneCount":{"type":"integer","example":10,"nullable":true},"tasksUndoneCount":{"type":"integer","example":14,"nullable":true},"tasksOverdueCount":{"type":"integer","example":2,"nullable":true},"customField":{"type":"object","example":{"project_str_1":"ERP-2026"},"additionalProperties":{"nullable":true}}},"type":"object"},"Service":{"description":"Usługa sprzedana/przypisana kontrahentowi. `catalogId`/`catalogName` wskazują pozycję\nkatalogu usług (GET /v2/service/catalog), `customName` to nazwa własna tej instancji\nusługi w CRM. Pola `agreement*` (daty umowy) są null, gdy usługa nie ma umowy.\n\n`customField` to obiekt klucz→wartość surowa (select = id opcji - mapowanie\nw GET /v2/service/custom-fields).\n\nSynchronizacja przyrostowa: `updatedAfter` po `updatedAt` (data modyfikacji\nutrzymywana przez bazę CRM).","properties":{"id":{"description":"Jedno źródło kontraktu usługi: pole API (camelCase, angielskie) ↔ wyrażenie SQL\nna modelach CRM (CRM_Service, prefiks sv_ + join Service__Names svn_ + join\nCRM_Service__ServiceAgree sva_). Używane przez select, filtry, sort i dokumentację.\n\nsv_last_modified (ON UPDATE CURRENT_TIMESTAMP - utrzymywana przez DB) niesie\nupdatedAt/updatedAfter. Daty umowy (agreement*) przychodzą z tabeli 1:1\nCRM_Service__ServiceAgree - null, gdy usługa nie ma umowy.\n\nfilter: 'exact' (=) | 'like' (%v%) | null (pole tylko w odpowiedzi).","type":"integer","example":412},"contractorId":{"description":"Kontrahent, do którego należy usługa","type":"integer","example":121},"catalogId":{"description":"Pozycja katalogu usług (GET /v2/service/catalog)","type":"integer","example":7,"nullable":true},"catalogName":{"description":"Nazwa pozycji katalogu","type":"string","example":"Abonament serwisowy","nullable":true},"customName":{"description":"Nazwa własna tej instancji usługi w CRM","type":"string","example":"Abonament serwisowy - oddział Warszawa"},"serviceStatusId":{"description":"Status usługi - słownik statusów wg konfiguracji CRM","type":"integer","example":1,"nullable":true},"billingPeriodId":{"description":"Okres rozliczeniowy (słownik CRM)","type":"integer","nullable":true},"paymentTermId":{"description":"Termin płatności (słownik CRM)","type":"integer","nullable":true},"invoiceTypeId":{"description":"Typ faktury (słownik CRM)","type":"integer","nullable":true},"salesUserId":{"description":"Handlowiec, który sprzedał usługę (id użytkownika systemu)","type":"integer","nullable":true},"ownerUserId":{"description":"Opiekun odpowiedzialny za realizację (id użytkownika systemu)","type":"integer","nullable":true},"place":{"description":"Miejsce realizacji","type":"string","nullable":true},"note":{"description":"Notatka do usługi","type":"string","nullable":true},"currency":{"type":"string","example":"PLN","nullable":true},"payValue":{"description":"Wartość sprzedaży - string dla precyzji dziesiętnej","type":"string","example":"1500.00","nullable":true},"costValue":{"description":"Koszt - string dla precyzji dziesiętnej","type":"string","example":"900.00","nullable":true},"salesDate":{"description":"Data sprzedaży","type":"string","format":"date","example":"2026-07-01","nullable":true},"agreementDate":{"description":"Data zawarcia umowy","type":"string","format":"date","nullable":true},"agreementFrom":{"description":"Obowiązywanie umowy od","type":"string","format":"date","nullable":true},"agreementTo":{"description":"Obowiązywanie umowy do","type":"string","format":"date","nullable":true},"agreementEnd":{"description":"Data zakończenia umowy","type":"string","format":"date","nullable":true},"agreementTermination":{"description":"Data wypowiedzenia umowy","type":"string","format":"date","nullable":true},"createdAt":{"description":"Data utworzenia rekordu","type":"string","format":"date-time","example":"2026-07-01T10:15:00+02:00"},"updatedAt":{"description":"Data ostatniej modyfikacji (utrzymywana przez DB). Klucz syncu przyrostowego (updatedAfter).","type":"string","format":"date-time","example":"2026-08-10T09:30:00+02:00"},"customField":{"type":"object","additionalProperties":{"nullable":true}}},"type":"object"},"ServiceCatalogItem":{"description":"Pozycja katalogu (cennika) usług - szablon, z którego powstają usługi kontrahentów\n(`GET /v2/services`, pole `catalogId`). Wartości `defaultPayValue`/`defaultTax`\nto domyślne stawki podpowiadane przy sprzedaży, nie ceny konkretnych usług.","properties":{"id":{"description":"Jedno źródło kontraktu pozycji katalogu usług: pole API (camelCase, angielskie)\n↔ wyrażenie SQL na modelach CRM (Service__Names, prefiks svn_ + join\nService__NameGroup svng_). Używane przez select, filtry, sort i dokumentację.\n\nKatalog to słownik konfiguracyjny - brak dat utworzenia/modyfikacji w CRM,\nstąd brak createdAt/updatedAt i zakresów dat.\n\nfilter: 'exact' (=) | 'like' (%v%) | null (pole tylko w odpowiedzi).","type":"integer","example":7},"name":{"type":"string","example":"Abonament serwisowy"},"icon":{"description":"Identyfikator ikony w CRM","type":"string","nullable":true},"active":{"description":"Pozycja aktywna (dostępna przy sprzedaży)","type":"boolean","example":true},"isAgreement":{"description":"Usługa z tej pozycji wiąże się z umową (daty agreement* w GET /v2/services)","type":"boolean","example":false},"defaultPayValue":{"description":"Domyślna cena - string dla precyzji dziesiętnej","type":"string","example":"1500.00","nullable":true},"currency":{"description":"Waluta domyślnej ceny","type":"string","example":"PLN","nullable":true},"defaultTax":{"description":"Domyślna stawka VAT (procent) - string dla precyzji dziesiętnej","type":"string","example":"23.00","nullable":true},"priority":{"description":"Kolejność wyświetlania w CRM (mniejsza = wyżej)","type":"integer","nullable":true},"groupId":{"description":"Grupa katalogu usług","type":"integer","nullable":true},"groupName":{"type":"string","example":"Serwis","nullable":true},"groupColor":{"description":"Kolor grupy (hex)","type":"string","example":"#3b82f6","nullable":true}},"type":"object"},"Stock":{"description":"Stan magazynowy produktu w magazynie. Jedna pozycja = jedna para magazyn+produkt (ewentualne duplikaty wpisów w CRM są sumowane).","properties":{"warehouseId":{"description":"Magazyn - szczegóły w GET /v2/warehouses","type":"integer","example":1},"productId":{"description":"Produkt - szczegóły w GET /v2/products (w przygotowaniu)","type":"integer","example":235},"quantity":{"description":"Stan - string dla precyzji dziesiętnej (DECIMAL 10,2)","type":"string","example":"12.00"}},"type":"object"},"SystemUser":{"description":"Użytkownik systemu - m.in. do mapowania opiekuna kontrahenta (`ownerUserId` z GET /v2/contractors) na osobę.","properties":{"id":{"description":"Kontrakt użytkownika systemu (spec §B) na modelu CRM App\\Model\\Musq\\Users.\nWYŁĄCZNIE dane niewrażliwe: id, firstName, lastName, email, userStatusId - żadnych\nhaseł, tokenów, ustawień 2FA ani telefonów. Konta ghost (u_ghost=1) nigdy nie\nwychodzą przez API.","type":"integer","example":5},"firstName":{"type":"string","example":"Jan"},"lastName":{"type":"string","example":"Kowalski","nullable":true},"email":{"description":"Login systemowy - unikalny w instancji","type":"string","example":"jan.kowalski@firma.pl"},"userStatusId":{"description":"Status konta wg słownika CRM (typowo: 1 aktywny, 2 nieaktywny, 3 zawieszony, 4 usunięty)","type":"integer","example":1,"nullable":true}},"type":"object"},"TaskAttachment":{"description":"Załącznik zadania. `downloadUrl` to podpisany, tymczasowy link do pobrania pliku\nprosto z Google Cloud Storage - ważny 1 minutę od wygenerowania, potem pobierz\nlistę ponownie. `downloadUrl` = null, gdy instancja nie trzyma plików w GCS albo\nśrodowisko nie ma kluczy podpisu - wtedy zostają metadane i `storagePath`\n(stabilna ścieżka pliku w prywatnej przestrzeni plików instancji) jako korelacja\npod przyszły endpoint binarny. `commentId` wskazuje komentarz, w którym plik\ndodano (GET /v2/tasks/{id}/comments); null = załącznik dodany wprost do zadania.","properties":{"id":{"description":"Kontrakt załącznika zadania: pole API ↔ kolumna modelu CRM Tasks__TasksAttachments.\nPodzasób bez filtrów i sortowania z zewnątrz - lista zawsze chronologicznie.\n\ndownloadUrl NIE jest w FIELDS - to pole wyliczane w kontrolerze\n(StorageUrlSigner, podpisany URL GCS), nie kolumna DB.","type":"integer","example":470},"fileName":{"description":"Oryginalna nazwa pliku nadana przy dodawaniu","type":"string","example":"oferta-acme.pdf"},"mimeType":{"description":"Typ MIME zapisany przy dodawaniu pliku","type":"string","example":"application/pdf","nullable":true},"sizeBytes":{"description":"Rozmiar pliku w bajtach","type":"integer","example":171605,"nullable":true},"creatorUserId":{"description":"Użytkownik, który dodał załącznik (GET /v2/users)","type":"integer","example":7,"nullable":true},"commentId":{"description":"Komentarz, w którym dodano załącznik; null = dodany wprost do zadania","type":"integer","example":null,"nullable":true},"createdAt":{"type":"string","format":"date-time","example":"2026-08-10T09:12:33+02:00"},"storagePath":{"description":"Ścieżka pliku w prywatnej przestrzeni plików instancji - stały identyfikator pod integracje i przyszły endpoint binarny","type":"string","example":"tasks/attachments/22/cc752e.pdf","nullable":true},"downloadUrl":{"description":"Podpisany, tymczasowy link do pobrania (ważny 1 minutę); null = generowanie niedostępne w tym środowisku","type":"string","example":"https://storage.googleapis.com/...","nullable":true}},"type":"object"},"TaskComment":{"description":"Komentarz zadania. `body` może zawierać HTML z edytora CRM. `parentCommentId`\nwskazuje komentarz nadrzędny (odpowiedź w wątku); null = komentarz główny.\n`updatedAt` i `editorUserId` opisują ostatnią edycję; null = komentarz nigdy\nnieedytowany. Załączniki dodane w komentarzu znajdziesz w\nGET /v2/tasks/{id}/attachments po polu `commentId`.","properties":{"id":{"description":"Kontrakt komentarza zadania: pole API (camelCase, angielskie) ↔ kolumna modelu\nCRM Tasks__TasksComments. Podzasób bez filtrów i sortowania z zewnątrz -\nlista zawsze chronologicznie (filter/sort wszędzie null/false).","type":"integer","example":679},"body":{"description":"Treść komentarza (może zawierać HTML z edytora CRM); null = komentarz techniczny bez treści (np. sam załącznik)","type":"string","example":"<p>Oferta wysłana, czekamy na odpowiedź.</p>","nullable":true},"creatorUserId":{"description":"Autor komentarza (id użytkownika z GET /v2/users)","type":"integer","example":7,"nullable":true},"parentCommentId":{"description":"Komentarz nadrzędny (odpowiedź w wątku); null = komentarz główny","type":"integer","example":null,"nullable":true},"createdAt":{"type":"string","format":"date-time","example":"2026-08-10T09:12:33+02:00"},"updatedAt":{"description":"Data ostatniej edycji; null = komentarz nigdy nieedytowany","type":"string","format":"date-time","nullable":true},"editorUserId":{"description":"Użytkownik, który ostatnio edytował komentarz; null = brak edycji","type":"integer","nullable":true}},"type":"object"},"Task":{"description":"Zadanie. `customField` to obiekt klucz→wartość surowa (select = id opcji - mapowanie\nw GET /v2/task/custom-fields). `assignedUserIds` to lista id użytkowników\nprzypisanych do zadania (wykonawców). `done`: czy zadanie wykonane (wyliczane\nz `endDate`; szczegółowe statusy per wykonawca w GET /v2/task/statuses,\nzapis przez `taskStatusId`).","properties":{"id":{"description":"Jedno źródło kontraktu zadania: pole API (camelCase, angielskie) ↔ wyrażenie SQL\nna modelach CRM (Tasks + join pivotu Project__ProjectTasks dla projectId).\nKontrakt NIE zna nazw kolumn DB (mapowane kontrakty I/O).\n\nupdatedAt = t_last_modified (ON UPDATE CURRENT_TIMESTAMP - utrzymywana przez DB,\nkolumna doszła w core 2026-08; wcześniej v2 liczyło updatedAt z dziennika zmian\nHistory__TasksHistory).\n\ndone to uproszczony stan całego zadania (wykonane / niewykonane), wyliczany\nz daty wykonania (endDate). Statusy per przypisany użytkownik żyją\nw Tasks__TasksUsers (zapis: taskStatusId).\n\nfilter: 'exact' (=) | 'like' (%v%) | null (pole tylko w odpowiedzi).","type":"integer","example":22},"title":{"type":"string","example":"Przygotować ofertę dla Acme"},"description":{"description":"Treść zadania (może zawierać HTML z edytora CRM)","type":"string","nullable":true},"done":{"description":"Czy zadanie wykonane (wyliczane z endDate)","type":"boolean","example":false},"taskTypeId":{"description":"Typ zadania wg słownika CRM","type":"integer","nullable":true},"priority":{"description":"0 = standard, 1 = wysoki, 2 = najwyższy","type":"integer","example":0,"nullable":true},"estimatedTime":{"description":"Szacowany czas w minutach","type":"integer","example":90,"nullable":true},"contractorId":{"description":"Kontrahent powiązany (GET /v2/contractors/{id})","type":"integer","example":121,"nullable":true},"contactId":{"description":"Kontakt powiązany z zadaniem (GET /v2/contacts). Wymaga nowszej wersji CRM - na starszej instalacji pola nie ma w odpowiedzi ani w filtrach, a zapis odrzuca je jako nieznane. Dostępne od wersji API 2.8.0.","type":"integer","example":3921,"nullable":true},"leadId":{"description":"Lead powiązany","type":"integer","nullable":true},"pipelineItemId":{"description":"Szansa sprzedaży powiązana z zadaniem (GET /v2/pipeline/items/{id}). Dostępne od wersji API 2.13.0.","type":"integer","example":2716,"nullable":true},"projectId":{"description":"Projekt, do którego należy zadanie (GET /v2/projects/{id}); null = zadanie poza projektem","type":"integer","example":27,"nullable":true},"ownerUserId":{"description":"Właściciel zadania (id użytkownika systemu)","type":"integer","example":7,"nullable":true},"creatorUserId":{"description":"Twórca zadania","type":"integer","example":3,"nullable":true},"archived":{"description":"Czy zadanie zarchiwizowane","type":"boolean","example":false},"startDate":{"type":"string","format":"date-time","example":"2026-08-10T09:00:00+02:00","nullable":true},"dueDate":{"description":"Termin wykonania","type":"string","format":"date-time","example":"2026-08-20T17:00:00+02:00","nullable":true},"endDate":{"description":"Data wykonania zadania; null = niewykonane","type":"string","format":"date-time","nullable":true},"createdAt":{"type":"string","format":"date-time","example":"2026-08-01T09:30:00+02:00"},"updatedAt":{"description":"Data ostatniej modyfikacji (utrzymywana przez DB). Klucz syncu przyrostowego (updatedAfter).","type":"string","format":"date-time","example":"2026-08-12T14:05:11+02:00"},"assignedUserIds":{"description":"Id użytkowników przypisanych do zadania (wykonawcy)","type":"array","items":{"type":"integer"},"example":[7,12]},"customField":{"type":"object","example":{"tasks_int_1":"5"},"additionalProperties":{"nullable":true}}},"type":"object"},"TextMessage":{"description":"Wiadomość SMS w tabeli wiadomości CRM - ten sam rekord, który zakładają integracje\nVoIP (Play, Focus). `provider` mówi, skąd rekord pochodzi (`TillioCalls` = zapisany\nprzez API); `source` + `sourceId` to identyfikacja w systemie źródłowym (klucz\nidempotencji zapisu).\n\n`contractorIds` to kontrahenci, do których wiadomość jest przypięta - każde\nprzypięcie ma w CRM notatkę „SMS\" z treścią wiadomości na osi czasu kontrahenta\n(id w `noteIds`, tytuł pierwszej w `title`).\n\nStatus: wiadomość przychodząca zawsze `received`; wychodząca `sent` → `delivered`\nalbo `failed`. CRM nie przechowuje daty modyfikacji - zakres czasu wybieraj po\n`sentAfter`/`sentBefore`.","properties":{"id":{"description":"Jedno źródło kontraktu wiadomości SMS: pole API ↔ wyrażenie SQL na modelu CRM\n`VoIP__Message` (prefiks vm_) z joinami słownika statusu (`VoIP__MessageStatus`)\ni dostawcy (`VoIP__Provider`). Kontrakt NIE zna nazw kolumn DB.\n\nKierunek z `vm_vmt_id` (1 = przychodząca, 2 = wychodząca - stałe core), status z kodu\n`VoIP__MessageStatus`: core nie ma statusu „odebrana\", więc każda przychodząca\nto `received`; wychodząca: `sent` / `delivered` / `failed` (findings task-19).\n\n`ownNumber`/`remoteNumber` jak `vm_user_number`/`vm_contact_number` w modelu core.\nRekord NIE ma daty modyfikacji - zakres czasu to `sentAfter`/`sentBefore` po dacie wysłania.\n`contractorIds`, `noteIds` i `title` dokleja repozytorium (pivot i notatki po `n_vm_id`).\n\nfilter: 'exact' (=) | 'like' (%v%) | null (pole tylko w odpowiedzi).","type":"integer","example":512},"provider":{"description":"Dostawca VoIP w CRM, który założył rekord: `TillioCalls` dla zapisów przez API, nazwy integracji natywnych dla synchronizacji.","type":"string","example":"TillioCalls"},"source":{"description":"System źródłowy podany przy zapisie; null dla rekordów integracji natywnych.","type":"string","example":"tillio-calls","nullable":true},"sourceId":{"description":"Identyfikator wiadomości w systemie źródłowym. Razem z `source` tworzy klucz idempotencji POST.","type":"string","example":"sms_01J8ZK3M9Q"},"direction":{"type":"string","enum":["inbound","outbound"],"example":"inbound"},"status":{"type":"string","enum":["received","sent","delivered","failed"],"example":"received"},"ownNumber":{"description":"Numer własny (linia użytkownika CRM), format międzynarodowy.","type":"string","example":"+48731427000","nullable":true},"remoteNumber":{"description":"Numer rozmówcy, format międzynarodowy.","type":"string","example":"+48601123456"},"body":{"description":"Treść wiadomości (do 1024 znaków - limit CRM).","type":"string","example":"Dzień dobry, proszę o kontakt w sprawie oferty."},"sentAt":{"description":"Data wysłania / odebrania (strefa instancji).","type":"string","format":"date-time","example":"2026-09-05T10:15:00+02:00"},"deliveredAt":{"description":"Data doręczenia (wychodzące); null, gdy brak potwierdzenia.","type":"string","format":"date-time","nullable":true},"contactId":{"description":"Osoba kontaktowa (GET /v2/contacts).","type":"integer","example":3921,"nullable":true},"contractorIds":{"description":"Kontrahenci, do których wiadomość jest przypięta.","type":"array","items":{"type":"integer"},"example":[121]},"userId":{"description":"Użytkownik CRM, którego jest wiadomość (właściciel linii).","type":"integer","example":7,"nullable":true},"title":{"description":"Tytuł notatki „SMS\" (pierwszej z `noteIds`), nadawany przez CRM.","type":"string","example":"SMS od Jan Kowalski","nullable":true},"callsUrl":{"description":"Link do wiadomości w systemie źródłowym.","type":"string","format":"uri","nullable":true},"noteIds":{"description":"Notatki „SMS\" spięte z wiadomością (jedna na przypiętego kontrahenta).","type":"array","items":{"type":"integer"},"example":[18302]}},"type":"object"},"Ticket":{"description":"Zgłoszenie (ticket). `customField` to obiekt klucz→wartość surowa (select = id opcji -\nmapowanie w GET /v2/<encja>/custom-fields).\n\nSynchronizacja przyrostowa: `updatedAfter` po `updatedAt` - data modyfikacji\nutrzymywana przez bazę CRM, łapie każdą zmianę rekordu zgłoszenia.","properties":{"id":{"description":"Jedno źródło kontraktu ticketa (zgłoszenia): pole API (camelCase, angielskie) ↔\nwyrażenie SQL na modelu CRM App\\Model\\Tickets\\Tickets. Używane przez select,\nfiltry, sort i dokumentację - kontrakt NIE zna nazw kolumn DB.\n\nfilter: 'exact' (=) | 'like' (%v%) | null (pole tylko w odpowiedzi).\n\nupdatedAt = tck_last_modified (ON UPDATE CURRENT_TIMESTAMP - utrzymywana przez DB,\nkolumna doszła w core 2026-08; wcześniej v2 przybliżało przez COALESCE\nlast_response/create).","type":"integer","example":1},"title":{"description":"Temat zgłoszenia","type":"string","example":"Brak faktury za lipiec"},"description":{"description":"Treść zgłoszenia (HTML z edytora CRM)","type":"string","nullable":true},"ticketStatusId":{"description":"Status zgłoszenia wg słownika CRM","type":"integer","example":1},"ticketSourceId":{"description":"Źródło zgłoszenia (mail, telefon, formularz...)","type":"integer","nullable":true},"ticketStageId":{"description":"Etap procesu obsługi zgłoszeń","type":"integer","example":3},"priority":{"description":"Priorytet (skala CRM, wyższa liczba = pilniejsze)","type":"integer","nullable":true},"contractorId":{"description":"Kontrahent zgłaszający (id z GET /v2/contractors)","type":"integer","nullable":true},"contactId":{"description":"Osoba zgłaszająca (główny powiązany kontakt z GET /v2/contacts). Ustawiane przez contactId w POST/PUT. Od wersji API 1.34.0 w odczycie.","type":"integer","example":50,"nullable":true},"serviceId":{"description":"Usługa/punkt kontrahenta, której dotyczy zgłoszenie (główne powiązanie z GET /v2/services). Ustawiane przez serviceId w POST/PUT. Od wersji API 1.34.0 w odczycie.","type":"integer","example":412,"nullable":true},"ownerUserId":{"description":"Użytkownik prowadzący zgłoszenie","type":"integer","nullable":true},"creatorUserId":{"description":"Użytkownik, który utworzył zgłoszenie","type":"integer","nullable":true},"lastResponseUserId":{"description":"Autor ostatniej odpowiedzi","type":"integer","nullable":true},"email":{"description":"E-mail kontaktowy zgłaszającego","type":"string","example":"klient@acme.pl","nullable":true},"open":{"description":"true = otwarte (do wersji API 2.0.3 zwracane jako string \"0\"/\"1\")","type":"boolean","example":true,"nullable":true},"archived":{"description":"true = zarchiwizowane (do wersji API 2.0.3 zwracane jako string \"0\"/\"1\")","type":"boolean","nullable":true},"estimatedTime":{"description":"Szacowany czas obsługi (sekundy)","type":"integer","nullable":true},"relatedTicketId":{"description":"Zgłoszenie powiązane","type":"integer","nullable":true},"uuId":{"description":"Publiczny identyfikator panelu klienta","type":"string","example":"0b9e9d1c-6a48-4a10-9d0f-1e0f4a9a2b11","nullable":true},"clientPanelUrl":{"description":"Gotowy link online dla klienta (panel zgłoszenia na tickets.tillio.pl). Null = panel nieutworzony - tworzy go flaga createClientPanel w POST /v2/tickets.","type":"string","example":"https://tickets.tillio.pl/ticket/0b9e9d1c-6a48-4a10-9d0f-1e0f4a9a2b11","nullable":true},"resolutionAt":{"description":"Termin realizacji (deadline)","type":"string","format":"date-time","nullable":true},"endedAt":{"description":"Data zakończenia zgłoszenia","type":"string","format":"date-time","nullable":true},"lastResponseAt":{"description":"Ostatnia odpowiedź w wątku","type":"string","format":"date-time","nullable":true},"createdAt":{"type":"string","format":"date-time","example":"2026-08-01T09:30:00+02:00"},"updatedAt":{"description":"Data ostatniej modyfikacji (utrzymywana przez DB). Klucz syncu przyrostowego (updatedAfter).","type":"string","format":"date-time","example":"2026-08-12T14:05:11+02:00"},"customField":{"type":"object","additionalProperties":{"nullable":true}},"url":{"description":"Adres karty zgłoszenia w interfejsie CRM (dla ludzi - do powiadomień, formularzy, raportów). Karta otwiera się jako modal nad listą, stąd kotwica w adresie. Od wersji API 1.38.0.","type":"string","example":"https://twoja-instancja.tillio.app/tickets/view/table#/tab=tickets-tabs/general&modal=ticket-read/tck_id:29","nullable":true}},"type":"object"},"Warehouse":{"description":"Magazyn - słownik dla stanów magazynowych (GET /v2/stocks).","properties":{"id":{"description":"Kontrakt magazynu (spec §B): id, name, symbol, status (+ createdAt) na modelu\nCRM App\\Model\\Warehouse\\Warehouse. Tabela nie ma kolumny modyfikacji, więc encja\nnie wystawia updatedAt ani syncu przyrostowego - magazynów jest kilka, pobiera się całość.","type":"integer","example":1},"name":{"type":"string","example":"Magazyn Centralny"},"symbol":{"description":"Unikalny symbol magazynu (klucz naturalny do matchowania z ERP)","type":"string","example":"MAIN"},"status":{"description":"1 aktywny, 0 nieaktywny","type":"integer","example":1,"nullable":true},"createdAt":{"type":"string","format":"date-time","example":"2026-08-01T09:30:00+02:00"}},"type":"object"},"Contact":{"description":"Kontakt (osoba). Kontakt może być powiązany z wieloma kontrahentami: `contractorId` to kontrahent główny (najwyższy priorytet powiązania w CRM), `contractorIds` - wszystkie powiązania. `email` to adres główny kontaktu. `customField` to obiekt klucz→wartość surowa (definicje w GET /v2/contact/custom-fields).","properties":{"id":{"type":"integer","example":50},"firstName":{"type":"string","example":"Jan"},"lastName":{"description":"Nazwisko. Przy zapisie (POST/PUT) musi przejść walidację CRM: 2-65 znaków - litery, spacje, myślnik, apostrof, kropka, przecinek; cyfra dozwolona tylko jako pierwszy znak. Ta sama reguła przy tworzeniu i aktualizacji (od wersji API 2.12.0; wcześniej tworzenie przepuszczało wartości, których aktualizacja już nie przyjmowała).","type":"string","example":"Kowalski","nullable":true},"name":{"description":"Imię i nazwisko razem (wygodne do wyświetlania)","type":"string","example":"Jan Kowalski"},"position":{"description":"Stanowisko","type":"string","example":"Dyrektor handlowy","nullable":true},"email":{"description":"Główny adres e-mail kontaktu (kontakt może mieć więcej adresów w CRM)","type":"string","example":"jan.kowalski@acme.pl","nullable":true},"phone":{"type":"string","example":"+48601234567","nullable":true},"phoneAlternative":{"type":"string","nullable":true},"note":{"type":"string","nullable":true},"contactStatusId":{"description":"Status kontaktu: 1 = aktywny, 0 = nieaktywny (CRM dezaktywuje zamiast kasować). Lista NIE ukrywa nieaktywnych - filtruj contactStatusId=1, jeśli chcesz tylko aktywne.","type":"integer","example":1},"ownerUserId":{"description":"Opiekun kontaktu (id użytkownika systemu). Opiekunem może być tylko aktywny użytkownik - konto nieaktywne albo techniczne CRM odrzuca.","type":"integer","nullable":true},"externalId":{"description":"Historyczny klucz zewnętrzny starszych integracji. Dla nowych integracji zalecane dedykowane pole niestandardowe per system.","type":"string","nullable":true},"acl":{"description":"ZNACZNIK PRYWATNOŚCI rekordu (inny mechanizm niż obiektowy ACL {userIds...} reszty API): null = publiczny, 0 = dostęp wyłącznie przez powiązanych kontrahentów, >0 = prywatny dla użytkownika o tym id. Do respektowania ograniczeń dostępu po stronie konsumenta.","type":"integer","example":null,"nullable":true},"contractorId":{"description":"Główny powiązany kontrahent (id z GET /v2/contractors); null, gdy kontakt nie ma powiązań. Główny = ten, którego panel CRM pokazuje na kartotece kontaktu jako pierwszego (najwyższy priorytet powiązania, przy remisie ostatnio aktywny). PUT /v2/contacts/{id} z `contractorId` ustawia głównego.","type":"integer","example":121,"nullable":true},"contractorIds":{"description":"Wszyscy powiązani kontrahenci (główny pierwszy, dalej wg priorytetu powiązania w CRM)","type":"array","items":{"type":"integer"},"example":[121,125]},"createdAt":{"type":"string","format":"date-time","example":"2026-08-01T09:30:00+02:00"},"updatedAt":{"description":"Ostatnia modyfikacja; dla rekordów nigdy niemodyfikowanych = createdAt. Klucz syncu przyrostowego.","type":"string","format":"date-time","example":"2026-08-12T14:05:11+02:00"},"lastActivityAt":{"description":"Ostatnia aktywność na kontakcie w CRM","type":"string","format":"date-time","nullable":true},"customField":{"type":"object","example":{"contact_str_1":"Zakupy"},"additionalProperties":{"nullable":true}},"url":{"description":"Adres karty kontaktu w interfejsie CRM (dla ludzi - do powiadomień i linków „otwórz w CRM”). Kontakt nie ma w CRM własnej strony: otwiera się jako okno nad kartoteką firmy głównej, więc kontakt bez powiązanej firmy ma tu null. Od wersji API 2.12.0.","type":"string","example":"https://twoja-instancja.tillio.app/crm/contractors/121/#/modal=contact-read/contactId:50","nullable":true}},"type":"object"},"DictionaryEntry":{"description":"Pozycja słownika. `active` występuje tylko w słownikach z kolumną statusu - pozycja nieaktywna nadal może być przypisana do starych rekordów, dlatego jej nie ukrywamy.","properties":{"id":{"description":"Słowniki systemowe (GET, bez parametrów, bez paginacji - słowniki są małe).\nEndpointy generyczne dzielą DictionaryRegistry + DictionaryRepository;\npipeline-stages (join grup) i currencies (config app.settings) mają dedykowane metody.","type":"integer","example":5},"name":{"type":"string","example":"Klient"},"active":{"description":"Tylko w słownikach ze statusem (contractor/statuses, contractor/sources, contractor/priorities, contractor/industries).","type":"boolean","example":true},"isFinal":{"description":"W task/statuses (kończy zadanie) i ticket/statuses (zamyka zgłoszenie).","type":"boolean","example":false},"color":{"description":"Kolor pozycji (hex) - w słownikach, które mają kolor w konfiguracji: contractor/statuses, contractor/priorities, note/types, project/statuses, task/statuses, ticket/statuses.","type":"string","example":"#2196f3","nullable":true},"days":{"description":"Tylko w service/payment-terms: liczba dni terminu płatności.","type":"integer","example":14},"isDefault":{"description":"Tylko w service/payment-terms: termin domyślny instancji.","type":"boolean","example":false},"isUnique":{"description":"Tylko w address/types: typ unikalny może wystąpić u kontrahenta raz (dostępne od wersji API 1.21.0).","type":"boolean","example":false},"acl":{"oneOf":[{"$ref":"#/components/schemas/AclJson"}],"nullable":true,"description":"Tylko w note/types: ACL typu notatki ({userIds, departmentIds, groupIds}; null = bez ograniczeń) - notatka nie ma własnego ACL, ograniczenia niesie jej typ. Zapis: POST/PUT /v2/note/types."}},"type":"object"},"UserRole":{"description":"Odpowiedź POST/PUT /v2/user/roles - zapisany stan roli z pełną listą uprawnień (klucze z GET /v2/user/permissions).","properties":{"data":{"properties":{"id":{"type":"integer","example":5},"name":{"type":"string","example":"Zewnętrzny handlowiec"},"description":{"type":"string","nullable":true},"permissions":{"type":"array","items":{"type":"string","example":"contractor.canEdit"}}},"type":"object"},"info":{"properties":{"created":{"type":"boolean","example":true},"ids":{"properties":{"id":{"type":"integer","example":5}},"type":"object"},"warnings":{"type":"object"}},"type":"object"}},"type":"object"},"AclJson":{"description":"JEDNOLITY kształt ACL w całym API - ten sam na wejściu i wyjściu: {userIds, departmentIds, groupIds}; null = bez ograniczeń, przy zapisie pusty obiekt zdejmuje ograniczenia. Uwaga: pole `acl` (liczba) na kontakcie, szansie i leadzie to INNY mechanizm - znacznik prywatności rekordu, nie obiektowy ACL.","properties":{"userIds":{"type":"array","items":{"type":"integer"}},"departmentIds":{"type":"array","items":{"type":"integer"}},"groupIds":{"type":"array","items":{"type":"integer"}}},"type":"object","nullable":true},"DmsDirectory":{"description":"Katalog DMS kontrahenta (drzewo - parentId wskazuje katalog nadrzędny, null = poziom główny).","properties":{"id":{"type":"integer","example":61},"name":{"type":"string","example":"Umowy"},"parentId":{"type":"integer","example":null,"nullable":true},"sizeBytes":{"description":"Łączny rozmiar plików w katalogu","type":"integer","example":1048576,"nullable":true},"creatorUserId":{"type":"integer","nullable":true},"acl":{"oneOf":[{"$ref":"#/components/schemas/AclJson"}],"nullable":true,"description":"ACL katalogu z CRM: dostęp ograniczony do wskazanych użytkowników/działów/grup; null = bez ograniczeń."}},"type":"object"},"DmsDocument":{"description":"Plik w DMS kontrahenta. `publicId` to identyfikator używany w adresach `/v2/dms/documents/{publicId}` - `id` jest wyłącznie referencją do rekordu w CRM i NIE adresuje endpointów. `downloadUrl` = podpisany, tymczasowy link do pobrania (ważny 1 minutę; wymusza oryginalną nazwę pliku i jego typ MIME - w storage pliki DMS leżą pod nazwą techniczną `.dmsfile`); null = generowanie niedostępne w tym środowisku.","properties":{"publicId":{"description":"Identyfikator pliku używany w adresach API (nieciągły, nie do odgadnięcia)","type":"string","example":"3417800497430272"},"id":{"description":"Id rekordu w CRM (referencja; do adresowania służy publicId)","type":"integer","example":428},"directoryId":{"description":"Katalog pliku; null = poziom główny","type":"integer","nullable":true},"fileName":{"type":"string","example":"umowa-2026.pdf"},"mimeType":{"type":"string","example":"application/pdf","nullable":true},"sizeBytes":{"type":"integer","example":171605,"nullable":true},"dmsStatusId":{"description":"Status dokumentu wg słownika DMS instancji","type":"integer","nullable":true},"dmsTypeId":{"description":"Typ dokumentu wg słownika DMS instancji","type":"integer","nullable":true},"title":{"description":"Tytuł nadany w CRM (niezależny od nazwy pliku)","type":"string","nullable":true},"description":{"type":"string","nullable":true},"creatorUserId":{"type":"integer","nullable":true},"createdAt":{"type":"string","format":"date-time"},"acl":{"oneOf":[{"$ref":"#/components/schemas/AclJson"}],"nullable":true,"description":"ACL pliku z CRM: dostęp ograniczony do wskazanych użytkowników/działów/grup; null = bez ograniczeń."},"downloadUrl":{"description":"Podpisany, tymczasowy link do pobrania (ważny 1 minutę)","type":"string","nullable":true}},"type":"object"},"GeneratedDocument":{"description":"Dokument z generatora. `publishUrl` = publiczny link online (docs.tillio.app) -\nistnieje dla typów z włączoną publikacją, ważny do `publishValidTo`\n(null = bezterminowo). `downloadUrl` = podpisany link do pliku PDF ze storage\n(ważny 1 minutę). `documentStatusId`: 4 = wygenerowano, 5 = błąd (szczegóły w `error`).","properties":{"id":{"type":"integer","example":294},"contractorId":{"type":"integer","example":121},"salesPipelineId":{"description":"Szansa sprzedaży, do której przypięto dokument","type":"integer","nullable":true},"documentTypeId":{"type":"integer","example":411},"typeName":{"type":"string","example":"Oferta handlowa"},"templateId":{"description":"Szablon HTML (typy z szablonami)","type":"integer","nullable":true},"documentStatusId":{"description":"1-3 = w toku, 4 = wygenerowano, 6 = wysłano, 5 = błąd","type":"integer","example":4},"statusName":{"type":"string","example":"Wygenerowano"},"number":{"description":"Numer dokumentu wg schematu numeracji instancji","type":"string","nullable":true},"creatorUserId":{"type":"integer","nullable":true},"value":{"description":"Wartość dokumentu (jeśli dotyczy)","type":"string","nullable":true},"currency":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time","nullable":true},"publishUrl":{"description":"Publiczny link online dokumentu (docs.tillio.app) - do wysłania klientowi; null = typ bez publikacji albo dokument jeszcze niewygenerowany","type":"string","example":"https://docs.tillio.app/25ea4...","nullable":true},"publishValidTo":{"description":"Ważność publikacji online; null = bezterminowo","type":"string","format":"date-time","nullable":true},"downloadUrl":{"description":"Podpisany link do pliku PDF (ważny 1 minutę)","type":"string","nullable":true},"error":{"description":"Szczegóły błędu generowania (documentStatusId = 5)","type":"object","nullable":true}},"type":"object"},"DocumentType":{"description":"Typ dokumentu generatora (szablon Google Docs) - szczegóły. Cykl życia:\nPOST /v2/document/types (draft) → POST .../source (plik źródłowy, wykrycie\nzmiennych) → PUT .../form (formularz mapujący zmienne na pola; `draft` schodzi\nautomatycznie, gdy każda zmienna ma pole) → POST .../activate (typ zaczyna\ngenerować dokumenty). `variables` = zmienne `{{...}}` wykryte w pliku źródłowym.","properties":{"id":{"type":"integer","example":512},"name":{"type":"string","example":"Umowa serwisowa"},"description":{"type":"string","nullable":true},"categoryId":{"type":"integer","nullable":true},"categoryName":{"type":"string","nullable":true},"numerationId":{"description":"Schemat numeracji dokumentów","type":"integer","nullable":true},"mailTemplateId":{"description":"Szablon maila do wysyłki dokumentu","type":"integer","nullable":true},"fileType":{"description":"Format generowanego pliku","type":"string","example":"pdf"},"store":{"description":"Czy wygenerowane dokumenty są zapisywane w CRM. Tylko typy ze store=true generują dokumenty przez API (POST /v2/contractors/{id}/documents).","type":"boolean","example":true},"publishDays":{"description":"Dni publikacji online (docs.tillio.app); 0 = bez publikacji. Przy store=false zawsze 0 (zachowanie CRM).","type":"integer","example":14},"active":{"description":"Tylko aktywne typy generują dokumenty","type":"boolean","example":false},"draft":{"description":"true dopóki formularz nie pokrywa wszystkich zmiennych z pliku źródłowego","type":"boolean","example":true},"requiresTemplate":{"description":"Typy z szablonami HTML (nie dotyczy typów zakładanych przez API)","type":"boolean","example":false},"shareEmails":{"description":"Adresy, którym plik źródłowy jest udostępniany w Google Docs","type":"array","items":{"type":"string"},"example":["anna.nowak@example.com"]},"sourceUploaded":{"description":"Czy typ ma wgrany plik źródłowy","type":"boolean","example":false},"sourceMime":{"description":"Typ pliku w Google Docs (document/presentation)","type":"string","nullable":true},"variables":{"description":"Zmienne {{...}} wykryte w pliku źródłowym","type":"array","items":{"type":"string"},"example":["numer_umowy","nazwa_klienta"]},"formFields":{"description":"Definicja formularza - grupy pól (patrz PUT /v2/document/types/{id}/form)","type":"array","items":{"type":"object"}},"templateVersion":{"description":"Wersja szablonu - rośnie przy zmianie zmiennych albo formularza","type":"integer","nullable":true}},"type":"object"},"DocumentCategory":{"description":"Kategoria typów dokumentów generatora - płaskie drzewo (`parentId` wskazuje\nkategorię nadrzędną, null = najwyższy poziom). Kategorie z `system: true`\nto katalogi wbudowane CRM - widoczne na liście, ale tylko do odczytu\n(nie można ich edytować ani wskazać jako rodzica).","properties":{"id":{"type":"integer","example":512},"name":{"type":"string","example":"Umowy handlowe"},"parentId":{"description":"Kategoria nadrzędna; null = najwyższy poziom","type":"integer","example":null,"nullable":true},"priority":{"description":"Kolejność na listach (wyższa = wyżej)","type":"integer","example":0},"system":{"description":"Kategoria wbudowana CRM - tylko do odczytu","type":"boolean","example":false}},"type":"object"},"TillioCallsIntegration":{"description":"Stan integracji Tillio Calls w instancji. Klucz nigdy nie jest zwracany.","properties":{"registered":{"description":"Czy integracja jest zarejestrowana i aktywna","type":"boolean","example":true},"apiUrl":{"description":"Adres API Tillio Calls zapisany w CRM","type":"string","example":"https://k7f3a2c1-calls.tillio.app","nullable":true},"hasApiKey":{"description":"Czy w konfiguracji jest zapisany klucz do API Calls","type":"boolean","example":true},"providerId":{"description":"Id dostawcy telefonii w CRM","type":"integer","example":404,"nullable":true},"configId":{"description":"Id konfiguracji dostawcy w CRM","type":"integer","example":404,"nullable":true}},"type":"object"},"PipelineItem":{"description":"Szansa sprzedaży. `customField` to obiekt klucz→wartość surowa (select = id opcji - mapowanie w GET /v2/pipeline/custom-fields). `updatedAt` = znacznik ostatniej aktywności - CRM podbija go przy każdej modyfikacji szansy (a także przy aktywności handlowej), więc jest bezpiecznym kluczem syncu przyrostowego.","properties":{"id":{"type":"integer","example":6},"name":{"description":"Nazwa szansy","type":"string","example":"Wdrożenie CRM - Acme"},"contractorId":{"description":"Kontrahent szansy (id z GET /v2/contractors)","type":"integer","example":121},"pipelineStageId":{"description":"Etap pipeline (id ze słownika etapów CRM)","type":"integer","example":5},"pipelineStatusId":{"description":"Status szansy: 1 = aktywna, 2 = stracona, 3 = wygrana","type":"integer","example":1},"ownerUserId":{"description":"Właściciel/opiekun szansy (id użytkownika systemu)","type":"integer","nullable":true},"creatorUserId":{"description":"Twórca szansy (id użytkownika systemu)","type":"integer","nullable":true},"closerUserId":{"description":"Użytkownik zamykający szansę","type":"integer","nullable":true},"amount":{"description":"Wartość szansy - string dla precyzji dziesiętnej","type":"string","example":"12500.00","nullable":true},"currency":{"description":"Waluta kwoty (kod ISO 4217)","type":"string","example":"PLN","nullable":true},"probability":{"description":"Prawdopodobieństwo wygranej w procentach (0-100)","type":"integer","example":60,"nullable":true},"closeDate":{"description":"Planowana data zamknięcia (YYYY-MM-DD)","type":"string","format":"date","example":"2026-09-30","nullable":true},"realCloseDate":{"description":"Rzeczywista data zamknięcia (YYYY-MM-DD)","type":"string","format":"date","nullable":true},"lostReason":{"description":"Powód przegranej (dla pipelineStatusId = 2)","type":"string","nullable":true},"note":{"type":"string","nullable":true},"externalId":{"description":"Historyczny klucz zewnętrzny starszych integracji. Dla nowych integracji zalecane dedykowane pole niestandardowe per system.","type":"string","nullable":true},"leadId":{"description":"Lead, z którego powstała szansa","type":"integer","nullable":true},"acl":{"description":"ZNACZNIK PRYWATNOŚCI rekordu (inny mechanizm niż obiektowy ACL {userIds...} reszty API): 0/null = publiczna, >0 = prywatna dla użytkownika o tym id. Do respektowania ograniczeń dostępu po stronie konsumenta.","type":"integer","example":0,"nullable":true},"createdAt":{"type":"string","format":"date-time","example":"2026-08-01T09:30:00+02:00"},"updatedAt":{"description":"Ostatnia zmiana/aktywność szansy. Klucz syncu przyrostowego.","type":"string","format":"date-time","example":"2026-08-12T14:05:11+02:00"},"customField":{"type":"object","example":{"pipeline_str_1":"42"},"additionalProperties":{"nullable":true}}},"type":"object"},"ProductGroup":{"description":"Grupa produktów (słownik drzewiasty). `parentId` = null oznacza grupę główną,\nwartość wskazuje grupę nadrzędną. `priority` steruje kolejnością wyświetlania\nw CRM (rosnąco). Grupy nie mają dat utworzenia/modyfikacji ani pól niestandardowych.","properties":{"id":{"type":"integer","example":66},"parentId":{"description":"Grupa nadrzędna; null = grupa główna","type":"integer","example":null,"nullable":true},"name":{"type":"string","example":"Meble"},"color":{"description":"Kolor etykiety w CRM (hex)","type":"string","example":"#fab061","nullable":true},"priority":{"description":"Kolejność wyświetlania (rosnąco)","type":"integer","example":0,"nullable":true},"status":{"description":"1 = aktywna, 0 = nieaktywna","type":"integer","example":1,"nullable":true}},"type":"object"},"Product":{"description":"Produkt. Cena bazowa (`price` + `currency`) i stawka VAT (`taxRate`) pochodzą wprost\nz kartoteki produktu w CRM - wartości dziesiętne jako string dla precyzji.\n`customField` to obiekt klucz→wartość surowa (definicje w GET /v2/<encja>/custom-fields);\nobecnie CRM nie definiuje pól niestandardowych dla produktów, więc obiekt jest pusty.\n`updatedAt` (data modyfikacji, utrzymywana przez bazę CRM) niesie sync\nprzyrostowy (`updatedAfter`); `createdAt` = data dodania kartoteki.","properties":{"id":{"type":"integer","example":235},"name":{"type":"string","example":"Krzesło biurowe Ergo"},"description":{"type":"string","example":"Krzesło z regulacją wysokości","nullable":true},"sku":{"description":"Kod magazynowy - klucz matchowania z ERP","type":"string","example":"KRZ-ERGO-01","nullable":true},"ean":{"description":"Kod kreskowy EAN","type":"string","example":"5901234567890","nullable":true},"externalId":{"description":"Id produktu w systemie zewnętrznym - klucz matchowania z ERP, bez wymuszania unikalności. Konwencja: własny prefix integracji, np. op1_10023.","type":"string","example":"op1_10023","nullable":true},"groupId":{"description":"Grupa produktowa - drzewo w GET /v2/product/groups","type":"integer","example":66,"nullable":true},"groupName":{"description":"Nazwa grupy (denormalizowana dla wygody integracji)","type":"string","example":"Meble","nullable":true},"measureId":{"type":"integer","example":1,"nullable":true},"measure":{"description":"Jednostka miary, np. szt., kg, m2","type":"string","example":"szt.","nullable":true},"price":{"description":"Cena bazowa netto - string dla precyzji dziesiętnej","type":"string","example":"150.00","nullable":true},"currency":{"description":"Waluta ceny bazowej (ISO 4217)","type":"string","example":"PLN","nullable":true},"taxRate":{"description":"Domyślna stawka VAT w procentach","type":"string","example":"23.00","nullable":true},"status":{"description":"1 = aktywny, 0 = nieaktywny","type":"integer","example":1,"nullable":true},"createdAt":{"description":"Data dodania kartoteki","type":"string","format":"date-time","example":"2026-07-01T10:15:00+02:00"},"updatedAt":{"description":"Data ostatniej modyfikacji kartoteki (utrzymywana przez DB). Klucz syncu przyrostowego (updatedAfter).","type":"string","format":"date-time","example":"2026-08-10T09:30:00+02:00"},"customField":{"type":"object","additionalProperties":{"nullable":true}}},"type":"object"}},"responses":{"Unauthorized":{"description":"Brak lub błędne nagłówki auth (X-Tenant-Domain / X-Tenant-Id / X-Api-Key)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"securitySchemes":{"tenantAuth":{"type":"apiKey","description":"Klucz API w formacie name:key (wystawiany w aplikacji Tillio). Wymagany RAZEM z X-Tenant-Domain i X-Tenant-Id.","name":"X-Api-Key","in":"header"},"tenantDomain":{"type":"apiKey","description":"Domena instancji Tillio (np. crm.przyklad.pl).","name":"X-Tenant-Domain","in":"header"},"tenantId":{"type":"apiKey","description":"Identyfikator tenanta = nazwa bucketa storage instancji.","name":"X-Tenant-Id","in":"header"},"instanceName":{"type":"apiKey","description":"OPCJONALNY. Dotyczy wyłącznie serwerów, na których stoi kilka instalacji CRM obok siebie - wtedy operator poda Ci nazwę instancji. Jeśli jej nie dostałeś, zostaw puste.","name":"X-Instance-Name","in":"header"}}},"tags":[{"name":"Contractors","description":"Kontrahenci - odczyt z filtrowaniem po dowolnym polu i polach niestandardowych"},{"name":"CustomFields","description":"Definicje pól niestandardowych (klucze, typy, opcje)"},{"name":"System","description":"Health, tożsamość klucza, dokumentacja"},{"name":"Wiki","description":"Baza wiedzy: bazy (artykuły / procedury), kategorie i wpisy"},{"name":"Orders","description":"Zamówienia - odczyt nagłówków i pozycji z filtrowaniem pod integracje ERP"},{"name":"Calendars","description":"Kalendarze - lista kalendarzy instancji wraz z dostępami użytkowników"},{"name":"Contacts","description":"Kontakty (osoby) - odczyt z filtrowaniem, w tym po kontrahencie i mailu"},{"name":"Dictionaries","description":"Słowniki systemowe - mapowania id → nazwa dla pól *Id z pozostałych endpointów"},{"name":"DMS","description":"Dokumenty kontrahenta (DMS) - katalogi i pliki: listing, tworzenie katalogów, upload i pobieranie plików"},{"name":"Documents","description":"Generator dokumentów z szablonów - typy, formularze, generowanie (PDF), publikacja online"},{"name":"Integrations","description":"Konfiguracja integracji w CRM (dziś: Tillio Calls)"},{"name":"Lookup","description":"Szybkie wyszukiwanie po identyfikatorze (numer telefonu) - jedno żądanie, dopasowanie dokładne, kilka encji naraz"},{"name":"Mail","description":"Wysyłka maili z kont pocztowych instancji - z szablonami i stopką konta"},{"name":"Notes","description":"Notatki kontrahentów, kontaktów i leadów - lista z filtrowaniem, zapis, kontakty, załączniki"},{"name":"Phone calls","description":"Połączenia telefoniczne - rekordy w tabeli połączeń CRM (te same, które zakładają integracje VoIP): lista z filtrowaniem, odczyt, zapis z systemu telefonii (Tillio Calls)"},{"name":"PipelineItems","description":"Szanse sprzedaży (pipeline) - lista z filtrowaniem, odczyt i zapis"},{"name":"Products","description":"Produkty i grupy produktów - odczyt z filtrowaniem po dowolnym polu"},{"name":"Projects","description":"Projekty - odczyt z licznikami zadań i polami niestandardowymi"},{"name":"ServiceCatalog","description":"Katalog (cennik) usług - słownik pozycji, z których powstają usługi kontrahentów"},{"name":"Services","description":"Usługi kontrahentów - lista z filtrowaniem, odczyt i zapis"},{"name":"Stocks","description":"Stany magazynowe - kluczowa końcówka syncu magazynów z ERP"},{"name":"TaskTemplates","description":"Szablony zadań - gotowce, z których w CRM tworzy się zadania jednym kliknięciem"},{"name":"Tasks","description":"Zadania - odczyt z filtrowaniem po polach, wykonawcy i polach niestandardowych"},{"name":"Text messages","description":"Wiadomości SMS - rekordy w tabeli wiadomości CRM (te same, które zakładają integracje VoIP): lista z filtrowaniem, odczyt, zapis z systemu telefonii (Tillio Calls)"},{"name":"Users","description":"Użytkownicy systemu - dane niewrażliwe do mapowania osób w integracjach"},{"name":"Warehouses","description":"Magazyny - słownik magazynów dla stanów magazynowych"},{"name":"Leads","description":"Leads"},{"name":"Tickets","description":"Tickets"}]}