Chat REST API
API HTTP per integrare la chat Tidiko senza widget e senza SDK. Due host:
| Host | Base | Cosa fa |
|---|---|---|
| Laravel | https://app.tidiko.ai | Scambio chiavi pk_ / sk_ → JWT di chat |
| Node Agent | https://langgraphjs-prod.tidiko.ai | Thread, storico, messaggio JSON o SSE |
Staging Node: https://langgraphjs-stage.tidiko.ai. In locale usa NODE_SERVER_URL / socket_url restituito dallo scambio token.
- Widget Preact — UI Tidiko già pronta.
- Chat SDK Headless — socket + stato in browser, UI tua.
- Questa pagina — backend, CLI, mobile nativo, o un client HTTP tuo. Nessun Socket.io.
Le route interne Node (/create-thread/..., upload, queue) sono un altro contratto: HTTP API Node Agent.
Prova l’API
| Superficie | URL | Chi la usa |
|---|---|---|
| Chat API docs (OpenAPI + UI) | https://app.tidiko.ai/docs/api | Tutti. Spec interattiva: token, thread, JSON e SSE. |
| Spec machine-readable | https://app.tidiko.ai/docs/openapi.json | Client codegen, Postman, CI. |
| Playground (dashboard) | https://app.tidiko.ai/playground | Utente loggato. Prova REST Chat e Chat SDK sull’agente scelto. |
Nel Playground non incolli pk_ / sk_: la dashboard emette un JWT di sessione. I messaggi contano sul piano della company. Apri il tab REST con ?tab=rest, oppure un agente preciso con ?type=agent&uuid=…&tab=rest.
Nelle Chat API docs, “Try it out” sulle route Node richiede Authorization: Bearer con l’access JWT (non la chiave raw). Non incollare chiavi o JWT veri su uno schermo condiviso. I server nello spec seguono APP_URL e NODE_SERVER_URL dell’ambiente.
Cosa fa e cosa non fa
| Fa | Non fa |
|---|---|
JWT aud: tidiko-chat da pk_ / sk_ | Accettare pk_ / sk_ raw sul Node |
| Creare thread e inviare messaggi | Privacy cookie / welcome / form HITL del widget |
| Risposta JSON completa o stream SSE | UI, markdown, product card |
Autorizzare ogni threadId su company + agente del token | Lasciare che il client imposti laravelAppUrl o la collection |
Flusso
- Crea una chiave API in dashboard (API Keys).
- Scambia la chiave su Laravel. Per REST da server usa
sk_. - Chiama il Node con
Authorization: Bearer <access JWT>. - Crea un thread, poi invia messaggi su quel
threadId. - Rinnova con
refresh_tokenprima che scada l’access (TTL 3600s).
Esempio completo (server)
Da un backend (mai da una pagina pubblica). Sostituisci le variabili, poi esegui i passi in ordine.
LARAVEL="https://app.tidiko.ai"
SECRET="sk_live_xxx"
AGENT_ID="AGENT_UUID"
# 1. Scambia la chiave segreta (salva token e socket_url dalla risposta)
TOKEN_JSON=$(curl -sS -X POST "$LARAVEL/api/v1/service-token" \
-H "Content-Type: application/json" \
-d "{\"secret_key\":\"$SECRET\",\"agent_id\":\"$AGENT_ID\",\"type\":\"agent\"}")
ACCESS=$(printf '%s' "$TOKEN_JSON" | jq -r '.data.token')
NODE=$(printf '%s' "$TOKEN_JSON" | jq -r '.data.socket_url')
# Usa sempre data.socket_url (prod / stage / locale), non un host fisso.
# 2. Crea un thread
curl -sS -X POST "$NODE/v1/threads" \
-H "Authorization: Bearer $ACCESS" \
-H "Content-Type: application/json" \
-d '{}'
# Salva threadId (es. thread_<uuid>_<uuid>).
THREAD="thread_OWNER_UUID"
# 3. Invia un messaggio (JSON, attende la risposta intera)
curl -sS -X POST "$NODE/v1/threads/$THREAD/messages" \
-H "Authorization: Bearer $ACCESS" \
-H "Content-Type: application/json" \
-d '{"message":"Ciao, quali sono i vostri orari?"}'
# 4. Oppure stream SSE (stesso URL)
curl -N -X POST "$NODE/v1/threads/$THREAD/messages" \
-H "Authorization: Bearer $ACCESS" \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-d '{"message":"Ciao, quali sono i vostri orari?"}'
# 5. Storico
curl -sS "$NODE/v1/threads/$THREAD/messages" \
-H "Authorization: Bearer $ACCESS"
Per assistant usa "type":"assistant" e l’UUID dell’assistente (con o senza prefisso assistant_).
Token (Laravel)
Prefisso route: /api/v1. Content-Type: application/json.
POST /api/v1/token — chiave pubblica (pk_)
Per frontend / SDK. Origin serve solo al CORS (allowed_origins sulla chiave). L’auth è pk_ + binding company/agente.
curl -X POST https://app.tidiko.ai/api/v1/token \
-H "Content-Type: application/json" \
-H "Origin: https://tuo-sito.example" \
-d '{"public_key":"pk_live_xxx","agent_id":"AGENT_UUID","type":"agent"}'
POST /api/v1/service-token — chiave segreta (sk_)
Solo backend. Nessun CORS. Mai in HTML, bundle frontend o playground pubblico.
curl -X POST https://app.tidiko.ai/api/v1/service-token \
-H "Content-Type: application/json" \
-d '{"secret_key":"sk_live_xxx","agent_id":"AGENT_UUID","type":"agent"}'
Body comune
| Campo | Obbligatorio | Valori |
|---|---|---|
public_key o secret_key | Sì | pk_… / sk_… |
agent_id | Sì | UUID, con o senza prefisso agent_ / assistant_ |
type | Sì | agent o assistant |
Se la chiave è vincolata a un agente, agent_id deve coincidere (i prefissi vengono normalizzati).
POST /api/v1/token/refresh
curl -X POST https://app.tidiko.ai/api/v1/token/refresh \
-H "Content-Type: application/json" \
-d '{"refresh_token":"REFRESH_JWT"}'
Il refresh è single-use (JTI). Non riusarlo. Diverso da POST /api/jwt/refresh del widget embed.
Risposta 200
{
"success": true,
"data": {
"token": "eyJ…",
"refresh_token": "eyJ…",
"expires_in": 3600,
"jwt": "eyJ…",
"jwtRefresh": "eyJ…",
"socket_url": "https://langgraphjs-prod.tidiko.ai"
}
}
| Campo | Note |
|---|---|
token / jwt | Access JWT, aud: tidiko-chat, token_use: access, TTL 3600s |
refresh_token / jwtRefresh | Refresh, aud: tidiko-chat-refresh, TTL 86400s |
socket_url | Base URL del Node Agent (anche per REST, non solo socket) |
Scope di default: pk_ → chat:read, chat:write. sk_ → gli stessi più threads:manage.
Errori token
| HTTP | code | Quando |
|---|---|---|
| 404 | agent_not_found | Agente/assistente inesistente |
| 403 | agent_not_allowed | Company o binding chiave non coincidono |
| 401 | invalid_refresh_token | Refresh invalido, scaduto o già speso |
| 422 | (validazione Laravel) | Campi mancanti / type non valido |
Chat (Node Agent)
Base: valore di socket_url (senza slash finale). Header obbligatorio:
Authorization: Bearer <access JWT>
Accettati solo JWT firmati HS256, iss: neting-ai, aud: tidiko-chat, token_use: access, con companyId, agentId, jti.
Rifiutati (non usare come Bearer sul Node):
| Credenziale | HTTP | error |
|---|---|---|
| Header assente / non Bearer | 401 | No token provided / Unauthorized |
pk_… o sk_… raw | 403 | invalid_key |
Refresh (token_use: refresh o aud refresh) | 401 | invalid_token |
JWT widget (aud: tidiko-widget) | 403 | forbidden |
| JWT scaduto / firma errata | 401 | Invalid token |
Il Node carica il runtime (collection, istruzioni, limiti) da Laravel in base all’identità del token. Campi extra nel body (laravelAppUrl, collection_name, …) non diventano contesto trusted.
POST /v1/threads
Crea un thread e registra la conversazione su Laravel. Serve scope chat:write.
curl -X POST https://langgraphjs-prod.tidiko.ai/v1/threads \
-H "Authorization: Bearer $ACCESS_JWT" \
-H "Content-Type: application/json" \
-d '{}'
201:
{ "threadId": "thread_<ownerUuid>_<uuid>" }
Body opzionale: { "agentId": "agent_…" }. Se presente e diverso dal token → 403 { "error": "agent_mismatch" }.
POST /v1/threads/:threadId/messages
Invia un messaggio. Serve chat:write. threadId path-safe, 1–191 caratteri, regex ^[A-Za-z0-9][A-Za-z0-9._:-]*$.
{ "message": "Ciao" }
message stringa obbligatoria, altrimenti 400 { "error": "message is required" }.
Risposta JSON (default)
curl -X POST "https://langgraphjs-prod.tidiko.ai/v1/threads/$THREAD_ID/messages" \
-H "Authorization: Bearer $ACCESS_JWT" \
-H "Content-Type: application/json" \
-d '{"message":"Ciao"}'
200: { "message": "testo completo dell'assistente" }.
Stream SSE
Stesso URL. Header Accept: text/event-stream. curl -N non bufferizza.
curl -N -X POST "https://langgraphjs-prod.tidiko.ai/v1/threads/$THREAD_ID/messages" \
-H "Authorization: Bearer $ACCESS_JWT" \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-d '{"message":"Ciao"}'
| Evento | data | Quando |
|---|---|---|
start | {} | Inizio stream |
chunk | { "text": "…" } | Token / pezzo di testo |
tool | { "feedback": "…" } | Feedback tool in corso |
done | { "text": "…" } | Testo concatenato finale |
error | { "code", "message" } | Errore dopo che lo stream è già partito |
Esempio parser:
const res = await fetch(`${nodeUrl}/v1/threads/${threadId}/messages`, {
method: "POST",
headers: {
Authorization: `Bearer ${accessJwt}`,
"Content-Type": "application/json",
Accept: "text/event-stream",
},
body: JSON.stringify({ message: "Ciao" }),
});
// 400 / 401 / 403 / 429 / 500 arrive as JSON before any SSE body.
if (!res.ok) {
const err = await res.json().catch(() => ({}));
fail(err.code ?? String(res.status), err.message);
return;
}
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buf = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buf += decoder.decode(value, { stream: true });
const parts = buf.split("\n\n");
buf = parts.pop() ?? "";
for (const block of parts) {
let event = "message";
let payload = "{}";
for (const line of block.split("\n")) {
if (line.startsWith("event: ")) event = line.slice(7);
if (line.startsWith("data: ")) payload = line.slice(6);
}
const data = JSON.parse(payload);
if (event === "chunk") append(data.text);
if (event === "tool") showTool(data.feedback);
if (event === "done") finish(data.text);
if (event === "error") fail(data.code);
}
}
GET /v1/threads/:threadId/messages
Storico. Serve scope chat:read.
curl "https://langgraphjs-prod.tidiko.ai/v1/threads/$THREAD_ID/messages" \
-H "Authorization: Bearer $ACCESS_JWT"
200:
{
"messages": [
{ "type": "user", "content": "Ciao", "timestamp": 1710000000000 },
{ "type": "bot", "content": "Come posso aiutarti?", "timestamp": 1710000001000 }
]
}
type: user | bot | system. I turn tool-assisted sono fusi in un solo bot (stesso confine della bolla widget). Thread vuoto: { "messages": [] }.
Errori chat
| HTTP | error | Quando |
|---|---|---|
| 400 | Invalid thread id | threadId non valido |
| 400 | message is required | Body senza message stringa |
| 401 | No token provided / Invalid token / Unauthorized | Bearer mancante o JWT invalido |
| 403 | invalid_key | pk_ / sk_ usati come Bearer |
| 403 | forbidden | Audience/runtime/scope non ok |
| 403 | agent_mismatch | agentId nel body ≠ token |
| 403 | thread_forbidden | Thread di un altro company/agente |
| 429 | Message limit reached | Limite messaggi piano |
| 404 | owner_not_found | Runtime Laravel assente |
| 500 | internal_error / Server misconfiguration | Errore interno o LARAVEL_APP_URL mancante sul Node |
Dopo che lo SSE è partito, l’errore arriva come evento error, non come JSON HTTP.
Checklist
- Chiave
sk_in dashboard per integrazioni server;pk_solo se il client è un browser con origini CORS. POST /api/v1/service-token(o/token) → salvatokenerefresh_token.POST {socket_url}/v1/threads→ tienithreadId.POST …/messagescon{ "message": "…" }; per lo stream aggiungiAccept: text/event-stream.GET …/messagesper lo storico.- Refresh prima di
expires_in; non riusare un refresh speso. - Mai
sk_o refresh nel browser.
Sicurezza
- Il JWT access non va loggato per intero.
- Ogni
threadIdè autorizzato contro company + agente del token: non condividere thread tra tenant. allowed_originssullapk_è CORS, non auth. Unapk_leakata si revoca da/apikeys.- Il client non può cambiare collection, istruzioni o URL Laravel dal body: il Node le prende dal runtime trusted.