Lingara Lingara 문서 학습 가이드 API 라이브러리 앱 만들기 웹 앱
언어: 한국어

웹훅과 이벤트

API 버전 2026-10-affable-towhee

Lingara는 계정의 수업 계획과 사용량에 일어난 일을 이벤트로 기록하고, 게임이나 앱에서 보내는 이벤트도 받습니다. 어떤 경로로 전달되든 모든 이벤트는 같은 엔벨로프를 가지며, 이벤트 카탈로그에 모두 나와 있습니다.

엔벨로프

모든 이벤트에는 여섯 개의 필드가 있습니다. id는 lgr_evt_로 시작하고 고유하며, 중복 제거의 키입니다. type은 이벤트의 종류를 나타냅니다. created_at은 이벤트가 발생한 시각입니다. api_version은 data의 형태가 따르는 버전으로, 클라이언트에 고정된 버전이거나, 피드와 스트림에서는 요청이 Lingara-Version에 지정한 버전입니다. subject는 lgr_sub_로 시작하며 이벤트가 누구에 관한 것인지 나타냅니다. 같은 클라이언트에서는 변하지 않지만 클라이언트마다 다르며, 이메일이나 이름, 계정 ID인 경우는 없습니다. data는 작으며 리소스를 복사하지 않고 이름으로 가리킵니다. 리소스는 그에 필요한 스코프로 가져오세요.

두 가지 종류는 한 문장씩 설명이 필요합니다. lesson_plan.ready는 한 계획에 대해 두 번 올 수 있습니다. 먼저 data.status가 partial로, 그다음 complete로 옵니다. 바로 쓸 수 있는 계획이 필요하면 첫 번째에 반응하고, 모든 세트가 필요하면 complete를 기다리세요. usage.threshold_reached는 사용량 기반 결제 계정과 클라이언트에만 전송되며, 여러 기준을 한꺼번에 넘으면 넘은 것 중 가장 높은 기준만 보고됩니다. 따라서 기준마다 이벤트가 하나씩 올 것이라고 기대하지 마세요.

하나의 로그, 세 가지 수신 방법

웹훅은 공개 HTTPS 엔드포인트가 있는 서버에 적합합니다. 피드와 스트림은 플레이어의 컴퓨터에서 실행되는 게임처럼 공개 엔드포인트가 없는 프로그램에 적합합니다. 엔벨로프는 모든 경로에서 같으므로, 프로그램은 피드로 시작했다가 나중에 이벤트를 읽는 방식을 바꾸지 않고 웹훅으로 옮겨 갈 수 있습니다. 이벤트 경로는 네이티브 프로그램에 응답합니다. 브라우저에서 실행되는 게임은 아직 이를 호출할 수 없는데, /v1/이 교차 출처 프리플라이트에 응답하지 않기 때문입니다.

누가 이벤트를 받나요

클라이언트는 events:read와 카탈로그에 나온 해당 이벤트 종류의 스코프를 모두 가지고 있고, 이벤트가 클라이언트 소유자에 관한 것일 때 이벤트를 받습니다. 피드와 스트림에서는 액세스 토큰의 스코프가 범위를 더 좁히고, types가 지정한 종류로 좁힙니다. webhook.test는 전송된 엔드포인트로만 가고 피드로는 절대 가지 않으며, 구독할 수 없습니다. app.installed와 app.uninstalled는 해당 앱 자체의 클라이언트로만 가며, 같은 계정의 다른 클라이언트로는 가지 않습니다.

엔드포인트 등록하기

Lingara 웹 앱의 웹훅 페이지(app.getlingara.com/admin/webhooks)에서 먼저 클라이언트를 선택한 뒤 엔드포인트를 등록하세요. URL은 포트 443에서 https를 사용해야 하며, 호스트는 공개 주소로만 확인되어야 합니다. 보낼 이벤트를 선택하세요. 클라이언트의 스코프가 허용하는 종류만 표시됩니다. URL과 이벤트는 나중에 수정할 수 없으므로, 새 엔드포인트를 추가하고 이전 엔드포인트를 삭제하세요. 서명 시크릿은 lgr_whsec_로 시작하며 한 번만 표시됩니다.

전송 검증하기

모든 전송은 Standard Webhooks 사양에 따라 세 개의 헤더가 붙은 POST입니다. webhook-id(이벤트의 id), webhook-timestamp, webhook-signature입니다. HMAC 키는 시크릿에서 lgr_whsec_ 뒤의 부분을 base64로 디코딩한 것이며, 시크릿 문자열 자체가 아닙니다. 아래와 같이 본문을 파싱하기 전에 원시 바이트에 대해 검증하세요. 타임스탬프가 현재와 5분 넘게 차이 나는 전송은 거부하세요. 이는 Standard Webhooks 라이브러리의 기본값이며, 가로챈 전송이 재사용되는 것을 막습니다.

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는 대부분의 언어용 검증 라이브러리를 제공합니다. 이 라이브러리들은 whsec_ 뒤에 base64가 오는 형식이나 base64만 있는 형식의 시크릿을 기대하므로, Lingara 시크릿에서 lgr_whsec_ 뒤의 부분을 넘기세요. Lingara 자체 라이브러리는 시크릿 전체를 받습니다.

빠르게 응답하고 재시도에 대비하기

10초 안에 아무 2xx로 응답하고, 작업은 그 뒤에 하세요. 시간 초과나 3xx(리디렉션은 따라가지 않음)를 포함한 그 밖의 응답은 간격을 늘려 가며 약 하루 동안 재시도됩니다. 자동 전송에 대한 응답이 410이면 엔드포인트가 즉시 비활성화되지만, 테스트나 재전송에 대한 410은 그렇지 않습니다. 5일 동안 전송이 실패해도 엔드포인트가 비활성화됩니다. 어느 경우든 소유자에게 이메일이 갑니다. Lingara 측의 장애는 엔드포인트 비활성화에 절대 포함되지 않습니다. 웹훅 페이지에서 테스트를 보내거나, 최근 30일 동안의 어떤 전송이든 재전송할 수 있습니다. 각각은 한 번만 시도되고 재시도되지 않으며, 비활성화된 엔드포인트에도 전송됩니다.

전송은 최소 한 번 이루어지며 순서가 보장되지 않습니다. 같은 이벤트가 두 번 올 수 있고, 재시도가 나중 이벤트보다 늦게 올 수도 있습니다. webhook-id는 모든 재시도에서, 그리고 최대 30일 후의 재전송에서도 같습니다. 처리한 각 id를 30일 동안 기록하고 반복은 무시하세요. 순서가 중요하다면 created_at으로 정렬하세요.

서명 시크릿 교체하기

엔드포인트는 서명 시크릿을 동시에 두 개 가질 수 있습니다. 둘 다 유효한 동안 webhook-signature에는 v1, 항목이 두 개 담기며, 어느 쪽이든 받아들이는 수신 측은 계속 동작합니다. 새 시크릿을 서버에 추가해 배포한 다음, 이전 시크릿을 취소하세요.

피드

클라이언트의 액세스 토큰으로 GET /v1/events를 호출하면 items(엔벨로프), next_cursor, has_more가 반환됩니다. 토큰에는 events:read와 받고 싶은 각 종류의 스코프가 필요합니다. events:read만 있으면 피드는 비어 있습니다. 피드는 지금부터 시작합니다. 최근 약 30일의 이벤트를 받으려면 start=oldest를 넘기세요. 공개 엔드포인트도 서명 시크릿도 필요 없으며, 누가 요청하는지는 액세스 토큰이 증명합니다. 아래 교환은 수업 계획 이벤트에 필요한 두 스코프를 모두 요청합니다.

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는 항상 있습니다. 저장해 두었다가 cursor로 다시 넘기세요. 이 값은 불투명합니다. has_more가 true면 바로 다시 호출하고, false면 따라잡은 것이므로 나중에 폴링하거나 스트림을 여세요. 30일보다 오래된 커서는 410과 cursor_expired로 거부됩니다. 커서 없이 호출하면 피드는 지금부터 시작하고 그 사이의 이벤트는 건너뜁니다. 이를 복구하려면 이벤트가 보관되는 기간까지 거슬러 올라가는 start=oldest로 호출하고, 이미 처리한 id 값은 건너뛰세요.

스트림

GET /v1/events/stream은 같은 이벤트를 서버 전송 이벤트로 전달합니다. 각 event 프레임의 data는 엔벨로프 하나이고, 각 프레임의 id:는 커서로서 next_cursor와 같은 토큰이므로, 피드와 스트림 사이를 공백 없이 전환할 수 있습니다. 연결이 끊기거나, done 프레임(스트림은 때때로 스스로 종료됨) 또는 error 프레임을 받은 뒤에는 마지막으로 받은 id:를 Last-Event-ID에 설정해 다시 연결하세요. 대부분의 SSE 클라이언트가 이를 대신 해 주며, Lingara 라이브러리의 tailEvents도 마찬가지입니다(거기서 streamEvents는 단일 연결입니다). 이것은 커서이지 이벤트의 id가 아닙니다. 스트림은 하트비트를 보내므로, 조용한 연결은 끊어진 연결입니다.

curl -N "https://api.getlingara.com/v1/events/stream" \
  -H "Authorization: Bearer $LINGARA_TOKEN"

Lingara로 이벤트 보내기

events:write가 있는 POST /v1/events는 이벤트를 {type, data} 형태로 Lingara에 보냅니다. world.context_changed(scene, source_lang, target_lang, level, 선택적으로 name과 persona가 있는 npc, 그리고 tags) 또는 world.practice_requested(topic과 같은 언어 및 레벨)입니다. Idempotency-Key는 필수이며, UUID처럼 최대 255자의 보이는 ASCII 문자입니다. 없으면 응답은 400과 idempotency_key_required입니다. 이벤트마다 한 번 설정하고, 재시도할 때는 같은 키를 보내세요. 키 하나는 이벤트 하나입니다. 하루 안에 같은 키로 두 번째 요청을 보내면 본문이 달라도 첫 번째 응답(바이트 단위가 아니라 JSON으로서 동일)을 받고, 그 이후에는 아래와 같이 같은 이벤트를 받습니다. 수신 이벤트에는 서명이 없으며, 액세스 토큰이 증명입니다. 플레이어가 아니라 세계를 묘사하세요. scene, npc, topic, 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}}'

각 텍스트 필드는 보이는 문자로 된 한 줄이며, 앞뒤 공백을 제거한 뒤 셉니다. scene과 topic은 최대 160자, npc.name은 최대 32자, npc.persona는 최대 120자입니다. 줄바꿈, 탭 및 기타 제어 문자는 거부되며, 보이지 않는 문자와 서식 문자도 거부됩니다. 방향 재정의 문자, 일부 문자 체계와 이모지에 필요한 결합 문자를 제외한 폭 없는 문자, 태그 블록, 사용자 정의 영역 문자가 여기에 해당합니다. tags에는 각각 최대 24자의 소문자 기계용 토큰을 최대 8개 넣을 수 있으며, 이는 수업 계획에 절대 전달되지 않습니다. level은 1부터 9까지이며, 두 언어는 서로 달라야 합니다. 이 제한을 벗어난 요청은 400으로 거부되고 이벤트가 기록되지 않습니다.

"generate": true(world.practice_requested의 기본값)이면 토큰에 lesson_plans:write도 필요합니다. 없으면 요청은 403으로 거부되고 이벤트가 기록되지 않습니다. 있으면 Lingara는 직접 만들 때와 같은 검사와 같은 요금으로 수업 계획을 시작하고, 202 응답의 reaction이 무슨 일이 일어났는지 알려 줍니다. started이고 plan_status가 generating이면, data.plan_id가 응답의 plan_id인 lesson_plan.ready 또는 lesson_plan.failed가 사용하는 모든 경로로 뒤따릅니다. partial 또는 complete이면 계획은 라이브러리에서 온 것이라 지금 바로 읽을 수 있으며, 이벤트는 약속되지 않습니다. 이벤트가 올 수도 있지만 기다릴 가치가 있는 것은 generating뿐입니다. refused 또는 failed여도 이벤트 자체는 유효합니다. 같은 키로는 재시도되지 않으므로, 다시 시도하려면 새 이벤트를 보내세요.

하루 안의 재시도는 첫 번째 응답을 돌려받습니다. 그 이후의 재시도는 저장된 이벤트로부터 다시 만들어지는데, 이벤트가 시작한 계획은 유지되지만 반응이 거부된 이유는 유지되지 않습니다. 따라서 늦은 재시도는 reaction failed와 internal로 응답할 수 있습니다. 이는 첫 결과가 기록되지 않았다는 뜻이지, 계획이 없다는 뜻이 아닙니다. plan_id를 보관했다면 그것으로 계획을 읽거나, 새 이벤트를 보내세요.

Tidewater Games: 서버 없는 게임

가상의 스튜디오 Tidewater Games는 플레이어가 야시장을 탐험하는 Godot 게임을 만듭니다. 개발자는 자신의 컴퓨터에서 자신의 클라이언트로 게임을 실행합니다.

플레이어가 국수 노점에 들어갑니다. 게임은 위 'Lingara로 이벤트 보내기' 절의 명령으로 "generate": true와 함께 world.context_changed를 보내고, 응답의 plan_id를 보관합니다.

plan_status가 generating이면, 게임은 해당 plan_id의 lesson_plan.ready가 도착할 때까지 스트림을 읽거나 피드를 폴링합니다. 그런 다음 아래와 같이 lesson_plans:read로 계획을 읽습니다. 계획이 이미 complete였다면 바로 읽습니다.

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"

나중에 스튜디오는 HTTPS 엔드포인트가 있는 작은 서버를 추가하고 lesson_plan.ready에 등록합니다. 같은 이벤트가 같은 id로 그곳에 도착하며, 이벤트를 읽는 게임 코드는 바뀌지 않습니다.

클라이언트 시크릿은 절대 게임 빌드에 포함되어서는 안 됩니다. 플레이어의 기기에 있는 것은 무엇이든 읽힐 수 있기 때문입니다. Lingara가 플레이어를 대신한 로그인을 지원하기 전까지, 플레이어 컴퓨터의 게임은 스튜디오 자체 서버와 통신하고, 개발자 자신의 사본만 Lingara와 직접 통신합니다.

이벤트 비용

클라이언트의 사용량은 모든 /v1/ 호출을 세는 것처럼, 수락된 수신 이벤트, 피드 호출, 열린 스트림을 각각 셉니다. "generate": true로 보낸 이벤트는 수업 계획으로도 셉니다. 각 웹훅 전송은 몇 번째 시도이든 첫 2xx 시점에 이벤트와 엔드포인트 쌍마다 한 번 셉니다. 다시 세는 일은 없고, 테스트는 세지 않습니다. GET /v1/usage에서 이번 달 현재까지의 사용량을 볼 수 있습니다.

번역본과 영어 참조 문서의 내용이 다른 경우, 영어 참조 문서가 올바른 것으로 봅니다.