Zgłoszenia (tickets) — API v1 i MCP | Dokumentacja Nodea
Moduł Zgłoszenia (tickets) obejmuje portal wsparcia klienta: listowanie własnych zgłoszeń, podgląd konwersacji, otwieranie nowego zgłoszenia, dodawanie odpowiedzi i zamykanie. Dokument opisuje warstwę REST API v1 (/api/v1/tickets) oraz narzędzia MCP z tego modułu.
Uwierzytelnianie i zakresy (scopes)
API v1 uwierzytelnia się osobistym tokenem Passport przez nagłówek Authorization: Bearer <token>.
account:read— listowanie i podgląd (index,show).account:write— tworzenie, odpowiedź i zamknięcie (store,reply,close).
Cały moduł jest dodatkowo bramkowany flagą Pennant features:hosting-suite — odpowiada web'owym grupom /app/account/*. Limit zapytań: throttle:api-v1 = 240/min na użytkownika tokenu.
Własność i widoczność (ownership)
Zgłoszenia nie mają modelu współdzielenia ani klasy polityki — dostęp na poziomie wiersza jest wymuszany inline (ensureOwned): tylko właściciel widzi i działa na swoim zgłoszeniu. Cudzy lub nieznany identyfikator zwraca 404 (nie 403), by API nie potwierdzało istnienia zasobu innego najemcy.
Notatki wewnętrzne — niewidoczne
Konwersacja jest zawsze ładowana przez scope public() na TicketReply, więc wewnętrzne notatki administratorów nigdy nie trafiają do odpowiedzi — to wymóg bezpieczeństwa, nie optymalizacja. Klient nigdy nie może utworzyć notatki wewnętrznej: pole is_internal_note jest twardo ustawiane na false przy każdej odpowiedzi tworzonej przez API. Załączniki nie są obsługiwane przez API (brak uploadu i URL-i pobierania) — eksponowane są wyłącznie metadane załączników odpowiedzi (id, original_name, size, mime).
API v1 — endpointy
| Metoda | Ścieżka | Scope | Opis |
|---|---|---|---|
| GET | /api/v1/tickets | account:read | Lista własnych zgłoszeń, najnowsza aktywność pierwsza (last_reply_at DESC, id DESC). Filtr status. Paginacja simple. |
| GET | /api/v1/tickets/{ticket} | account:read | Szczegóły zgłoszenia z pełną konwersacją (publiczne odpowiedzi w porządku chronologicznym, bez notatek wewnętrznych). |
| POST | /api/v1/tickets | account:write | Otwarcie nowego zgłoszenia (status open); treść staje się pierwszą odpowiedzią. Zwraca 201. |
| POST | /api/v1/tickets/{ticket}/reply | account:write | Dodanie odpowiedzi klienta. Zwraca 201 (TicketReplyResource). |
| POST | /api/v1/tickets/{ticket}/close | account:write | Zamknięcie zgłoszenia przez klienta. Zwraca zaktualizowane zgłoszenie. |
GET /tickets — parametry zapytania
status(enum, opcjonalny) — dokładne dopasowanie:open,pending,answered,closed,spam.per_page(int 1–100, domyślnie 20),page(int ≥1).
Odpowiedź: kolekcja TicketResource — pola: id, number (T-YYYY-NNNNNN), subject, status, priority, department (id/name/slug), last_reply_at, last_reply_by_admin, is_closed, closed_at, created_at, updated_at. Klucz replies[] pojawia się tylko w szczegółach (gdy kontroler doładował relację). Każda odpowiedź: id, body, source, is_by_owner, author (id/name), attachments[] (metadane), created_at.
POST /tickets — parametry ciała
subject(string, wymagany, min 5, max 255).body(string, wymagany, min 10) — persystowany jako pierwsza publiczna odpowiedź.ticket_department_id(wymagany, musi istnieć wticket_departmentszis_active = true).priority(enum, opcjonalny:low|normal|high|urgent; domyślnienormal).
Tworzenie jest atomowe (transakcja): tymczasowy numer spełnia unikalny indeks, następnie generateNumber() wylicza realny T-YYYY-NNNNNN, a treść zapisywana jest jako pierwsza odpowiedź (source = api). Skrzynka działu jest powiadamiana jako anonimowy notifiable. Załączniki nie są przyjmowane przez API.
POST /reply oraz /close — reguły biznesowe
reply: ciałobody(string, wymagany, min 1). Odpowiedź klienta przełącza statusanswered→open(pozostałe stany zachowane).- Zgłoszenie
closedodrzuca odpowiedź:422, kodticket_closed. - Zgłoszenie
spamodrzuca odpowiedź:422, kodticket_spam. close: zamknięcie już zamkniętego to422, kodalready_closed; zgłoszeniaspamklient nie może zamknąć:422, kodticket_spam. Sukces stemplujeclosed_aticlosed_by_id.
Kody błędów
401— brak/niepoprawny token.403— brak wymaganego scope lub nieaktywna flagahosting-suite.404— nieznane lub cudze zgłoszenie (ownership inline).422— walidacja lub reguła biznesowa (kody:ticket_closed,ticket_spam,already_closed).
Przykłady — API
Lista zgłoszeń z filtrem statusu
curl -s "https://app.nodea.io/api/v1/tickets?status=open&per_page=20" \
-H "Authorization: Bearer $NODEA_TOKEN" \
-H "Accept: application/json"
Otwarcie nowego zgłoszenia
curl -s -X POST https://app.nodea.io/api/v1/tickets \
-H "Authorization: Bearer $NODEA_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"ticket_department_id": 1,
"subject": "Problem z DNS",
"body": "Rekord A nie propaguje się od godziny.",
"priority": "high"
}'
Odpowiedź na zgłoszenie
curl -s -X POST https://app.nodea.io/api/v1/tickets/42/reply \
-H "Authorization: Bearer $NODEA_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "body": "Dziękuję, już działa." }'
Zamknięcie zgłoszenia
curl -s -X POST https://app.nodea.io/api/v1/tickets/42/close \
-H "Authorization: Bearer $NODEA_TOKEN" \
-H "Accept: application/json"
MCP — narzędzia
Narzędzia MCP dzielą model uwierzytelniania i zakresów z API v1 (Bearer + scope sprawdzany w ensureScope) oraz flagę features:hosting-suite — narzędzie na nieaktywnej fladze znika z tools/list i każde wywołanie kończy się błędem „Tool [...] not found.”. Identyfikatory zgłoszeń to liczby całkowite (id). Brakujący i cudzy identyfikator zwracają ten sam błąd (AuthorizationException: „Ticket not found or not accessible with your permissions.”) — brak enumeracji między najemcami.
| Narzędzie | Scope | Tryb | Opis |
|---|---|---|---|
| tickets-list | account:read | read-only | Lista własnych zgłoszeń; filtr status, page, per_page (max 50, domyślnie 15). reply_count liczy tylko publiczne odpowiedzi. |
| ticket-get | account:read | read-only | Jedno zgłoszenie z pełną konwersacją (publiczne odpowiedzi chronologicznie, bez notatek wewnętrznych). Param: id. |
| ticket-create | account:write | zapis | Otwarcie zgłoszenia; ticket_department_id, subject (max 255), body, opcjonalny priority. Status open, source = mcp. |
| ticket-reply | account:write | zapis | Odpowiedź klienta; id, body. answered → open. Zamknięte/spam odrzucają. |
| ticket-close | account:write | zapis | Zamknięcie zgłoszenia; id. Już zamknięte lub spam zwracają błąd narzędzia. |
Uwagi: tickets-list, ticket-get mają adnotacje IsReadOnly/IsIdempotent. ticket-create, ticket-reply, ticket-close są zapisujące (bez adnotacji read-only). Reguły biznesowe (zamknięte/spam, reopen z answered, twarde is_internal_note = false, powiadomienie skrzynki działu) są identyczne jak w API; przy błędzie zwracany jest Response::error zamiast HTTP 422. ticket-create i ticket-close ładują reply_count tylko z publicznych odpowiedzi.
Przykład — MCP (JSON-RPC)
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "ticket-create",
"arguments": {
"ticket_department_id": 1,
"subject": "Problem z DNS",
"body": "Rekord A nie propaguje się od godziny.",
"priority": "high"
}
}
}
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "ticket-reply",
"arguments": { "id": 42, "body": "Dziękuję, już działa." }
}
}