Passer au contenu principal

Webhooks - Documentation

Mis à jour il y a plus d’une semaine

Action(s) Tomorro pouvant déclencher l’automatisation :

  • Un contrat a été créé

  • Un contrat a été supprimé

  • Un contrat a été entièrement signé

  • Un statut de contract a été modifié

  • Un rappel automatique a été déclenché

Créer un webhook sur Tomorro

Sélectionner “Ajouter un webhook” :

4 choses à faire ici :

  1. Nommer votre webhook

  2. Coller l’URL copié préalablement crée pour recevoir l'évènement

  3. Sélectionner quel déclencheur activera l’automatisation (peut-être multiple)

  4. Créer le webhook

Nb : les webhooks sont personnels.

Ex :

Le webhook de création de contrat ne se déclenche que si je suis participant.

Le webhook de rappel automatique ne se déclenche que si le rappel m'est destiné.

Exemple de webhook

Voici un exemple d’un webhook complet (de création de contrat) afin de pouvoir exploiter la data :

{
"eventId": "b21213e3-8a9a-4e04-9bfc-c4e53f123xxx",
"webhookId": "2a76094c-1f2e-48c8-a47f-1add41234xxx",
"createdAt": "2025-03-25T14:55:16.280Z",
"eventType": "contractCreated",
"data": {
"contract": {
"id": "6954ad41-19f3-446f-b0db-fcc661234xxx",
"name": "TestWebhook",
"organizationId": "115e926b-9c61-4172-8609-02212344exxx",
"author": {
"id": "974d2a1c-eb63-4237-877e-37d12341axxx",
"user": {
"id": "fd61234d-a4e1-4103-8d31-59e351d2bxxx",
"username": "paul.lubet@tomorro.com",
"firstname": "Paul",
"lastname": "Lubet"
}
},
"referent": {
"id": "974d2a1c-eb63-4237-877e-37123451axxx",
"user": {
"id": "fd61234d-a4e1-4103-8d31-59e351d2bxxx",
"username": "paul.lubet@tomorro.com",
"firstname": "Paul",
"lastname": "Lubet"
}
},
"referentId": "974d2a1c-eb63-4237-877e-37123451axxx",
"supervisorId": null,
"externalCompany": {
"id": "01611234-7cdf-4785-966e-ee06b74ccxxx",
"name": "My external company"
},
"priorNoticeDuration": "undefined",
"priorNoticeValue": null,
"renewal": "no",
"signatureDate": null,
"status": "draft",
"createdAt": "2025-03-25T14:55:14.690Z",
"updatedAt": "2025-03-25T14:55:14.000Z",
"documentId": null,
"typeId": null,
"templateId": null,
"contractAttributes": [
{
"attributeDefinition": {
"name": "durationType"
},
"value": "PERMANENT"
},
{
"attributeDefinition": {
"name": "endAt"
},
"value": null
},
{
"attributeDefinition": {
"name": "externalPartyName"
},
"value": "My external company"
},
{
"attributeDefinition": {
"name": "initialDuration"
},
"value": null
},
{
"attributeDefinition": {
"name": "internalPartyName"
},
"value": "Paul Lubet"
},
{
"attributeDefinition": {
"name": "language"
},
"value": "fr"
},
{
"attributeDefinition": {
"name": "nextRenewalDate"
},
"value": null
},
{
"attributeDefinition": {
"name": "priorNotice"
},
"value": null
},
{
"attributeDefinition": {
"name": "renewalDuration"
},
"value": null
},
{
"attributeDefinition": {
"name": "renewalType"
},
"value": "no"
},
{
"attributeDefinition": {
"name": "signatureDate"
},
"value": null
},
{
"attributeDefinition": {
"name": "startAt"
},
"value": null
}
],
"attributes": {
"durationType": {
"value": "PERMANENT",
"name": "durationType",
"attributeDefinitionId": "durationType"
},
"endAt": {
"value": null,
"name": "endAt",
"attributeDefinitionId": "endAt"
},
"externalPartyName": {
"value": "My external company",
"name": "externalPartyName",
"attributeDefinitionId": "externalPartyName"
},
"initialDuration": {
"value": null,
"name": "initialDuration",
"attributeDefinitionId": "initialDuration"
},
"internalPartyName": {
"value": "Paul Lubet",
"name": "internalPartyName",
"attributeDefinitionId": "internalPartyName"
},
"language": {
"value": "fr",
"name": "language",
"attributeDefinitionId": "language"
},
"nextRenewalDate": {
"value": null,
"name": "nextRenewalDate",
"attributeDefinitionId": "nextRenewalDate"
},
"priorNotice": {
"value": null,
"name": "priorNotice",
"attributeDefinitionId": "priorNotice"
},
"renewalDuration": {
"value": null,
"name": "renewalDuration",
"attributeDefinitionId": "renewalDuration"
},
"renewalType": {
"value": "no",
"name": "renewalType",
"attributeDefinitionId": "renewalType"
},
"signatureDate": {
"value": null,
"name": "signatureDate",
"attributeDefinitionId": "signatureDate"
},
"startAt": {
"value": null,
"name": "startAt",
"attributeDefinitionId": "startAt"
}
}
}
}
}

Documentation technique

🔐 Security policy

Header signature

We sign the webhook events by including a signature in each event’s Leeway_Signature header.

Each webhook has a secret autogenerated and shared between us and the end user. Then, to sign the event, we compute an HMAC-SHA256 of the timestamp and the event’s body:

sha256 = HMAC_SHA256(timestamp + “.” + eventBody)

Finally, the Leeway_Signature header has the following format:

Leeway-Signature: t=1492774577, sha256=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

with t the timestamp and sha256 the previously computed hash.

Note that newlines have been added for clarity, but a real Leeway_Signature header is on a single line.

Signature check

When a client receive a webhook event, he should check that the signature is valid. Here is an example of how to do it:

import {createHmac, timingSageEqual} from 'crypto';  

function checkSignature(signature, secret, body) {
const timestamp = signature[0].split('=')[1];
const sig = Buffer.from(signature[1].split('=')[1], 'utf8');
const hmac = createHmac('sha256', secret);
const digest = Buffer.from(hmac.update(timestamp + '.' + JSON.stringify(body)).digest('hex'), 'utf8');
if (sig.length !== digest.length || !timingSafeEqual(digest, sig)) {
return false;
} else {
return true;
}
}

Note that it’s recommended to use a time constant-time string comparison to avoid timing attack vulnerability, that’s why we are using here timingSafeEqual instead of === (see https://en.wikipedia.org/wiki/Timing_attack)

Moreover, the client should check if the difference between the received timestamp et the current timestamp is within his tolerance (this is to avoid replay attack).

🔂 Retry policy

If the webhook respond with an other code than a 2xx or if the webhook take too much time (the timeout is set to 3s for the moment but the value may change), we consider the try as failed.

In this case, if the number of tries for this WebhookEvent is less than the max allowed (currently MAX_RETRIES = 3 ), we publish a new event in the queue with a delay of 5min (same here, value may change).

Otherwise, we don’t retry and disable the webhook.

Avez-vous trouvé la réponse à votre question ?