Webhooks in LogAlto
Webhooks
LogAlto offers a dynamic feature where various changes within the platform can be monitored using webhooks. A webhook is essentially a URL that you control. LogAlto is capable of dispatching notifications about specific events of your interest directly to this URL.
For effective management of these webhooks, you have two convenient options: utilizing the API or navigating to the Settings section within LogAlto.
To explore the full range of events that you can track with webhooks, please refer to our API documentation.
Create a webhook
As an example, we will create a webhook to listen to project events.
The webhook URL used in the following example is a hypothetical one. Be aware that if you don't yet have an endpoint for your webhooks, you can easily use a third-party service like https://webhook.site/ to test LogAlto webhooks.
First, you'll need to create a URL on your server to which LogAlto can send events. Let's say this URL is https://webhook.acme.com/logalto
In Settings, only if you have the rights to manage webhooks, you will find a "Webhooks" link under the "General" section.
You can then create a webhook with the following values:
- Endpoint URL: https://webhook.acme.com/logalto
- Description: Webhook to monitor projects
- LogAlto events: Choose from the dropdown list of options
Type | Event |
|
|
When any of the selected events are triggered, LogAlto will send a POST request to the endpoint URL with the event data in the request body.
If you prefer to use the API, here is the API call to create this webhook:
REST
POST https://acme.logalto.com/api/webhooks Content-Type: application/json { "data": { "endpoint_url": "https://webhook.acme.com/logalto", "listened_events": ["project.created", "project.updated", "project.deleted"], "description": "Webhook to monitor projects" } }
CURL
curl -X POST \ https://acme.logalto.com/api/webhooks \ -H 'Content-Type: application/json' \ -d '{ "data": { "endpoint_url": "https://webhook.acme.com/logalto", "listened_events": ["project.created", "project.updated", "project.deleted"], "description": "Webhook to monitor projects" } }'
Webhook payload
When a project is created, updated, or deleted, the URL https://webhook.acme.com/logalto
will be called like so by LogAlto:
REST
POST https://webhook.acme.com/logalto Content-Type: application/json User-Agent: LogAlto/v27.15.1-0-ged673955 X-Webhook-Signature: eyJhbGciOiJSUzI1NiIsImtpZCI6IkhpMmhJV0hCMFp6ZktwcVQ4SGNXRkxHVk9fRy1FaDMwaVNEeWFob0YxencifQ.eyJqd2tzLWVuZHBvaW50IjoiXC9qd2tzXC93ZWJob29rcyIsInBheWxvYWQtY2hlY2tzdW0iOiJjZGE5MWQ1ZmQyNzEwMWZiNzg5NTFiOTgyZTg4NzcwMmZjZTE5N2EzYTQyODZkNWUyMWVlZWMxYjBlNzdjMzAyIiwicGF5bG9hZC1jaGVja3N1bS1hbGciOiJzaGEyNTYifQ.Bt3vyqFk__LJTQ9icjJHhb5TJO0mFGejGMuC1pwXsnPRW_UMv9Zc57T7SfP_8cqulZTqyWcROmD6you49j7QC_cL34828-1TWYRnaS9yRgXPPxOFsV5pdZYSUjs5vx7dreT24KGONU13HEiP7oFtI_Jz092lSmmQjn74rmyk7kZ23nJrwUqob0JDCgkzptqT_Dvt6dr6e3KIzDuoyEEltlz8ThUwnjZySIMVE0FBQYf380GG9QQ5IT40zls8XC_XBtkyl31NnFyR2FshY00dFZ-tmIHppUiH4mTGzwvuLCxF6yTAMtF3_20EME1N3p-6Z_cAG_OUizsp2wyax6aGOA { "event": { "id": "b14cb4b0-54fc-4b31-8051-72c9dd55cf61", "metadata": { "id": 34, "code": "PROJECT-1", "name": "Project one", "status": 34, "end_date": "2035-12-31", "start_date": "2035-01-01", "description": "Sample project", "currency_currency": "EUR", "org_unit_org-unit-role-655041cd3f029": 54 }, "created_at": "2023-11-11 22:09:01", "fk_user": "182", "event_type": "CREATED", "project_id": "34", "subject_id": "34", "activity_id": null, "indicator_id": null, "subject_type": "project", "data_table_id": null, "data_table_record_id": null }, "webhook": { "id": "a71ad00e-3384-4504-aa99-5d64a6b2fdd2", "endpoint_url": "https://webhook.acme.com/logalto", "listened_events": [ "project.created", "project.updated", "project.deleted" ] }, "delivery": { "id": "3997dd6d-4659-448a-82fd-dda00ab9b9c9", "attempt_id": "00b927ec-9a83-44b0-b357-3d9f23d35687", "attempted_at": "2023-11-11 22:16:51" } }
Let's go through the request above.
Headers
The request is always a POST, and it has a signature header that you should verify to make sure it is LogAlto making the call. There is a section below on how to verify the signature.
Event object
The event object contains useful data such as the event ID, the event_type, and the subject_type, as well as metadata about the resource, in this case, a project. This is the same object returned by the API's /events
endpoint.
Webhook object
This is the webhook that triggered the call.
Delivery object
This is the delivery information for sending a specific event to a specific webhook. It has an ID, current attempt ID and date. If the initial attempt fails, it will be attempted again, with the same delivery ID, and a different attempt ID and date. The rules for retrying are described below.
Webhook response
LogAlto expects a status code 2XX response from your webhook's endpoint URL, for the delivery to be successful. If the response is not 2XX, if it times out, or if it's not reachable, LogAlto will retry the delivery. See below for the retry strategy.
Webhook timeout
LogAlto will wait 10 seconds for a response from your webhook's endpoint URL. If LogAlto does not get a response within 10 seconds, it is interpreted as a failure, and the delivery will be retried later.
Duplicate delivery
Due to delivery retries, you might receive the same event more than once. Rely on the event ID to ensure you handle it once. Do not rely on the delivery ID, as it is used to group attempts, not to ensure uniqueness.
Out-of-Order Delivery
You might not receive events in chronological order due to network delays or webhook failures. Rely on the event.created_at
if you need to make sure you did not already handle a more recent event on the same resource.
Delivery retry strategy
If the initial delivery fails, LogAlto will retry up to 5 times, at exponential intervals.
Retry | Time |
---|---|
1 | 2 minutes after the failure |
2 | 6 minutes after the previous retry |
3 | 30 minutes after the previous retry |
4 | 1 hour after the previous retry |
5 | 5 hours after the previous retry |
Verifying the signature
The webhook call has a header named X-Webhook-Signature
. The value of this header is a signed JWT that you should verify. It guarantees that the request comes from LogAlto and that you can trust it.
Let's use the token in the previous sample.
eyJhbGciOiJSUzI1NiIsImtpZCI6IkhpMmhJV0hCMFp6ZktwcVQ4SGNXRkxHVk9fRy1FaDMwaVNEeWFob0YxencifQ.eyJqd2tzLWVuZHBvaW50IjoiXC9qd2tzXC93ZWJob29rcyIsInBheWxvYWQtY2hlY2tzdW0iOiJjZGE5MWQ1ZmQyNzEwMWZiNzg5NTFiOTgyZTg4NzcwMmZjZTE5N2EzYTQyODZkNWUyMWVlZWMxYjBlNzdjMzAyIiwicGF5bG9hZC1jaGVja3N1bS1hbGciOiJzaGEyNTYifQ.Bt3vyqFk__LJTQ9icjJHhb5TJO0mFGejGMuC1pwXsnPRW_UMv9Zc57T7SfP_8cqulZTqyWcROmD6you49j7QC_cL34828-1TWYRnaS9yRgXPPxOFsV5pdZYSUjs5vx7dreT24KGONU13HEiP7oFtI_Jz092lSmmQjn74rmyk7kZ23nJrwUqob0JDCgkzptqT_Dvt6dr6e3KIzDuoyEEltlz8ThUwnjZySIMVE0FBQYf380GG9QQ5IT40zls8XC_XBtkyl31NnFyR2FshY00dFZ-tmIHppUiH4mTGzwvuLCxF6yTAMtF3_20EME1N3p-6Z_cAG_OUizsp2wyax6aGOA
You can decode it, if you like, with an online tool like https://jwt.io/
HEADER { "alg": "RS256", "kid": "Hi2hIWHB0ZzfKpqT8HcWFLGVO_G-Eh30iSDyahoF1zw" } PAYLOAD:DATA { "jwks-endpoint": "/jwks/webhooks", "payload-checksum": "cda91d5fd27101fb78951b982e887702fce197a3a4286d5e21eeec1b0e77c302", "payload-checksum-alg": "sha256" }
Now let's verify the signature with PHP (as an example). The first step is to fetch the public key from the jwks-endpoint, and to verify the signature. The second step is to validate the payload checksum. The jwks-endpoint is always relative to your LogAlto API.
Install a JWT library
composer requires web-token/jwt-framework
Verify the signature
In the snippet below, replace https://acme.logalto.com/api
with your LogAlto API URL.
<?php require_once __DIR__ . '/vendor/autoload.php'; $logalto_api = "https://acme.logalto.com/api"; // parse JWT $jwt = (new \Jose\Component\Signature\Serializer\CompactSerializer()) ->unserialize($_SERVER['HTTP_X_WEBHOOK_SIGNATURE']); $payload = json_decode($jwt->getPayload(), true); $protected_header = $jwt->getSignature(0)->getProtectedHeader(); // fetch public key from API endpoint in claim $public_keyset = \Jose\Component\Core\JWKSet::createFromJson( file_get_contents($logalto_api . $payload['jwks-endpoint']) ); //Verify the signature with the public key $algorithm = new \Jose\Component\Signature\Algorithm\RS256(); $algorithm_manager = new \Jose\Component\Core\AlgorithmManager([$algorithm]); $verifier = new \Jose\Component\Signature\JWSVerifier($algorithm_manager); if (true !== $verifier->verifyWithKey($jwt, $public_keyset->get($protected_header['kid']), 0)) { http_response_code(401); exit(0); } // verify request body checksum $expected_checksum = $payload['payload-checksum']; $actual_checksum = hash($payload['payload-checksum-alg'], file_get_contents('php://input')); if ($expected_checksum !== $actual_checksum) { http_response_code(401); exit(0); } http_response_code(204);