Saltar al contenido principal

Webhooks

La integración con webhooks permite a una organización suscribir uno o más endpoints para recibir confirmaciones de diferentes eventos. Pueden usarse para actualizar bases de datos, sistemas de inventario y similares. Los webhooks se configuran para cada convenio que tenga la organización.

Los datos se envían utilizando el método HTTP POST. Estos endpoints no serán visibles para los usuarios finales. Su único propósito es comunicar los diferentes sistemas backend integrados.

En cada endpoint, debe capturar la información necesaria para actualizar o almacenar según su caso de uso y dependiendo del lenguaje de programación que utilice.

Consideraciones

  • La información se envía utilizando el método POST en formato JSON.
  • Los métodos de autenticación que puede usar el endpoint son "Basic authentication" y "Bearer authentication". Las credenciales no pueden rotar automáticamente ni ser temporales.
  • Solo se aceptan webhooks que utilicen HTTPS (SSL).
  • La comunicación entre Leah y el endpoint webhook es de servidor a servidor. Es transparente para el usuario final.
  • Los endpoints deben responder con un código de estado HTTP entre 200 y 299 en caso de éxito. Cualquier otro código se considerará fallido.
  • En caso de que una petición resulte fallida, el sistema de Leah intentará enviar la información hasta dos veces más.

Eventos que pueden activar un webhook

Los eventos que pueden activar un webhook son los siguientes:

Puede utilizar un único endpoint que gestione todos los eventos o distintos endpoints para diferentes tipos de confirmaciones.

Evento de usuario registrado

Este evento activa una petición cuando un usuario se vincula a la organización. Esta vinculación ocurre de dos maneras: Cuando el usuario redime un código de convenio o cuando redime una clave de licencia. En ambos casos, se envía la petición con la información del usuario vinculado.

La petición POST enviada cuando un usuario se registra luce así:

POST / HTTP/1.1
Host: example.com
User-Agent: axios/1.6.2
Content-Length: 712
Accept: application/json, text/plain, */*
Accept-Encoding: gzip, compress, deflate, br
Authorization: Basic bXlfdXNlcjpteV9wYXNz
Content-Type: application/json
X-Forwarded-For: 0.0.0.0
X-Forwarded-Host: example.com
X-Forwarded-Proto: https

{"event":"USER_REGISTERED","user":{"id":"65e9c4884805c146b5770c61","personalInformation":{"email":"johndoe@example.com","familyName":"Doe","givenName":"John","phoneNumber":"+573334445555","picture":"https://cdn.example.com/pictures/profile.jpg","customFields":[{"name":"doc_type","value":"CC"},{"name":"doc_number","value":"1048222222"}]}},"partner":{"id":"662fc3c33eb47f6dcb97c71e","name":"Test Partner","code":null},"date":"2024-03-07T13:43:40.674Z","externalIds":["random-external-id"]}

Confirmación de inducción finalizada

Este evento activa una petición cuando un usuario completa su información de perfil, autoevaluación y meta de aprendizaje. La información enviada incluye toda esta nueva información.

La petición POST enviada cuando un usuario finaliza la inducción luce así:

POST / HTTP/1.1
Host: example.com
User-Agent: axios/1.6.2
Content-Length: 905
Accept: application/json, text/plain, */*
Accept-Encoding: gzip, compress, deflate, br
Authorization: Basic bXlfdXNlcjpteV9wYXNz
Content-Type: application/json
X-Forwarded-For: 0.0.0.0
X-Forwarded-Host: example.com
X-Forwarded-Proto: https

{"event":"ONBOARDING_FINISHED","externalIds":["random-external-id"],"date":"2024-09-02T14:31:28.757Z","user":{"id":"65e9c4884805c146b5770c61","personalInformation":{"email":"johndoe@example.com","familyName":"Doe","givenName":"John","phoneNumber":"+573334445555","picture":"https://cdn.example.com/pictures/profile.jpg","customFields":[{"name":"doc_type","value":"CC"},{"name":"doc_number","value":"1048222222"}]}},"partner":{"id":"662fc3c33eb47f6dcb97c71e","name":"Test Partner","code":null},"perception":{"countryCode":"CO","regionName":"Atlantico","proficiency":{"grammarAndVocabulary":5,"readingComprehension":4,"listeningComprehension":3,"writing":2,"speaking":1},"goal":"B2","timeStudyingEnglish":"BETWEEN_3_AND_5_YEARS","whyIsLearningEnglish":"INCREASE_YOUR_KNOWLEDGE","topicsOfInterest":["General English","Technology"]}}

Confirmación de examen diagnóstico finalizado

Este evento activa una petición cuando un usuario vinculado a la organización finaliza un examen diagnóstico. La información enviada incluye los resultados del usuario en el examen.

La petición POST enviada cuando un usuario finaliza un examen diagnóstico luce así:

POST / HTTP/1.1
Host: example.com
User-Agent: axios/1.6.2
Content-Length: 1079
Accept: application/json, text/plain, */*
Accept-Encoding: gzip, compress, deflate, br
Authorization: Basic bXlfdXNlcjpteV9wYXNz
Content-Type: application/json
X-Forwarded-For: 0.0.0.0
X-Forwarded-Host: example.com
X-Forwarded-Proto: https

{"event":"PLACEMENT_TEST_FINISHED","test":{"id":"65e9c74f4805c146b5770d4c","start":"2024-03-07T13:55:27.709Z","end":"2024-03-07T13:56:27.846Z","questionCount":36,"result":{"level":"A1","sublevel":"A1.2","score":7.61,"languageScore":2.65,"readingScore":24.26,"listeningScore":26.83,"pdf":"https://static.leahapp.com/certificates/diagnostic/fee7d137-8119-4ee7-b4f3.pdf"}},"user":{"id":"65e9c4884805c146b5770c61","personalInformation":{"email":"johndoe@example.com","familyName":"Doe","givenName":"John","phoneNumber":"+573334445555","picture":"https://cdn.example.com/pictures/profile.jpg","customFields":[{"name":"doc_type","value":"CC"},{"name":"doc_number","value":"1048222222"}]}},"partner":{"id":"662fc3c33eb47f6dcb97c71e","name":"Test Partner","code":null},"date":"2024-03-07T13:56:27.846Z","externalIds":["random-external-id"]}

Confirmación de examen de expresión oral finalizado

Este evento activa una petición cuando un usuario vinculado a la organización finaliza un examen de expresión oral. La información enviada incluye los resultados del usuario.

La petición POST enviada cuando un usuario finaliza un examen de expresión oral luce así:

POST / HTTP/1.1
Host: example.com
User-Agent: axios/1.6.2
Content-Length: 1110
Accept: application/json, text/plain, */*
Accept-Encoding: gzip, compress, deflate, br
Authorization: Basic bXlfdXNlcjpteV9wYXNz
Content-Type: application/json
X-Forwarded-For: 0.0.0.0
X-Forwarded-Host: example.com
X-Forwarded-Proto: https

{"event":"SPEAKING_TEST_FINISHED","test":{"id":"65e9c9384805c146b57710bc","start":"2024-03-07T14:03:36.894Z","end":"2024-03-07T14:05:45.078Z","questionCount":3,"isValid":true,"result":{"level":"Pre-A1","sublevel":"Pre-A1","score":42.87,"vocabularyScore":41.49,"fluencyScore":39.54,"pronunciationScore":59.31,"grammarScore":38.86,"pdf":"https://static.leahapp.com/certificates/speaking/e8b40139-c654-4fd3-bb14.pdf"}},"user":{"id":"65e9c4884805c146b5770c61","personalInformation":{"email":"johndoe@example.com","familyName":"Doe","givenName":"John","phoneNumber":"+573334445555","picture":"https://cdn.example.com/pictures/profile.jpg","customFields":[{"name":"doc_type","value":"CC"},{"name":"doc_number","value":"1048222222"}]}},"partner":{"id":"662fc3c33eb47f6dcb97c71e","name":"Test Partner","code":null},"date":"2024-03-07T14:05:45.078Z","externalIds":["random-external-id"]}

Confirmación de examen de expresión escrita finalizado

Este evento activa una petición cuando un usuario vinculado a la organización finaliza un examen de expresión escrita. La información enviada incluye los resultados del usuario.

La petición POST enviada cuando un usuario finaliza un examen de expresión escrita luce así:

POST / HTTP/1.1
Host: example.com
User-Agent: axios/1.6.2
Content-Length: 1110
Accept: application/json, text/plain, */*
Accept-Encoding: gzip, compress, deflate, br
Authorization: Basic bXlfdXNlcjpteV9wYXNz
Content-Type: application/json
X-Forwarded-For: 0.0.0.0
X-Forwarded-Host: example.com
X-Forwarded-Proto: https

{"event":"WRITING_TEST_FINISHED","test":{"id":"68813a5a1db72e5831361e7a","start":"2025-07-23T19:39:06.171Z","end":"2025-07-23T19:40:15.374Z","questionCount":3,"viewWalkThrough":false,"result":{"level":"Pre-A1","sublevel":"Pre-A1","score":0,"tenseScore":0,"sentenceScore":0,"clauseScore":0,"vocabScore":0}},"user":{"id":"669042b24a0228d9dd37b3d9","personalInformation":{"email":"john@doe.com","familyName":"Doe","givenName":"John","phoneNumber":"+573334445555","locale":"es","customFields":[]}},"partner":{"id":"6854681adb413a7e24998f5a","name":"Test partner","code":"TEST_CODE"},"date":"2025-07-23T19:40:15.374Z","externalIds":[]}

Confirmación de Nivel General

Este evento lanza una petición cuando el nivel general de un usuario es calculado.

La petición POST que se envía cuando se calcula el nivel general, luce así:

POST / HTTP/1.1
Host: example.com
User-Agent: axios/1.6.8
Content-Length: 972
Accept: application/json, text/plain, */*
Accept-Encoding: gzip, compress, deflate, br
Authorization: Basic bXlfdXNlcjpteV9wYXNz
Content-Type: application/json
X-Forwarded-For: 190.61.42.42
X-Forwarded-Host: example.com
X-Forwarded-Proto: https

{"event":"OVERALL_LEVEL","overall":{"score":27.4,"level":"Level 1","sublevel":"Sublevel 1.1"},"speakingTest":{"id":"663d0406c293f8d5895f639c","start":"2024-05-09T17:12:38.616Z","end":"2024-05-09T17:15:20.238Z","questionCount":3,"isValid":false,"result":{"level":"Pre-A1","sublevel":"Pre-A1","score":0,"vocabularyScore":0,"fluencyScore":0,"pronunciationScore":0,"grammarScore":0}},"placementTest":{"id":"6647c08136ec32eb28152cf6","start":"2024-05-17T20:39:29.218Z","end":"2024-05-17T20:41:09.135Z","questionCount":36,"result":{"level":"Level 1","sublevel":"Sublevel 1.1","score":21.75,"languageScore":23.37,"readingScore":21.09,"listeningScore":31.68}},"user":{"id":"660b2921fd05f52867c408e1","personalInformation":{"email":"johndoe@example.com","familyName":"Doe","givenName":"John","phoneNumber":"+573334445555","customFields":[]}},"partner":{"id":"6408f36388f7f41b188288a6","name":"Test Partner","code":"0000"},"date":"2024-05-17T20:41:23.238Z","externalIds":[]}

Ejemplo

A continuación, se encuentran instrucciones para construir un servidor sencillo que sea capaz de funcionar como un "webhook" que puede gestionar los tipos de confirmaciones que puede enviar Leah.

El ejemplo permitirá implementar las siguientes consideraciones:

  • Manejar autenticación a través de "Bearer Secret Token" y "Basic HTTP". Si la autenticación es incorrecta, responder con un código de estado HTTP 401 Unauthorized.
  • Manejar cualquiera de los distintos tipos de confirmación: Usuario registrado, inducción finalizada, examen diagnóstico finalizado y examen de expresión oral finalizado.
  • Validar los tipos de eventos recibidos por cada confirmación, y en caso de que no se detecte un tipo válido responde con un código de estado HTTP 400 Bad request. Si es un evento correcto responde con un código de estado HTTP 200 Success.

Paso 1: Crear servidor

Guardar el siguiente bloque de código en un archivo con nombre server.mjs:

server.mjs
import http from "node:http";

const SECRET_TOKEN = "qhDm976TwG2sBZcftRLube";
const USER = "my_user";
const PASSWORD = "my_pass";

http
.createServer(function (req, res) {
const token = req.headers.authorization ?? "";
const auth = checkToken(token);

if (!auth) {
res.writeHead(401);
res.write("Unauthorized");
return res.end();
}

const chunks = [];
req.on("data", (chunk) => chunks.push(chunk));
req.on("end", () => {
const data = JSON.parse(Buffer.concat(chunks).toString());

switch (data.event) {
case "USER_REGISTERED":
console.log("User registered", JSON.stringify(data, null, 4));
break;
case "ONBOARDING_FINISHED":
console.log("Onboarding finished", JSON.stringify(data, null, 4));
break;
case "PLACEMENT_TEST_FINISHED":
console.log("Placement Test finished", JSON.stringify(data, null, 4));
break;
case "SPEAKING_TEST_FINISHED":
console.log("Speaking Test finished", JSON.stringify(data, null, 4));
break;
case "WRITING_TEST_FINISHED":
console.log("Writing Test finished", JSON.stringify(data, null, 4));
break;
case "OVERALL_LEVEL":
console.log("Overall Level", JSON.stringify(data, null, 4));
break;
default:
res.writeHead(400);
res.write("Bad request");
return res.end();
}

res.writeHead(200, { "Content-Type": "application/json" });
res.write(JSON.stringify({ success: true }));
res.end();
});
})
.listen(8080);

function checkToken(token) {
const [tokenType, tokenValue] = token.split(" ");

if (tokenType === "Bearer") {
if (tokenValue === SECRET_TOKEN) return true;
} else if (tokenType === "Basic") {
const credentials = Buffer.from(tokenValue, "base64").toString();
const [user, pass] = credentials.split(":");
if (USER === user && PASSWORD === pass) return true;
}

return false;
}
info

El código de arriba sirve solo a modo de ejemplo y no está pensado para usarse en un entorno real. Contiene malas prácticas y problemas de seguridad como las credenciales incluidas directamente en el código, la comparación de credenciales sin un algoritmo de tiempo constante y no realiza ninguna operación real. Lo único que se realiza es imprimir los datos de la petición en la stdout.

Paso 2: Ejecutar servidor

El servidor se inicia en el puerto 8080 ejecutando el siguiente comando:

node server.mjs

Paso 3: Simular confirmaciones entrantes

Se pueden hacer peticiones al servidor utilizando curl. Por ejemplo, para simular el evento de usuario registrado, utilizando autenticación "Basic HTTP", se puede ejecutar el siguiente comando curl:

curl --request POST \
--url http://localhost:8080/ \
--header 'Authorization: Basic bXlfdXNlcjpteV9wYXNz' \
--header 'Content-Type: application/json' \
--header 'accept: application/json' \
--data '{
"event": "USER_REGISTERED",
"user": {
"id": "65e9c4884805c146b5770c61",
"personalInformation": {
"email": "johndoe@example.com",
"familyName": "Doe",
"givenName": "John",
"phoneNumber": "+573334445555",
"picture": "https://cdn.example.com/pictures/profile.jpg",
"customFields": [
{
"name": "doc_type",
"value": "CC"
},
{
"name": "doc_number",
"value": "1048222222"
}
]
}
},
"date": "2024-03-07T13:43:40.674Z",
"externalIds": [
"random-external-id"
]
}'
tip

A través del comando curl se envía la información de la confirmación de usuario registrado. Puede probar con cualquiera de las otra confirmaciones utilizando las peticiones de ejemplos discutidas arriba.

Configuración de "webhooks"

Para configurar uno o más "webhooks" debe tener un convenio activo con Leah. Puede ser un convenio que utilice el flujo de "Código de convenio" o el flujo de "Claves de licencias".

Si posee con convenio capaz de generar claves de licencia, Leah le entregó un identificador conocido como "partnerId". Para agregar un "webhook" que reciba confirmaciones de este convenio, debe enviar un correo electrónico al agente o desarrollador que gestiona su convenio con la siguiente información:

  1. Especificar el "partnerId".
  2. URL del "endpoint".
  3. Tipo de autenticación y credenciales del "endpoint".
  4. Tipo de confirmaciones que deben enviarse a ese "webhook".