nessie

API

Το HTTP API πίσω από κάθε πελάτη του Nessie και πώς γίνεται ο έλεγχος ταυτότητας σε αυτό.

Τι είναι το API

Το Nessie έχει ένα HTTP API. Η διαχειριστική εφαρμογή web, τα clients για macOS και iOS και ο απομακρυσμένος daemon του εκτελεστή είναι όλοι συνηθισμένοι πελάτες του, και τίποτα από όσα μπορούν να κάνουν δεν είναι προσβάσιμο με άλλον τρόπο. Είναι ένα JSON API πάνω από διακομιστή Fastify, δεν είναι GraphQL, και δεν έχει χωριστό υποσύνολο «δημόσιο»: η επιφάνεια που καλούν τα δικά σας σενάρια είναι η ίδια που καλεί το προϊόν.

Η διαδρομή δεν έχει τμήμα έκδοσης. Κάθε διαδρομή βρίσκεται κάτω από το /api, εκτός από το εισερχόμενο endpoint MCP στο POST /mcp και μερικά έγγραφα ανακάλυψης κάτω από το /.well-known/. Η συμβατότητα διατηρείται με το να προστίθενται μόνο πεδία, ώστε ένας πελάτης που αγνοεί όσα δεν αναγνωρίζει να συνεχίζει να λειτουργεί.

Βασικό URL

Το Nessie είναι αυτοφιλοξενούμενο, οπότε το βασικό URL είναι η προέλευση (origin) όπου η εγκατάστασή σας σερβίρει το API. Ο διαχειριστής το ορίζει ως NESSIE_API_PUBLIC_URL, και αυτή είναι η ίδια τιμή που χρησιμοποιεί το API όταν χρειάζεται να δημιουργήσει δικό του απόλυτο URL. Στην εγκατάσταση που τρέχουμε, είναι το https://api.nessie.works· μια εγκατάσταση ανάπτυξης που ξεκινά με pnpm dev απαντά στο http://localhost:5454.

Η διαχειριστική εφαρμογή web βρίσκεται σε άλλη προέλευση από το API, γι’ αυτό τα προγράμματα περιήγησης υπόκεινται σε λίστα επιτρεπόμενων CORS (NESSIE_CORS_ORIGINS). Τα αιτήματα από σενάριο ή διακομιστή δεν έχουν προέλευση προς έλεγχο και δεν επηρεάζονται.

Έλεγχος ταυτότητας

Κάθε αίτημα εκτός από ένα μικρό σύνολο δημόσια προσβάσιμων διαδρομών μεταφέρει διακριτικό πρόσβασης στην τυπική κεφαλίδα:

Authorization: Bearer <token>

Το διακριτικό είναι ένα υπογεγραμμένο JWT που εκδίδει το API. Είναι βραχύβιο — τριάντα λεπτά από προεπιλογή (NESSIE_AUTH_TOKEN_TTL) — και είναι το μόνο διαπιστευτήριο που παρουσιάζει ποτέ ένας συνηθισμένος καλών. Το Nessie δεν έχει μακρόβια προσωπικά κλειδιά API. Αν θέλετε ένα σενάριο να καλεί το API, συνδέεται με τον ίδιο τρόπο όπως το πρόγραμμα περιήγησης ενός ανθρώπου και ανανεώνει το διακριτικό του καθώς προχωρά.

Λήψη διακριτικού

Το POST /api/auth/session δημιουργεί συνεδρία. Απαντά με το διακριτικό πρόσβασης και την ταυτότητα του καλούντος, και ορίζει το διακριτικό ανανέωσης ως ξεχωριστό cookie HTTP-only.

Το πώς αποδεικνύετε ποιοι είστε εξαρτάται από τον τρόπο εγκατάστασης της εγκατάστασης. Μια εγκατάσταση σε λειτουργία local δέχεται email και κωδικό πρόσβασης. Σε εγκατάσταση hosted ή selfHosted, η σύνδεση με κωδικό απορρίπτεται κατευθείαν με 403 PASSWORD_AUTH_DISABLED, και η μόνη είσοδος είναι ο ρυθμισμένος πάροχος ταυτότητας — ώστε ο λογαριασμός να μη δημιουργήσει ποτέ δίπλα του δεύτερη, ασθενέστερη διαδρομή ταυτότητας.

Σύνδεση μέσω παρόχου ταυτότητας

Η ενιαία σύνδεση (SSO) είναι ανταλλαγή κωδικού εξουσιοδότησης (authorization code) OAuth με PKCE, σε τρία βήματα. Ο πάροχος ταυτότητας του ίδιου του Nessie είναι το UnlikeOtherAI, που κατέχει επίσης τη δομή οργανισμών και ομάδων που αντιγράφει μια εγκατάσταση· η ανταλλαγή παρακάτω έχει το ίδιο σχήμα για κάθε ρυθμισμένο πάροχο.

  1. Το GET /api/auth/providers επιστρέφει όσους παρόχους δέχεται αυτή η εγκατάσταση, καθέναν με providerId, ετικέτα και ένδειξη αν η σελίδα σύνδεσης πρέπει να τον χρησιμοποιεί αυτόματα.
  2. Το GET /api/auth/providers/:providerId/authorize, με τα codeChallenge, redirectUri και state ως παραμέτρους query, απαντά με { data: { authorizeUrl } }. Στείλτε εκεί το άτομο.
  3. Ο πάροχος επιστρέφει το άτομο στο redirectUri σας με έναν κωδικό. Στείλτε τον πίσω ως POST /api/auth/session με { providerId, code, codeVerifier, redirectUri }— και τα τέσσερα πεδία απαιτούνται μαζί, και ένα ελλιπές σύνολο δίνει 400 EXTERNAL_AUTH_INCOMPLETE.

Ανανέωση συνεδρίας

Το διακριτικό ανανέωσης δεν εμφανίζεται ποτέ στο σώμα μιας απάντησης. Ορίζεται ως cookie HTTP-only με όνομα nessie_refresh, περιορισμένο στη διαδρομή /api/auth, με διάρκεια ζωής τριάντα ημερών από προεπιλογή (NESSIE_AUTH_REFRESH_TOKEN_TTL). Η κλήση POST /api/auth/refresh με αυτό το cookie το καταναλώνει, εκδίδει νέο διακριτικό πρόσβασης και ορίζει νέο cookie στη θέση του — τα διακριτικά περιστρέφονται, οπότε ένα κλεμμένο ισχύει μόνο μέχρι ο πραγματικός πελάτης να ανανεωθεί ξανά.

Ένα πρόγραμμα περιήγησης συνδεδεμένο σε πολλούς λογαριασμούς κρατά ένα τέτοιο cookie για κάθε σύνδεση, με όνομα nessie_refresh_<slot>, και δηλώνει ποιο αφορά ένα αίτημα με την κεφαλίδα x-nessie-account-slot. Ένας πελάτης που δεν στέλνει αυτή την κεφαλίδα αφορά το nessie_refresh, και αυτό είναι ό,τι χρειάζεται ένας πελάτης γραμμής εντολών.

Ένας πελάτης γραμμής εντολών χρειάζεται λοιπόν ένα cookie jar. Με το curl, αυτό είναι το --cookie-jar στην κλήση σύνδεσης και το --cookie στην ανανέωση, όπως στο παράδειγμα παρακάτω.

Τι ελέγχεται σε κάθε αίτημα

Ο έλεγχος ταυτότητας δεν είναι μόνο έλεγχος υπογραφής. Πριν εκτελεστεί ένας χειριστής, το API επαληθεύει την υπογραφή και τη λήξη του διακριτικού, επιβεβαιώνει ότι η συνεδρία δεν έχει ανακληθεί (τόσο με την έκδοση διακριτικού σε επίπεδο χρήστη όσο και με το αναγνωριστικό της μεμονωμένης συνεδρίας), και φορτώνει τη συμμετοχή του καλούντος στον οργανισμό που ονομάζει το διακριτικό. Μια απενεργοποιημένη συμμετοχή δίνει 403. Όταν ο οργανισμός υποστηρίζεται από εξωτερικό πάροχο ταυτότητας, η τρέχουσα κατάσταση του καλούντος ελέγχεται ξανά με τον πάροχο σε κάθε αίτημα, αντί να διαβάζεται από το διακριτικό, ώστε η πρόσβαση που αφαιρείται εκεί να σταματά αμέσως και όχι στην επόμενη λήξη.

Διαπιστευτήρια που δεν είναι διακριτικά συνεδρίας

Υπάρχουν άλλα δύο διαπιστευτήρια bearer, και κανένα δεν είναι κλειδί γενικής χρήσης. Ένα διαπιστευτήριο φωνητικής συσκευής γίνεται δεκτό μόνο στις διαδρομές φωνητικών κλήσεων, και ένα διαπιστευτήριο πρόσβασης πράκτορα μόνο στο endpoint MCP. Αν παρουσιαστεί οπουδήποτε αλλού, απορρίπτεται με 403 που κατονομάζει το διαπιστευτήριο, και δεν επαναδοκιμάζεται ως πιο αδύναμη συνεδρία — δείτε MCP και συνδεδεμένα εργαλεία για το πώς εκδίδεται το διαπιστευτήριο πράκτορα.

Η δομή της απάντησης

Κάθε επιτυχημένη απάντηση είναι αντικείμενο με κλειδί data. Δεν υπάρχει γυμνός πίνακας ούτε ανώτατο φορτίο στη ρίζα, ώστε ένας πελάτης να μη χρειάζεται ποτέ να διακρίνει με βάση το σχήμα:

{
  "data": { "id": "3f1c…", "label": "engineering" }
}

Οι λίστες προσθέτουν δίπλα του ένα αντικείμενο meta. Τίποτα άλλο δεν προστίθεται ποτέ στο ανώτατο επίπεδο.

Οι αποτυχίες χρησιμοποιούν επίσης ένα σχήμα, ό,τι κι αν τις προκάλεσε:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "A message needs content or at least one attachment",
    "field": "body",
    "details": { "formErrors": [], "fieldErrors": {} }
  }
}

Το code είναι το μέρος που διαβάζει η μηχανή, και με βάση αυτό πρέπει να διακλαδίζετε· το message είναι για άνθρωπο. Τα field και details υπάρχουν όταν η αποτυχία είναι αρκετά συγκεκριμένη ώστε να εντοπιστεί. Ένα μη χειρισμένο σφάλμα διακομιστή είναι πάντα 500 INTERNAL_ERROR με σταθερό μήνυμα — το κείμενο του υποκείμενου σφάλματος καταγράφεται, δεν επιστρέφεται, επειδή μπορεί να έχει πιάσει δεδομένα αιτήματος στην πορεία προς τα έξω.

Κωδικοί σφάλματος που θα συναντήσετε πρώτα

ΚατάστασηΚωδικόςΣημασία
400VALIDATION_ERRORΤο σώμα ή το query δεν ταίριαξαν με το σχήμα.
401AUTH_REQUIREDΔεν υπάρχει διακριτικό bearer, ή η κεφαλίδα είναι λανθασμένη.
401TOKEN_EXPIREDΤο διακριτικό πρόσβασης έχει λήξει. Ανανεώστε και δοκιμάστε ξανά.
401TOKEN_REVOKEDΗ συνεδρία αποσυνδέθηκε, ή όλες οι συνεδρίες αναγκάστηκαν να τερματιστούν.
403ACCOUNT_DEACTIVATEDΗ συμμετοχή σε αυτόν τον οργανισμό έχει απενεργοποιηθεί.
404NOT_FOUNDΔεν υπάρχει τέτοια διαδρομή, ή δεν υπάρχει τέτοια εγγραφή ορατή σε αυτόν τον καλούντα.
429RATE_LIMITEDΈνας κάδος είναι γεμάτος. Διαβάστε την κεφαλίδα Retry-After.
500INTERNAL_ERRORΈνα μη χειρισμένο σφάλμα διακομιστή, σκόπιμα αδιαφανές.

Σελιδοποίηση

Οι λίστες σελιδοποιούνται με αδιαφανή δρομέα keyset, όχι με offset — οι εγγραφές εισάγονται ενώ κάποιος διαβάζει, και με offset η δεύτερη σελίδα επαναλαμβάνει σιωπηλά μια εγγραφή που έδειξε ήδη η πρώτη.

Γίνονται δεκτές τρεις παράμετροι query: cursor, limit και direction (forward ή backward). Η προεπιλεγμένη σελίδα έχει 25 εγγραφές και το ανώτατο όριο είναι 100· μια μεγαλύτερη τιμή περιορίζεται, δεν απορρίπτεται. Τα endpoints που ταξινομούν ή φιλτράρουν προσθέτουν επιπλέον τα sort, order και ένα ελεύθερο κείμενο q.

Το meta της σελίδας σάς δείχνει πού βρίσκεστε:

{
  "data": [ … ],
  "meta": {
    "hasMore": true,
    "nextCursor": "2026-09-12T08:31:44.005Z|8b2f…",
    "prevCursor": null,
    "total": 134
  }
}

Τα nextCursor και prevCursor είναι και τα δύο πραγματικά: το Προηγούμενο και το Επόμενο είναι και τα δύο στοιχεία ελέγχου που περιμένει ένας χρήστης. Είναι αδιαφανή — ο διακομιστής κωδικοποιεί μέσα τους το δικό του κλειδί ταξινόμησης, και ένας πελάτης που τα αναλύει ή τα κατασκευάζει θα σπάσει τη στιγμή που μια λίστα αλλάξει τη σειρά της. Το total υπάρχει στις περισσότερες λίστες και παραλείπεται εκεί όπου η καταμέτρηση δεν έχει νόημα, όπως στα ταξινομημένα αποτελέσματα αναζήτησης.

Όρια ρυθμού

Τα όρια ισχύουν ανά σταθερό παράθυρο και επιβάλλονται πριν διαβαστεί καν το σώμα του αιτήματος. Ένα απορριπτόμενο αίτημα είναι 429 με κωδικό RATE_LIMITED και κεφαλίδα Retry-After σε δευτερόλεπτα.

Οι περισσότεροι κάδοι βασίζονται στη διεύθυνση IP του πελάτη. Οι ευαίσθητοι σε διαπιστευτήρια — σύνδεση, ανανέωση, επαλήθευση κωδικού, εγγραφές διαπιστευτηρίων MCP — κρατούν και έναν μετρητή ανά λογαριασμό, και απορρίπτουν όταν ενεργοποιηθεί είτε ο ένας είτε ο άλλος· έτσι, μια διεύθυνση που δοκιμάζει πολλούς λογαριασμούς και ένας λογαριασμός που δέχεται επίθεση από πολλές διευθύνσεις συναντούν και οι δύο τοίχο. Η σύνδεση έχει προεπιλογή 10 προσπάθειες ανά IP και 5 ανά διεύθυνση email ανά δέκα λεπτά· η δημοσίευση μηνύματος 60 ανά λεπτό· οι εγγραφές κάτω από το /api/agents 60 ανά λεπτό. Κάθε όριο ρυθμίζεται ανά εγκατάσταση, οπότε αντιμετωπίστε αυτές τις τιμές ως προεπιλογές του προϊόντος και όχι ως δέσμευση. Οι έλεγχοι υγείας και οι ροές σε πραγματικό χρόνο εξαιρούνται.

Σε πραγματικό χρόνο

Η επανειλημμένη ανάγνωση μιας λίστας είναι λάθος τρόπος για να παρακολουθείτε μια συνομιλία. Υπάρχουν τρία endpoints ροής:

  • GET /api/events/stream — συμβάντα που στέλνει ο διακομιστής για όλα όσα μπορεί να δει το συνδεδεμένο άτομο.
  • GET /api/threads/:threadId/stream — συμβάντα που στέλνει ο διακομιστής για ένα νήμα. Σέβεται το Last-Event-ID, οπότε μια επανασύνδεση συνεχίζει και δεν ξεκινά από την αρχή.
  • GET /api/activity — ένα WebSocket που μεταφέρει τη ροή δραστηριότητας.

Τα προγράμματα περιήγησης δεν μπορούν να ορίσουν κεφαλίδες στη χειραψία WebSocket, γι’ αυτό η διαδρομή WebSocket δέχεται επίσης το διακριτικό πρόσβασης ως παράμετρο query ?token=. Αυτό είναι το μόνο σημείο όπου ένα διακριτικό ανήκει σε URL· κάθε άλλη διαδρομή το δέχεται στην κεφαλίδα.

Οικογένειες πόρων

Υπάρχουν πολύ περισσότερα από χίλια endpoints, πάρα πολλά για να τα απαριθμήσουμε. Ομαδοποιούνται σε οικογένειες, και μόλις γνωρίζετε το πρόθεμα μιας οικογένειας, οι μεμονωμένες διαδρομές είναι προβλέψιμες: μια συλλογή στο πρόθεμα, μια εγγραφή κάτω από αυτό και ονομασμένες ενέργειες ως υποδιαδρομές POST.

ΟικογένειαΠρόθεμαΤι καλύπτει
Έλεγχος ταυτότητας/api/authΣύνδεση, ανανέωση, αποσύνδεση, η τρέχουσα ταυτότητα και οι συνεδρίες του καθενός.
Κανάλια και νήματα/api/channels, /api/threadsΤο μοντέλο συνομιλίας: τα κανάλια περιέχουν νήματα, τα νήματα περιέχουν μηνύματα, και οι απαντήσεις φωλιάζουν κάτω από τη ρίζα τους.
Πράκτορες/api/agentsΟρισμοί πρακτόρων, οι δεσμεύσεις τους σε κανάλια, οι συνομιλίες, οι εκκρεμότητες και η κατάστασή τους.
Έργα και πίνακες/api/projects, /api/tasks, /api/iterationsΠαρακολούθηση εργασιών: πίνακες και στήλες μέσα σε ένα έργο, εργασίες πάνω τους, και επαναλήψεις (iterations) πάνω από αυτές.
Βάση γνώσεων/api/knowledge-baseΧώροι με εκδόσεις σελίδων και αρχείων, με σχόλια, backlinks και αναζήτηση.
Email/api/mail, /api/mailbox-connectionsΣυνδεδεμένα γραμματοκιβώτια, γραμματοκιβώτια πρακτόρων και πρόχειρα που έγραψε ένας πράκτορας για να τα εγκρίνει ένα άτομο.
Εγκρίσεις/api/approvalsΗ ουρά ενεργειών που ένας πράκτορας δεν μπορεί να κάνει μόνος του, και η μία κλήση που τις επιλύει.
Ίχνος ελέγχου/api/audit-logΤο ίχνος ελέγχου του οργανισμού, με αλυσίδα κατακερματισμού, και ένα endpoint επαλήθευσης. Ιδιοκτήτες και διαχειριστές.
Κόστος και χρήση/api/ledger, /api/billingΛογιστική διακριτικών και κόστους, προϋπολογισμοί και χρόνοι εκτέλεσης· χωριστά, πιστώσεις και πληρωμές ανανέωσης. Ιδιοκτήτες και διαχειριστές.
Οργανισμός και ομάδες/api/organizations, /api/teams, /api/usersΔιαχείριση οργανισμού, ομάδων και συμμετοχών, συμπεριλαμβανομένων των προσκλήσεων.
Ενεργοποιήσεις/api/triggersΠρογράμματα και webhooks που ξεκινούν εργασία πρακτόρων, με το ιστορικό ενεργοποιήσεών τους.
Κλήσεις και φωνή/api/calls, /api/voiceΚλήσεις μέσα στο κανάλι, και η συνεδρία, η μεταγραφή και τα διακριτικά συσκευών του φωνητικού βοηθού.
Εκτελεστές/api/executors, /api/execution-environment-templatesΟι υπολογιστές όπου δουλεύουν οι πράκτορες, τα περιβάλλοντά τους και το κανάλι σύζευξης του daemon.
Εκτελέσεις/api/runsΖωντανές εκτελέσεις πρακτόρων: ακύρωση, συνέχιση, επανεκκίνηση, σύνδεση με εκτελεστή.
Μεταφορτώσεις/api/uploads, /api/attachmentsΑνεβάστε πρώτα ένα αρχείο, και μετά συνδέστε το επιστρεφόμενο αναγνωριστικό με ένα μήνυμα.
Αναζήτηση/api/messages/searchΑναζήτηση μηνυμάτων, σε όλον τον οργανισμό ή περιορισμένη σε ένα κανάλι.
Μυστικά και ρυθμίσεις/api/secrets, /api/settings/scopedΑποθήκευση διαπιστευτηρίων μόνο για εγγραφή, με ρητές άδειες, και ρυθμίσεις σε επίπεδα.
Συνδεδεμένα εργαλεία/api/mcpΔιακομιστές MCP στους οποίους καλεί αυτή η εγκατάσταση. Δείτε MCP και συνδεδεμένα εργαλεία.
Υγεία/api/healthΖωντάνια και ετοιμότητα, και τα δύο δημόσια. Το /api/health/ready ελέγχει επίσης τη βάση δεδομένων.

Τα δικαιώματα επιβάλλονται ανά διαδρομή, όχι ανά οικογένεια. Γενικά: το ίχνος ελέγχου, το καθολικό κόστους, οι κανόνες πολιτικής και το επίπεδο ελέγχου της inference είναι επιφάνειες για τους ιδιοκτήτες και τους διαχειριστές του οργανισμού, ενώ οι συνομιλίες, τα έργα, η γνώση και οι εγκρίσεις είναι ανοιχτά σε κάθε μέλος και περιορίζονται σε όσα μπορεί πραγματικά να δει αυτό το μέλος.

Παραδείγματα βήμα προς βήμα

Σύνδεση και διατήρηση του cookie ανανέωσης

Σε εγκατάσταση σε λειτουργία local, με κωδικό πρόσβασης. Το διακριτικό πρόσβασης επιστρέφει στο σώμα της απάντησης· το διακριτικό ανανέωσης καταλήγει στο cookie jar.

curl -sS https://api.example.com/api/auth/session \
  --cookie-jar nessie.cookies \
  -H 'Content-Type: application/json' \
  -d '{"email":"ada@example.com","password":"…"}'
{
  "data": {
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…",
    "me": {
      "user": {
        "id": "1d4e…",
        "email": "ada@example.com",
        "displayName": "Ada Lovelace",
        "roleIds": ["owner"],
        "superAdmin": false
      },
      "session": {
        "sessionId": "9c07…",
        "issuedAt": "2026-09-16T09:00:00.000Z",
        "expiresAt": "2026-09-16T09:30:00.000Z"
      },
      "context": {
        "organizationId": "5a81…",
        "projectId": "7b12…",
        "teamId": "2e60…",
        "channelId": null,
        "bootstrapMode": false
      },
      "auth": {
        "providerId": "local",
        "providerType": "local-bootstrap",
        "autoRedirectToSso": false
      }
    }
  }
}

Όταν λήξει το διακριτικό, ανταλλάξτε το cookie με νέο:

curl -sS -X POST https://api.example.com/api/auth/refresh \
  --cookie nessie.cookies --cookie-jar nessie.cookies

Η απάντηση έχει το ίδιο σχήμα { token, me }, και το cookie jar κρατά πλέον το περιστραμμένο διακριτικό ανανέωσης.

Λίστα των καναλιών που βλέπετε

curl -sS https://api.example.com/api/channels \
  -H "Authorization: Bearer $TOKEN"
{
  "data": [
    {
      "id": "8b2f…",
      "label": "engineering",
      "type": "standard",
      "visibility": "public",
      "projectId": "…",
      "projectName": "Platform",
      "teamId": "…",
      "defaultThreadId": "c41a…",
      "unreadCount": 3,
      "lastMessageAt": "2026-09-15T18:22:07.441Z",
      "viewerCanManage": true
    }
  ]
}

Προσθέστε ?teamId=… για να περιορίσετε στη μία ομάδα, ή ?includeArchived=true για να συμπεριλάβετε αρχειοθετημένα κανάλια. Το defaultThreadId είναι το νήμα όπου θα δημοσιεύσετε στη συνέχεια.

Δημοσίευση μηνύματος

Ένα μήνυμα χρειάζεται είτε κάποιο content είτε τουλάχιστον ένα μεταφορτωμένο συνημμένο. Στείλτε ένα Idempotency-Key, και ένα αίτημα που επαναλαμβάνεται θα αντιστοιχίζεται στο μήνυμα που δημιούργησε η πρώτη προσπάθεια, αντί να δημοσιεύσει δεύτερο αντίγραφο.

curl -sS https://api.example.com/api/threads/c41a…/messages \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: 0f9c2d16-8f4a-4c1e-9a77-6b0d4f2e1a53' \
  -d '{"content":"Deploying the migration now."}'

Ένα νέο μήνυμα είναι 201:

{
  "data": {
    "message": {
      "id": "d70e…",
      "threadId": "c41a…",
      "role": "user",
      "content": "Deploying the migration now.",
      "createdAt": "2026-09-16T09:04:11.882Z"
    },
    "pendingAgentInvites": []
  }
}

Μια επανάληψη με το ίδιο κλειδί idempotency απαντά 200 με το αρχικό μήνυμα και έναν κενό πίνακα pendingAgentInvites, επειδή η πρώτη προσπάθεια έκανε ήδη τις ειδοποιήσεις και την αποστολή που περιγράφει ο πίνακας.

Δύο απορρίψεις σε αυτή τη διαδρομή αξίζει να τις χειριστείτε ρητά. Περιεχόμενο πέρα από το όριο μήκους συνομιλίας είναι 413 MESSAGE_TOO_LARGE, και η σωστή απάντηση είναι να ανεβάσετε το κείμενο ως αρχείο. Περιεχόμενο που μοιάζει δομικά με διαπιστευτήριο είναι 422 SECRET_INTERCEPTED — ο διακομιστής αρνείται πριν αποθηκευτεί, σταλεί ή καταγραφεί το μήνυμα, και το διαπιστευτήριο ανήκει στο /api/secrets.