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.


With the Settings

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
  • project
  • activity
  • indicator
  • data_table_record
  • indicator_actual
  • indicator_combination_actual
  • indicator_target
  • indicator_combination_target
  • created
  • updated
  • deleted

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.


With the API

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);