Webhook ed eventi
Versione API 2026-10-affable-towhee
Lingara registra come eventi ciò che accade ai piani di lezione e all'utilizzo del tuo account, e accetta eventi dal tuo gioco o dalla tua app. Ogni evento, comunque viaggi, ha la stessa busta, e il catalogo degli eventi li elenca tutti.
La busta
Ogni evento contiene sei campi. id inizia con lgr_evt_, è univoco ed è la chiave su cui deduplicare. type indica l'evento. created_at indica quando è avvenuto. api_version è la versione secondo cui è strutturato data: quella a cui è fissato il tuo client oppure, nel feed e nel flusso, la versione indicata dalla tua richiesta in Lingara-Version. subject inizia con lgr_sub_ e dice a chi si riferisce l'evento: è stabile per il tuo client ma diverso per ogni client, e non è mai un'email, un nome o un ID account. data è piccolo e indica le risorse invece di copiarle: recupera una risorsa con lo scope di cui ha bisogno.
Due tipi meritano una frase ciascuno. lesson_plan.ready può arrivare due volte per lo stesso piano, prima con data.status partial e poi complete: agisci sul primo per avere un piano utilizzabile, oppure attendi complete per avere tutti i set. usage.threshold_reached viene inviato solo per account e client con fatturazione a consumo, e un salto oltre più soglie segnala solo la più alta superata, quindi non aspettarti un evento per soglia.
Un registro, tre modi per ascoltarlo
I webhook sono adatti a un server con un endpoint HTTPS pubblico. Il feed e il flusso sono adatti a un programma che non ne ha uno, come un gioco sul computer di un giocatore. La busta è la stessa su ogni canale, quindi un programma può iniziare con il feed e passare in seguito ai webhook senza cambiare il modo in cui legge un evento. Le route degli eventi rispondono ai programmi nativi. Un gioco eseguito in un browser non può ancora chiamarle, perché /v1/ non risponde ad alcuna richiesta preliminare cross-origin.
Chi riceve un evento
Un client riceve un evento quando possiede events:read e lo scope proprio del tipo di evento, indicato nel catalogo, e quando l'evento riguarda il proprietario del client. Nel feed e nel flusso, gli scope del token di accesso restringono ulteriormente l'insieme, e types lo restringe ai tipi che indichi. webhook.test va solo all'endpoint a cui è stato inviato, mai al feed, e non è possibile abbonarsi. app.installed e app.uninstalled vanno solo al client dell'app stessa, mai a un altro client dello stesso account.
Registra un endpoint
Registra un endpoint nella pagina Webhook dell'app web di Lingara, all'indirizzo app.getlingara.com/admin/webhooks, scegliendo prima il client. Il suo URL deve usare https sulla porta 443, e il suo host deve risolversi solo in indirizzi pubblici. Scegli gli «Eventi da inviare»: vengono proposti solo i tipi consentiti dagli scope del client. L'URL e gli eventi non possono essere modificati in seguito: aggiungi un nuovo endpoint ed elimina quello vecchio. Il segreto di firma inizia con lgr_whsec_ e viene mostrato una sola volta.
Verifica una consegna
Ogni consegna è un POST con tre intestazioni, secondo la specifica Standard Webhooks: webhook-id (l'id dell'evento), webhook-timestamp e webhook-signature. La chiave HMAC è la parte del segreto dopo lgr_whsec_, decodificata da base64, mai il segreto come stringa. Verifica prima di analizzare il corpo, sui suoi byte grezzi, come qui sotto. Rifiuta una consegna il cui timestamp si discosta di più di cinque minuti dall'ora attuale, il valore predefinito delle librerie Standard Webhooks: questo impedisce di riprodurre una consegna intercettata.
signed = webhook-id + "." + webhook-timestamp + "." + raw request body
key = base64_decode(the secret after its prefix)
expected = "v1," + base64(hmac_sha256(key, signed))
accept if |now - webhook-timestamp| <= 5 minutes
and some entry of webhook-signature (space-separated) equals expected
(compare in constant time)Standard Webhooks pubblica verificatori per la maggior parte dei linguaggi. Si aspettano un segreto scritto come whsec_ seguito da base64, oppure come solo base64, quindi passa loro la parte del segreto Lingara dopo lgr_whsec_. Le librerie di Lingara accettano il segreto intero.
Rispondi in fretta, aspettati nuovi tentativi
Rispondi con un qualsiasi 2xx entro 10 secondi e svolgi il lavoro dopo. Qualsiasi altra risposta, compreso un timeout o un 3xx (i reindirizzamenti non vengono seguiti), viene ritentata a intervalli crescenti per circa un giorno. Un 410 in risposta a una consegna automatica disattiva subito l'endpoint; un 410 in risposta a una prova o a un nuovo invio no. Dopo cinque giorni di consegne non riuscite, l'endpoint viene disattivato anche in questo caso. In entrambi i casi il suo proprietario riceve un'email. Un guasto da parte di Lingara non conta mai ai fini della disattivazione di un endpoint. Dalla pagina Webhook puoi usare «Invia prova», oppure «Invia di nuovo» per qualsiasi consegna degli ultimi 30 giorni. Ciascuno è un singolo tentativo, mai ripetuto, e viene inviato anche a un endpoint disattivato.
La consegna avviene almeno una volta e senza ordine. Lo stesso evento può arrivare due volte, e un nuovo tentativo può arrivare dopo un evento successivo. webhook-id è lo stesso a ogni nuovo tentativo, e in un nuovo invio fino a 30 giorni dopo. Registra per 30 giorni ogni id che hai gestito e ignora le ripetizioni. Ordina per created_at se l'ordine conta.
Ruota un segreto di firma
Un endpoint può avere due segreti di firma contemporaneamente. Finché entrambi sono attivi, webhook-signature contiene due voci v1,, e un ricevitore che accetta l'una o l'altra continua a funzionare. Aggiungi il nuovo segreto al tuo server, distribuiscilo, poi revoca quello vecchio.
Il feed
GET /v1/events con il token di accesso di un client restituisce items (buste), next_cursor e has_more. Il token richiede events:read e lo scope di ogni tipo che vuoi ricevere: con il solo events:read, il feed è vuoto. Parte da adesso. Passa start=oldest per gli eventi degli ultimi 30 giorni circa. Non richiede né un endpoint pubblico né un segreto di firma: il token di accesso dimostra chi sta chiedendo. Lo scambio qui sotto richiede entrambi gli scope di cui hanno bisogno gli eventi dei piani di lezione.
export LINGARA_TOKEN="$(curl -sS --fail-with-body -X POST "https://api.getlingara.com/oauth/token" \
-u "$LINGARA_CLIENT_ID:$LINGARA_CLIENT_SECRET" \
-d "grant_type=client_credentials" \
--data-urlencode "scope=events:read lesson_plans:read" | jq -r '.access_token // error(.error)')"curl "https://api.getlingara.com/v1/events" \
-H "Authorization: Bearer $LINGARA_TOKEN"next_cursor è sempre presente: salvalo e ripassalo come cursor. È opaco. has_more true significa richiamare subito, e false significa che sei in pari: interroga più tardi, oppure apri il flusso. Un cursore più vecchio di 30 giorni viene rifiutato con 410 e cursor_expired. Senza cursore il feed parte da adesso, e gli eventi intermedi vengono saltati. Per recuperarli, chiama con start=oldest, che risale fin dove gli eventi vengono conservati, e salta i valori id che hai già gestito.
Il flusso
GET /v1/events/stream trasmette gli stessi eventi come eventi inviati dal server (SSE). Il data di ogni frame event è una busta, e l'id: di ogni frame è un cursore, lo stesso token di next_cursor, quindi puoi passare dal feed al flusso senza lacune. Dopo una connessione interrotta, un frame done (il flusso termina da solo di tanto in tanto) o un frame error, riconnettiti con Last-Event-ID impostato sull'ultimo id: ricevuto. La maggior parte dei client SSE lo fa per te, e così anche tailEvents nelle librerie di Lingara (streamEvents lì è una singola connessione). È un cursore, non l'id dell'evento. Il flusso invia un segnale di vita periodico, quindi una connessione silenziosa è una connessione morta.
curl -N "https://api.getlingara.com/v1/events/stream" \
-H "Authorization: Bearer $LINGARA_TOKEN"Invia un evento a Lingara
POST /v1/events con events:write invia un evento a Lingara come {type, data}: world.context_changed (una scene, source_lang, target_lang, level e, facoltativamente, un npc con name e persona, e tags) oppure world.practice_requested (un topic e le stesse lingue e lo stesso livello). Idempotency-Key è obbligatoria: fino a 255 caratteri ASCII visibili, come un UUID. Senza di essa la risposta è 400 e idempotency_key_required. Impostala una volta per evento e invia la stessa chiave in un nuovo tentativo. Una chiave è un evento: entro un giorno, una seconda richiesta con la stessa chiave riceve la prima risposta (uguale come JSON, non byte per byte), anche se il suo corpo è diverso, e dopo riceve lo stesso evento, come qui sotto. Gli eventi in ingresso non sono firmati: la prova è il tuo token di accesso. Descrivi il mondo, mai il giocatore: niente nomi o chat in scene, npc, topic o tags.
export LINGARA_TOKEN="$(curl -sS --fail-with-body -X POST "https://api.getlingara.com/oauth/token" \
-u "$LINGARA_CLIENT_ID:$LINGARA_CLIENT_SECRET" \
-d "grant_type=client_credentials" \
--data-urlencode "scope=events:write lesson_plans:write" | jq -r '.access_token // error(.error)')"curl -X POST "https://api.getlingara.com/v1/events" \
-H "Authorization: Bearer $LINGARA_TOKEN" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-H "Content-Type: application/json" \
-d '{"type":"world.context_changed","data":{"scene":"A night market in Taipei, just after rain","npc":{"name":"Auntie Lin","persona":"a street-food vendor who likes to haggle"},"source_lang":"en","target_lang":"zh","level":3,"tags":["market","food","chapter-2"],"generate":true}}'Ogni campo di testo è una sola riga di caratteri visibili, contati dopo aver rimosso gli spazi iniziali e finali: scene e topic fino a 160, npc.name fino a 32 e npc.persona fino a 120. Interruzioni di riga, tabulazioni e altri caratteri di controllo vengono rifiutati, così come i caratteri invisibili e di formattazione: forzature di direzione, caratteri a larghezza zero diversi dai caratteri di unione di cui hanno bisogno alcune scritture e le emoji, il blocco dei tag e i caratteri per uso privato. tags contiene fino a 8 token macchina in minuscolo di massimo 24 caratteri ciascuno, e non raggiunge mai il piano di lezione. level va da 1 a 9, e le due lingue devono essere diverse. Una richiesta al di fuori di questi limiti viene rifiutata con 400 e non registra alcun evento.
Con "generate": true (il valore predefinito per world.practice_requested), il token richiede anche lesson_plans:write. Senza, la richiesta viene rifiutata con 403 e non viene registrato alcun evento. Con esso, Lingara avvia un piano di lezione, con gli stessi controlli e la stessa fatturazione della creazione diretta, e il campo reaction della risposta 202 dice cosa è successo. Con started e plan_status generating, segue un lesson_plan.ready o lesson_plan.failed il cui data.plan_id è il plan_id della risposta, su ogni canale che usi. Con partial o complete, il piano proviene dalla libreria e può essere letto subito, e nessun evento è garantito: uno potrebbe comunque arrivare, quindi vale la pena attendere solo con generating. Con refused o failed, l'evento resta valido. Non viene ritentato con la stessa chiave, quindi invia un nuovo evento per riprovare.
Un nuovo tentativo entro un giorno riceve di nuovo la prima risposta. Un nuovo tentativo successivo viene ricostruito dall'evento memorizzato, che conserva il piano avviato ma non il motivo per cui una reazione è stata rifiutata. Quindi un nuovo tentativo tardivo può rispondere con reaction failed e internal: significa che il primo esito non è stato registrato, non che non esista alcun piano. Leggi il piano tramite il suo plan_id se l'hai conservato, oppure invia un nuovo evento.
Tidewater Games: un gioco senza server
Tidewater Games, uno studio fittizio, sviluppa un gioco Godot in cui il giocatore esplora un mercato notturno. Il suo sviluppatore esegue il gioco sul proprio computer, con il proprio client.
Il giocatore entra in una bancarella di noodle. Il gioco invia world.context_changed con "generate": true, il comando della sezione sugli eventi in ingresso qui sopra, e conserva il plan_id della risposta.
Se plan_status è generating, il gioco legge il flusso, o interroga il feed, finché non arriva un lesson_plan.ready con quel plan_id. Poi legge il piano con lesson_plans:read, come qui sotto. Se il piano era già complete, lo legge subito.
export LINGARA_TOKEN="$(curl -sS --fail-with-body -X POST "https://api.getlingara.com/oauth/token" \
-u "$LINGARA_CLIENT_ID:$LINGARA_CLIENT_SECRET" \
-d "grant_type=client_credentials" \
--data-urlencode "scope=lesson_plans:read" | jq -r '.access_token // error(.error)')"curl "https://api.getlingara.com/v1/lesson-plans/$ID" \
-H "Authorization: Bearer $LINGARA_TOKEN"In seguito lo studio aggiunge un piccolo server con un endpoint HTTPS e lo registra per lesson_plan.ready. Lì arriva lo stesso evento, con lo stesso id, e il codice del gioco che lo legge non cambia.
Un segreto del client non deve mai essere incluso in una build del gioco, perché tutto ciò che si trova sul dispositivo di un giocatore può essere letto. Finché Lingara non supporterà l'accesso per conto di un giocatore, un gioco sui computer dei giocatori comunica con il proprio server, e solo la copia dello sviluppatore comunica direttamente con Lingara.
Quanto costano gli eventi
L'utilizzo del tuo client conta ogni evento in ingresso accettato, ogni chiamata al feed e ogni flusso aperto, così come conta ogni chiamata /v1/. Un evento inviato con "generate": true conta anche come un piano di lezione. Ogni consegna di webhook conta una volta per evento per endpoint, al suo primo 2xx, qualunque tentativo sia. Non conta mai di nuovo, e una prova non conta mai. GET /v1/usage mostra il mese finora.
In caso di discrepanza tra una traduzione e il riferimento in inglese, fa fede il riferimento in inglese.