Skip to main content

Webhooks

Integration with webhooks allows an organization to subscribe one or more endpoints to receive confirmations of different events. They can be used to update your databases, inventory system and the like. Webhooks are set up for each partnership that the organization has.

Data is sent using the HTTP POST method. These endpoints will not be visible to end users. Its sole purpose is to communicate the different integrated backend systems.

On each endpoint, you must capture the information necessary to update or store according to your use case and depending on the programming language you use.

Considerations

  • The information is sent using the method POST in JSON format.
  • The authentication methods that the endpoint can use are Basic authentication and Bearer authentication. Credentials cannot auto-rotate or be temporary.
  • Only webhooks that use HTTPS (SSL) are accepted.
  • Communication between Leah and the webhook endpoint is server-to-server. It is transparent to the end user.
  • Endpoints must respond with an HTTP status code between 200 and 299 upon success. Any other code will be considered failed.
  • In the event that a request is unsuccessful, Leah's system will attempt to send the information up to two more times.

Event triggers

The events that can trigger a webhook are the following:

You can use a single endpoint that handles all events, or use different endpoints for different triggers.

User registered event

This event triggers a request when a user joins the organization. This linking occurs in two ways: When the user redeems a partnership code or when the user redeems a license key. In both cases, the request is sent with the information of the user who linked.

The POST request send when a user registers looks like this:

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"]}

Onboarding finished event

This event triggers a request when a user completes their profile, self-assessment, and learning goal information. The information sent includes all of this new information.

The POST request sent when a user finish the onboarding looks like this:

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"]}}

Placement test finished event

This event triggers a request when a user linked to the organization completes a placement test. The information sent includes the user's results on the test.

The POST request sent when a user finishes a placement test looks like this:

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"]}

Speaking test finished event

This event triggers a request when a user linked to the organization completes a speaking test. The information sent includes the user's results on the test.

The POST request sent when a user finishes a speaking test looks like this:

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"]}

Overall level event

This event triggers a request when a user completes a speaking test or a placement test and their overall level is calculated.

The POST request sent when the overall level is calculated looks like this:

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":[]}

Example

Below are instructions for building a simple server that is capable of handle the four types of webhook events that Leah can trigger.

The example will allow the following considerations to be implemented:

  • Handle authentication via "Bearer Secret Token" and "Basic HTTP". If authentication fails, respond with an HTTP status code 401 Unauthorized.
  • Handle any of the different types of event.
  • Validate the types of events received for each trigger, and if a valid type is not detected, respond with an HTTP status code 400 Bad request. If it is a successful event, it responds with an HTTP status code 200 Success.

Step 1: Create server

Save the following block of code in a file named 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 "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

The code above is for example purposes only and is not intended for use in a real environment. It contains bad practices and security issues such as including credentials directly in the code, comparing credentials without a constant time algorithm and does not perform any real operations. The only thing that is done is to print the request data in the stdout.

Step 2: Start server

The server is started on the port 8080 by running the following command:

node server.mjs

Step 3: Simulate incoming requests

Requests can be made to the server using curl. For example, to simulate the user registered event, using "Basic HTTP" authentication, you can run the following curl command:

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

The user registered event information is sent through the curl command. You can try any of the other events using the example requests discussed above.

Setting up webhooks

To set up one or more webhooks you must have an active partnership with Leah. This can be a B2B partnership, a B2B2C partnership or a Leads partnership.

If you have B2B2C partnership, Leah provided you with an identifier known as the partnerId. To add a webhook that receives events triggered by this partnership users, you must send an email to the agent or developer managing your partnership with the following information:

  1. Specify the partnerId.
  2. Endpoint URL.
  3. Endpoint authentication type and credentials.
  4. Event types that should trigger requests to the endpoint.