Webhooks e eventos
Versão da API 2026-10-affable-towhee
O Lingara regista como eventos o que acontece aos planos de aula e à utilização da sua conta, e aceita eventos do seu jogo ou aplicação. Cada evento, seja qual for o caminho por onde viaja, tem o mesmo envelope, e o catálogo de eventos lista-os todos.
O envelope
Cada evento transporta seis campos. id começa por lgr_evt_, é único e é a chave para eliminar duplicados. type identifica o evento. created_at indica quando aconteceu. api_version é a versão segundo a qual data está estruturado: a versão em que o seu cliente está fixado ou, no feed e no fluxo, a versão que o seu pedido indicou em Lingara-Version. subject começa por lgr_sub_ e indica a quem o evento diz respeito: é estável para o seu cliente mas diferente para cada cliente, e nunca é um email, um nome ou um ID de conta. data é pequeno e identifica recursos em vez de os copiar: obtenha um recurso com o âmbito de que ele precisa.
Dois tipos merecem uma frase cada. lesson_plan.ready pode chegar duas vezes para o mesmo plano, primeiro com data.status partial e depois complete: aja com base no primeiro para ter um plano utilizável, ou aguarde complete para ter todos os conjuntos. usage.threshold_reached só é enviado para contas e clientes com faturação por utilização, e um salto que ultrapasse vários limiares só comunica o mais alto ultrapassado, por isso não espere um evento por limiar.
Um registo, três formas de o ouvir
Os webhooks servem um servidor com um endpoint HTTPS público. O feed e o fluxo servem um programa que não o tenha, como um jogo no computador de um jogador. O envelope é o mesmo em todos os canais, por isso um programa pode começar pelo feed e passar mais tarde para webhooks sem mudar a forma como lê um evento. As rotas de eventos respondem a programas nativos. Um jogo a correr num navegador ainda não as pode chamar, porque /v1/ não responde a nenhum pedido preliminar de origem cruzada.
Quem ouve um evento
Um cliente ouve um evento quando detém events:read e o âmbito próprio do tipo de evento, indicado no catálogo, e quando o evento diz respeito ao proprietário do cliente. No feed e no fluxo, os âmbitos do token de acesso restringem-no ainda mais, e types restringe-o aos tipos que indicar. webhook.test vai apenas para o endpoint para o qual foi enviado, nunca para o feed, e não permite subscrição. app.installed e app.uninstalled vão apenas para o cliente da própria app, nunca para outro cliente da mesma conta.
Registar um endpoint
Registe um endpoint na página Webhooks da aplicação web do Lingara, em app.getlingara.com/admin/webhooks, escolhendo primeiro o cliente. O URL tem de usar https na porta 443, e o anfitrião só pode resolver para endereços públicos. Escolha os «Eventos a enviar»: só são oferecidos os tipos que os âmbitos do cliente permitem. O URL e os eventos não podem ser editados mais tarde: adicione um novo endpoint e elimine o antigo. O segredo de assinatura começa por lgr_whsec_ e só é mostrado uma vez.
Verificar uma entrega
Cada entrega é um POST com três cabeçalhos, segundo a especificação Standard Webhooks: webhook-id (o id do evento), webhook-timestamp e webhook-signature. A chave HMAC é a parte do segredo depois de lgr_whsec_, descodificada de base64, nunca o segredo como cadeia de texto. Verifique antes de analisar o corpo, sobre os seus bytes em bruto, como abaixo. Recuse uma entrega cujo carimbo de data/hora se afaste mais de cinco minutos da hora atual, a predefinição das bibliotecas Standard Webhooks: isso impede que uma entrega intercetada seja reproduzida.
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)A Standard Webhooks publica verificadores para a maioria das linguagens. Esperam um segredo escrito como whsec_ seguido de base64, ou apenas em base64, por isso passe-lhes a parte do segredo do Lingara depois de lgr_whsec_. As bibliotecas do próprio Lingara aceitam o segredo completo.
Responda depressa, conte com novas tentativas
Responda com qualquer 2xx em 10 segundos e faça o trabalho depois. Qualquer outra resposta, incluindo um tempo limite esgotado ou um 3xx (os redirecionamentos não são seguidos), é repetida com intervalos crescentes durante cerca de um dia. Um 410 em resposta a uma entrega automática desativa o endpoint de imediato; um 410 em resposta a um teste ou a um reenvio não. Após cinco dias de entregas falhadas, o endpoint também é desativado. Em qualquer dos casos, o proprietário recebe um email. Uma falha do lado do Lingara nunca conta para desativar um endpoint. Na página Webhooks pode «Enviar teste», ou «Reenviar» qualquer entrega dos últimos 30 dias. Cada um é uma única tentativa, nunca repetida, e é enviado mesmo para um endpoint desativado.
A entrega é feita pelo menos uma vez e sem ordem. O mesmo evento pode chegar duas vezes, e uma nova tentativa pode chegar depois de um evento posterior. webhook-id é o mesmo em cada nova tentativa, e num reenvio até 30 dias depois. Registe durante 30 dias cada id que já tratou e ignore as repetições. Ordene por created_at se a ordem importar.
Rodar um segredo de assinatura
Um endpoint pode ter dois segredos de assinatura ao mesmo tempo. Enquanto ambos estão ativos, webhook-signature contém duas entradas v1,, e um recetor que aceite qualquer uma delas continua a funcionar. Adicione o novo segredo ao seu servidor, implemente-o e depois revogue o antigo.
O feed
GET /v1/events com o token de acesso de um cliente devolve items (envelopes), next_cursor e has_more. O token precisa de events:read e do âmbito de cada tipo que quer ouvir: só com events:read, o feed está vazio. Começa a partir de agora. Passe start=oldest para obter os eventos de cerca dos últimos 30 dias. Não precisa de endpoint público nem de segredo de assinatura: o token de acesso prova quem está a pedir. A troca abaixo pede os dois âmbitos de que os eventos de planos de aula precisam.
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 está sempre presente: guarde-o e devolva-o como cursor. É opaco. has_more true significa chamar de novo já, e false significa que está em dia: consulte mais tarde, ou abra o fluxo. Um cursor com mais de 30 dias é recusado com 410 e cursor_expired. Sem cursor, o feed começa a partir de agora, e os eventos intermédios são ignorados. Para os recuperar, chame com start=oldest, que recua até onde os eventos são guardados, e ignore os valores id que já tratou.
O fluxo
GET /v1/events/stream transmite os mesmos eventos como eventos enviados pelo servidor (SSE). O data de cada trama event é um envelope, e o id: de cada trama é um cursor, o mesmo token que next_cursor, por isso pode alternar entre o feed e o fluxo sem lacunas. Após uma ligação perdida, uma trama done (o fluxo termina sozinho de vez em quando) ou uma trama error, volte a ligar-se com Last-Event-ID definido para o último id: que recebeu. A maioria dos clientes SSE faz isto por si, tal como tailEvents nas bibliotecas do Lingara (streamEvents é aí uma única ligação). É um cursor, não o id do evento. O fluxo envia um sinal de vida periódico, por isso uma ligação silenciosa é uma ligação morta.
curl -N "https://api.getlingara.com/v1/events/stream" \
-H "Authorization: Bearer $LINGARA_TOKEN"Enviar um evento ao Lingara
POST /v1/events com events:write envia um evento ao Lingara como {type, data}: world.context_changed (uma scene, source_lang, target_lang, level e, opcionalmente, um npc com name e persona, e tags) ou world.practice_requested (um topic e os mesmos idiomas e nível). Idempotency-Key é obrigatório: até 255 caracteres ASCII visíveis, como um UUID. Sem ele, a resposta é 400 e idempotency_key_required. Defina-o uma vez por evento e envie a mesma chave numa nova tentativa. Uma chave é um evento: durante um dia, um segundo pedido com a mesma chave recebe a primeira resposta (igual enquanto JSON, não byte a byte), mesmo que o corpo seja diferente, e depois disso recebe o mesmo evento, como abaixo. Os eventos recebidos não são assinados: o seu token de acesso é a prova. Descreva o mundo, nunca o jogador: nada de nomes ou conversas em scene, npc, topic ou 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}}'Cada campo de texto é uma única linha de caracteres visíveis, contados depois de remover os espaços nas extremidades: scene e topic até 160, npc.name até 32 e npc.persona até 120. Quebras de linha, tabulações e outros caracteres de controlo são recusados, tal como os caracteres invisíveis e de formatação: substituições de direção, caracteres de largura zero que não sejam os conectores de que alguns sistemas de escrita e os emoji precisam, o bloco de etiquetas e os caracteres de uso privado. tags contém até 8 tokens de máquina em minúsculas com até 24 caracteres cada, e nunca chega ao plano de aula. level vai de 1 a 9, e os dois idiomas têm de ser diferentes. Um pedido fora destes limites é recusado com 400 e não regista nenhum evento.
Com "generate": true (a predefinição para world.practice_requested), o token também precisa de lesson_plans:write. Sem ele, o pedido é recusado com 403 e nenhum evento é registado. Com ele, o Lingara inicia um plano de aula, com as mesmas verificações e a mesma faturação de uma criação direta, e o campo reaction da resposta 202 diz o que aconteceu. Com started e plan_status generating, segue-se um lesson_plan.ready ou lesson_plan.failed cujo data.plan_id é o plan_id da resposta, em todos os canais que usar. Com partial ou complete, o plano veio da biblioteca e pode ser lido já, e nenhum evento é prometido: pode ainda chegar um, por isso só vale a pena esperar com generating. Com refused ou failed, o evento mantém-se. Não é repetido com a mesma chave, por isso envie um novo evento para tentar de novo.
Uma nova tentativa dentro de um dia recebe de volta a primeira resposta. Uma nova tentativa depois disso é reconstruída a partir do evento guardado, que mantém o plano que iniciou mas não o motivo pelo qual uma reação foi recusada. Por isso, uma nova tentativa tardia pode responder com reaction failed e internal: isso significa que o primeiro resultado não foi registado, e não que não exista nenhum plano. Leia o plano pelo seu plan_id se o guardou, ou envie um novo evento.
Tidewater Games: um jogo sem servidor
A Tidewater Games, um estúdio fictício, cria um jogo em Godot no qual o jogador explora um mercado noturno. O seu programador corre o jogo no próprio computador, com o seu próprio cliente.
O jogador entra numa banca de noodles. O jogo publica world.context_changed com "generate": true, o comando da secção de eventos recebidos acima, e guarda o plan_id da resposta.
Se plan_status for generating, o jogo lê o fluxo, ou consulta o feed, até chegar um lesson_plan.ready com esse plan_id. Depois lê o plano com lesson_plans:read, como abaixo. Se o plano já estava complete, lê-o de imediato.
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"Mais tarde, o estúdio acrescenta um pequeno servidor com um endpoint HTTPS e regista-o para lesson_plan.ready. O mesmo evento chega lá, com o mesmo id, e o código do jogo que o lê não muda.
Um segredo do cliente nunca deve ser incluído numa compilação do jogo, porque tudo o que está no dispositivo de um jogador pode ser lido. Até o Lingara suportar o início de sessão em nome de um jogador, um jogo nos computadores dos jogadores comunica com o seu próprio servidor, e só a cópia do próprio programador comunica diretamente com o Lingara.
Quanto custam os eventos
A utilização do seu cliente conta cada evento recebido aceite, cada chamada ao feed e cada fluxo aberto, tal como conta cada chamada a /v1/. Um evento enviado com "generate": true conta também como um plano de aula. Cada entrega de webhook conta uma vez por evento e por endpoint, no seu primeiro 2xx, seja qual for a tentativa. Nunca volta a contar, e um teste nunca conta. GET /v1/usage mostra o mês até agora.
Se uma tradução e a referência em inglês divergirem, prevalece a referência em inglês.