Webhooks και συμβάντα
Έκδοση API 2026-10-affable-towhee
Το Lingara καταγράφει ως συμβάντα ό,τι συμβαίνει στα σχέδια μαθήματος και στη χρήση του λογαριασμού σας, και δέχεται συμβάντα από το παιχνίδι ή την εφαρμογή σας. Κάθε συμβάν, όποιον δρόμο κι αν ακολουθεί, έχει τον ίδιο φάκελο, και ο κατάλογος συμβάντων τα παραθέτει όλα.
Ο φάκελος
Κάθε συμβάν φέρει έξι πεδία. Το id αρχίζει με lgr_evt_, είναι μοναδικό και είναι το κλειδί για την αφαίρεση διπλοτύπων. Το type ονομάζει το συμβάν. Το created_at είναι η στιγμή που συνέβη. Το api_version είναι η έκδοση στη μορφή της οποίας είναι το data: η δεσμευμένη έκδοση του πελάτη σας ή, στην τροφοδοσία και στη ροή, η έκδοση που όρισε το αίτημά σας στο Lingara-Version. Το subject αρχίζει με lgr_sub_ και λέει ποιον αφορά το συμβάν: είναι σταθερό για τον πελάτη σας αλλά διαφορετικό για κάθε πελάτη, και δεν είναι ποτέ email, όνομα ή αναγνωριστικό λογαριασμού. Το data είναι μικρό και ονομάζει πόρους αντί να τους αντιγράφει: ανακτήστε έναν πόρο με το εύρος δικαιωμάτων που χρειάζεται.
Δύο τύποι χρειάζονται από μία πρόταση. Το lesson_plan.ready μπορεί να φτάσει δύο φορές για ένα σχέδιο, πρώτα με data.status partial και έπειτα complete: ενεργήστε με το πρώτο για ένα αξιοποιήσιμο σχέδιο ή περιμένετε το complete για να έχετε κάθε σύνολο. Το usage.threshold_reached στέλνεται μόνο για λογαριασμούς και πελάτες με χρέωση βάσει χρήσης, και ένα άλμα πάνω από πολλά όρια αναφέρει μόνο το υψηλότερο που ξεπεράστηκε, οπότε μην περιμένετε ένα συμβάν ανά όριο.
Ένα αρχείο καταγραφής, τρεις τρόποι να το ακούσετε
Τα webhooks ταιριάζουν σε έναν διακομιστή με δημόσιο τελικό σημείο HTTPS. Η τροφοδοσία και η ροή ταιριάζουν σε ένα πρόγραμμα που δεν έχει, όπως ένα παιχνίδι στον υπολογιστή ενός παίκτη. Ο φάκελος είναι ίδιος σε κάθε δρόμο, οπότε ένα πρόγραμμα μπορεί να ξεκινήσει με την τροφοδοσία και να περάσει αργότερα στα webhooks χωρίς να αλλάξει τον τρόπο που διαβάζει ένα συμβάν. Οι διαδρομές συμβάντων απαντούν σε εγγενή προγράμματα. Ένα παιχνίδι που εκτελείται σε πρόγραμμα περιήγησης δεν μπορεί ακόμη να τις καλέσει, επειδή το /v1/ δεν απαντά σε cross-origin preflight.
Ποιος ακούει ένα συμβάν
Ένας πελάτης ακούει ένα συμβάν όταν έχει το events:read και το εύρος δικαιωμάτων του ίδιου του τύπου συμβάντος, που αναφέρει ο κατάλογος, και όταν το συμβάν αφορά τον κάτοχο του πελάτη. Στην τροφοδοσία και στη ροή, τα εύρη δικαιωμάτων του διακριτικού πρόσβασης το περιορίζουν περαιτέρω, και το types το περιορίζει στους τύπους που ονομάζετε. Το webhook.test πηγαίνει μόνο στο τελικό σημείο στο οποίο στάλθηκε, ποτέ στην τροφοδοσία, και δεν επιδέχεται συνδρομή. Τα app.installed και app.uninstalled πηγαίνουν μόνο στον πελάτη της ίδιας της εφαρμογής, ποτέ σε άλλον πελάτη του ίδιου λογαριασμού.
Καταχωρίστε ένα τελικό σημείο
Καταχωρίστε ένα τελικό σημείο στη σελίδα Webhooks της εφαρμογής web του Lingara, στη διεύθυνση app.getlingara.com/admin/webhooks, επιλέγοντας πρώτα τον πελάτη. Το URL του πρέπει να χρησιμοποιεί https στη θύρα 443, και το όνομα διακομιστή του πρέπει να επιλύεται μόνο σε δημόσιες διευθύνσεις. Επιλέξτε τα «Συμβάντα προς αποστολή»: προσφέρονται μόνο οι τύποι που επιτρέπουν τα εύρη δικαιωμάτων του πελάτη. Το URL και τα συμβάντα δεν μπορούν να αλλάξουν αργότερα: προσθέστε νέο τελικό σημείο και διαγράψτε το παλιό. Το μυστικό υπογραφής αρχίζει με lgr_whsec_ και εμφανίζεται μόνο μία φορά.
Επαληθεύστε μια παράδοση
Κάθε παράδοση είναι ένα POST με τρεις κεφαλίδες, σύμφωνα με την προδιαγραφή Standard Webhooks: webhook-id (το id του συμβάντος), webhook-timestamp και webhook-signature. Το κλειδί HMAC είναι το αποκωδικοποιημένο από base64 τμήμα του μυστικού μετά το lgr_whsec_, ποτέ το μυστικό ως συμβολοσειρά. Επαληθεύστε πριν αναλύσετε το σώμα, πάνω στα ακατέργαστα bytes του, όπως παρακάτω. Απορρίψτε μια παράδοση της οποίας η χρονοσφραγίδα απέχει περισσότερο από πέντε λεπτά από τώρα, την προεπιλογή των βιβλιοθηκών 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 δέχονται ολόκληρο το μυστικό.
Απαντήστε γρήγορα, αναμένετε επαναλήψεις
Απαντήστε με οποιοδήποτε 2xx μέσα σε 10 δευτερόλεπτα και κάντε την εργασία μετά. Οτιδήποτε άλλο, συμπεριλαμβανομένης της λήξης χρονικού ορίου ή ενός 3xx (οι ανακατευθύνσεις δεν ακολουθούνται), επαναλαμβάνεται με αυξανόμενα διαστήματα για περίπου μία ημέρα. Ένα 410 ως απάντηση σε αυτόματη παράδοση απενεργοποιεί αμέσως το τελικό σημείο· ένα 410 ως απάντηση σε δοκιμή ή σε εκ νέου παράδοση όχι. Μετά από πέντε ημέρες αποτυχημένων παραδόσεων το τελικό σημείο απενεργοποιείται επίσης. Και στις δύο περιπτώσεις, ο κάτοχός του λαμβάνει email. Ένα σφάλμα από την πλευρά του Lingara δεν μετρά ποτέ για την απενεργοποίηση ενός τελικού σημείου. Από τη σελίδα Webhooks μπορείτε να κάνετε «Αποστολή δοκιμής» ή «Εκ νέου παράδοση» οποιασδήποτε παράδοσης των τελευταίων 30 ημερών. Καθεμία είναι μία προσπάθεια, χωρίς ποτέ επανάληψη, και στέλνεται ακόμη και σε απενεργοποιημένο τελικό σημείο.
Η παράδοση γίνεται τουλάχιστον μία φορά και χωρίς σειρά. Το ίδιο συμβάν μπορεί να φτάσει δύο φορές, και μια επανάληψη μπορεί να φτάσει μετά από ένα μεταγενέστερο συμβάν. Το webhook-id είναι ίδιο σε κάθε επανάληψη, και σε μια εκ νέου παράδοση έως 30 ημέρες αργότερα. Καταγράψτε κάθε id που έχετε χειριστεί για 30 ημέρες και αγνοήστε ένα διπλότυπο. Ταξινομήστε κατά created_at αν η σειρά έχει σημασία.
Εναλλαγή μυστικού υπογραφής
Ένα τελικό σημείο μπορεί να έχει δύο μυστικά υπογραφής ταυτόχρονα. Όσο είναι ενεργά και τα δύο, το webhook-signature φέρει δύο καταχωρίσεις v1,, και ένας παραλήπτης που δέχεται οποιαδήποτε από τις δύο συνεχίζει να λειτουργεί. Προσθέστε το νέο μυστικό στον διακομιστή σας, αναπτύξτε το και έπειτα ανακαλέστε το παλιό.
Η τροφοδοσία
Το GET /v1/events με το διακριτικό πρόσβασης ενός πελάτη επιστρέφει items (φακέλους), next_cursor και has_more. Το διακριτικό χρειάζεται το events:read και το εύρος δικαιωμάτων κάθε τύπου που θέλετε να ακούτε: μόνο με το events:read, η τροφοδοσία είναι κενή. Ξεκινά από τώρα. Στείλτε start=oldest για τα συμβάντα περίπου των τελευταίων 30 ημερών. Δεν χρειάζεται δημόσιο τελικό σημείο ούτε μυστικό υπογραφής: το διακριτικό πρόσβασης αποδεικνύει ποιος ρωτά. Η παρακάτω ανταλλαγή ζητά και τα δύο εύρη δικαιωμάτων που χρειάζονται τα συμβάντα σχεδίων μαθήματος.
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 μεταφέρει τα ίδια συμβάντα ως server-sent events. Το data κάθε πλαισίου event είναι ένας φάκελος, και το id: κάθε πλαισίου είναι ένας δρομέας, η ίδια τιμή με το next_cursor, οπότε μπορείτε να εναλλάσσεστε μεταξύ τροφοδοσίας και ροής χωρίς κενό. Μετά από μια σύνδεση που διακόπηκε, ένα πλαίσιο done (η ροή τερματίζεται μόνη της κατά διαστήματα) ή ένα πλαίσιο error, επανασυνδεθείτε με το Last-Event-ID ορισμένο στο τελευταίο id: που λάβατε. Οι περισσότεροι πελάτες SSE το κάνουν αυτό για εσάς, όπως και το tailEvents στις βιβλιοθήκες του Lingara (το streamEvents εκεί είναι μία μόνο σύνδεση). Είναι δρομέας, όχι το id του συμβάντος. Η ροή στέλνει σήμα ζωής, οπότε μια σιωπηλή σύνδεση είναι νεκρή σύνδεση.
curl -N "https://api.getlingara.com/v1/events/stream" \
-H "Authorization: Bearer $LINGARA_TOKEN"Στείλτε ένα συμβάν στο Lingara
Το POST /v1/events με το events:write στέλνει ένα συμβάν στο Lingara ως {type, data}: world.context_changed (ένα scene, source_lang, target_lang, level και προαιρετικά ένα npc με name και persona, και tags) ή world.practice_requested (ένα topic και τις ίδιες γλώσσες και επίπεδο). Το Idempotency-Key είναι υποχρεωτικό: έως 255 ορατοί χαρακτήρες ASCII, όπως ένα UUID. Χωρίς αυτό η απάντηση είναι 400 και idempotency_key_required. Ορίστε το μία φορά ανά συμβάν και στείλτε το ίδιο κλειδί σε μια επανάληψη. Ένα κλειδί είναι ένα συμβάν: μέσα σε μία ημέρα, ένα δεύτερο αίτημα με το ίδιο κλειδί λαμβάνει την πρώτη απάντηση (ίση ως JSON, όχι byte προς byte), ακόμη κι αν το σώμα του διαφέρει, και μετά από αυτό λαμβάνει το ίδιο συμβάν, όπως παρακάτω. Τα εισερχόμενα συμβάντα δεν υπογράφονται: το διακριτικό πρόσβασής σας είναι η απόδειξη. Περιγράψτε τον κόσμο, ποτέ τον παίκτη: κανένα όνομα ή συνομιλία στα 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. Αλλαγές γραμμής, στηλοθέτες και άλλοι χαρακτήρες ελέγχου απορρίπτονται, όπως και οι αόρατοι χαρακτήρες και οι χαρακτήρες μορφοποίησης: παρακάμψεις κατεύθυνσης, χαρακτήρες μηδενικού πλάτους εκτός από τους συνδέτες που χρειάζονται ορισμένα συστήματα γραφής και τα emoji, το μπλοκ ετικετών και οι χαρακτήρες ιδιωτικής χρήσης. Το tags περιέχει έως 8 ετικέτες μηχανής με πεζά γράμματα, των έως 24 χαρακτήρων η καθεμία, και δεν φτάνει ποτέ στο σχέδιο μαθήματος. Το level είναι από 1 έως 9, και οι δύο γλώσσες πρέπει να διαφέρουν. Ένα αίτημα εκτός αυτών των ορίων απορρίπτεται με 400 και δεν καταγράφει κανένα συμβάν.
Με "generate": true (η προεπιλογή για το world.practice_requested), το διακριτικό χρειάζεται επίσης το lesson_plans:write. Χωρίς αυτό το αίτημα απορρίπτεται με 403 και δεν καταγράφεται κανένα συμβάν. Με αυτό, το Lingara ξεκινά ένα σχέδιο μαθήματος, με τους ίδιους ελέγχους και την ίδια χρέωση με την απευθείας δημιουργία του, και το reaction της απάντησης 202 λέει τι συνέβη. Με started και plan_status generating, ακολουθεί ένα lesson_plan.ready ή lesson_plan.failed του οποίου το data.plan_id είναι το plan_id της απάντησης, σε κάθε δρόμο που χρησιμοποιείτε. Με partial ή complete, το σχέδιο προήλθε από τη βιβλιοθήκη και μπορεί να διαβαστεί τώρα, και δεν υπόσχεται κανένα συμβάν: κάποιο μπορεί ακόμη να φτάσει, οπότε μόνο για το generating αξίζει να περιμένετε. Με refused ή failed, το συμβάν εξακολουθεί να ισχύει. Δεν επαναλαμβάνεται με το ίδιο κλειδί, οπότε στείλτε νέο συμβάν για να προσπαθήσετε ξανά.
Μια επανάληψη μέσα σε μία ημέρα λαμβάνει πίσω την πρώτη απάντηση. Μια επανάληψη μετά από αυτό ανασυντίθεται από το αποθηκευμένο συμβάν, το οποίο κρατά το σχέδιο που ξεκίνησε αλλά όχι τον λόγο για τον οποίο απορρίφθηκε μια αντίδραση. Έτσι μια καθυστερημένη επανάληψη μπορεί να απαντήσει με reaction failed και internal: αυτό σημαίνει ότι το πρώτο αποτέλεσμα δεν καταγράφηκε, όχι ότι δεν υπάρχει σχέδιο. Διαβάστε το σχέδιο με το plan_id του αν το κρατήσατε, ή στείλτε νέο συμβάν.
Tidewater Games: ένα παιχνίδι χωρίς διακομιστή
Η Tidewater Games, ένα φανταστικό στούντιο, φτιάχνει ένα παιχνίδι Godot στο οποίο ο παίκτης εξερευνά μια νυχτερινή αγορά. Ο προγραμματιστής του εκτελεί το παιχνίδι στον δικό του υπολογιστή, με τον δικό του πελάτη.
Ο παίκτης μπαίνει σε έναν πάγκο με νουντλς. Το παιχνίδι στέλνει το world.context_changed με "generate": true, την εντολή της παραπάνω ενότητας για τα εισερχόμενα συμβάντα, και κρατά το plan_id από την απάντηση.
Αν το plan_status είναι generating, το παιχνίδι διαβάζει τη ροή, ή ρωτά επανειλημμένα την τροφοδοσία, μέχρι να φτάσει ένα lesson_plan.ready με αυτό το plan_id. Έπειτα διαβάζει το σχέδιο με το 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 μετρά επίσης ως σχέδιο μαθήματος. Κάθε παράδοση webhook μετρά μία φορά ανά συμβάν ανά τελικό σημείο, στο πρώτο της 2xx, όποια προσπάθεια κι αν είναι αυτή. Δεν μετρά ποτέ ξανά, και μια δοκιμή δεν μετρά ποτέ. Το GET /v1/usage δείχνει τον μήνα μέχρι στιγμής.
Αν μια μετάφραση διαφέρει από την αγγλική τεκμηρίωση, ισχύει η αγγλική τεκμηρίωση.