# Webhooks

With webhooks, you can trigger http-request to your own backend based on events that occur in Omnidesk. Our implementation of webhooks conforms to the [Standard Webhooks](https://www.standardwebhooks.com/) specification:

* Webhook-endpoints consist of an `HTTPS`-URL and a list of subscribed events.
* A `POST`-request is send to this URL whenever one of the subscribed events occur within Omnidesk.
* Failed webhook-requests are retried up to 5 times on an [exponential backoff schedule](https://en.wikipedia.org/wiki/Exponential_backoff) with base 2.
* Each webhook-request has a signature so that webhook-consumers can verify if it was send by a legitimate source.

## Creating Webhook Endpoints

First, you will need to host some HTTP-server that will function as the handler of your. The minimal requirements for a webhook endpoint are as follows:

1. It MUST be **publicly** accessible (so *without* authentication) by our backend servers. Also, there MUST be a valid `HTTPS`-URL that points to your endpoint.
2. It MUST respond to [`OPTIONS`-requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods/OPTIONS) with a `200` or `204` status code. This response also MUST have an [`Allow`-header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Allow) that contains `POST`. We use this on our backend to verify if your endpoint is (still) safe use.
3. It MUST respond to valid [`POST`-requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods/POST) within 1 second with a `2xx`-response (when the webhook-message is accepted) OR a `5xx`-response (when some internal error occurs while parsing the message). You should give an early [`202`-response](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/202) when you expect that some tasks in the webhook endpoint can take longer than 1 second.

You can create a new webhook endpoint in the Omnidesk UI after you started your webhook consumer:

* Go to *Admin Settings > Webhooks* and click on *Create*.
* Give the URL of your endpoint.
* Choose the events to which you endpoint should subscribe.
* Click on *Create*.

> Our backend will check if your webhook-consumer satisfies (1) and (2) of the minimal requirements when creating a new endpoint. Please check your backend server if you get an error when creating a new webhook-endpoint.

### Minimal example (NodeJS)

This example is a minimal implementation of a valid webhook endpoint handler written for NodeJS. The minimal requirements are satisfied by:

1. Handling `OPTIONS`-requests that is expected by our backend.
2. Sending a `202`-response to `POST`-requests as soon as it has received the body. Note that the response is send *before* it starts any long-running tasks.

```javascript
import * as http from "node:http";

http.createServer(async function (req, res) {
    // 1. Handle only OPTIONS and POST http-requests.
    if (req.method === "OPTIONS") {
        res.writeHead(204, {
            allow: "OPTIONS, POST",
        }).end();
        return;
    } else if (req.method !== "POST") {
        res.writeHead(405, {
            allow: "OPTIONS, POST",
        }).end();
        return;
    }

    // Retrieve the request contents (also referred to as payload).
    const payload = await streamToString(req);
    // 2. Send a success response.
    res.writeHead(202).end();

    // Parse the message payload.
    const {
        type, // The event type.
        timestamp, // Moment the event occurred (ISO8601 formatted).
        data, // Information about the event itself.
    } = JSON.parse(payload);

    // -- PUT YOUR (Potentially long-running) EVENT HANDLING TASKS HERE --
    console.log(`Handling [${type}] event...`, data);
}).listen(8080);

function streamToString(req) {
    return new Promise((resolve, reject) => {
        const bufs = [];
        req.on("data", (d) => bufs.push(d));
        req.on("end", () => resolve(Buffer.concat(bufs).toString("utf-8")));
        req.on("error", (err) => reject(err));
    });
}
```

> **IMPORTANT!** Only use this example during development, debugging or for exploring the possibilities of our Websockets feature. It is NOT safe to use this example in production environments, because it does not check if the webhook-request comes from a legitimate source!

### Example with signature verification (NodeJS)

Below, you can find a minimal example implementation of a webhook handler written for NodeJS. It uses the asymmetric (ed25519) signature to verify that the incoming webhook-requests were send from our backend.

1. Handles the `OPTIONS` request to allow our backend to verify that your webhook-endpoint is safe to use.
2. Checks if the request has all required headers to verify the incoming webhook-request.
3. Checks if the `webhook-timestamp` is close enough to the timestamp of your backend (to prevent replay-attacks).
4. Verifies that the `webhook-signature` matches the contents of this of the webhook-request.

```javascript
import * as crypto from "node:crypto";
import * as http from "node:http";

const PUBLIC_KEY_PREFIX = "whpk_";
const ASYMMETRIC_SIGNATURE_PREFIX = "v1a,";

const TOLERANCE_IN_SECONDS = 5 * 60; // 5 minutes
const PUBLIC_KEY = parseWebhookPublicKey(
    // Set this to the public key of your webhook endpoint!
    "whpk_8akOU8gfL7feQx/gZk1FZJkEwNwIXYOlM6Hjcpc/jBA=",
);

http.createServer(async function (req, res) {
    // 1. Handle the HTTP-method:
    if (req.method === "OPTIONS") {
        res.writeHead(204, {
            allow: "OPTIONS, POST",
        }).end();
        return;
    } else if (req.method !== "POST") {
        res.writeHead(405, {
            allow: "OPTIONS, POST",
        }).end();
        return;
    }

    // 2. Retrieve the headers that are required for validation.
    const msgId = req.headers["webhook-id"];
    const msgTimestamp = req.headers["webhook-timestamp"];
    const msgSignature = req.headers["webhook-signature"];
    if (!msgId || !msgTimestamp || !msgSignature) {
        console.error("Missing required headers");
        res.writeHead(400).end();
        return;
    }

    // 3. Validate the timestamp.
    if (!verifyTimestamp(msgTimestamp)) {
        console.error("Invalid msg timestamp");
        res.writeHead(400).end();
        return;
    }

    // Retrieve the request contents (also referred to as payload).
    const payload = await streamToString(req);

    // 4. Verify the signature.
    if (!verifySignature(msgId, msgTimestamp, payload, msgSignature)) {
        console.error("Invalid msg signature");
        res.writeHead(400).end();
        return;
    }

    // The message is now verified! Send the success response.
    res.writeHead(202).end();

    // Parse the message payload.
    const {
        type, // The event type.
        timestamp, // Moment the event occurred (ISO8601 formatted).
        data, // Information about the event itself.
    } = JSON.parse(payload);

    // -- PUT YOUR EVENT HANDLING TASKS HERE --
    console.log(`Handling [${type}] event...`, data);
}).listen(8080);

function verifyTimestamp(timestampHeader) {
    const now = Math.floor(Date.now() / 1000);
    const timestamp = parseInt(timestampHeader, 10);
    return (
        !Number.isNaN(timestamp) &&
        Math.abs(now - timestamp) <= TOLERANCE_IN_SECONDS
    );
}

function verifySignature(msgId, timestamp, payload, signatureHeader) {
    if (typeof signatureHeader !== "string") return false;

    const signature = signatureHeader
        .split(/\s+/g)
        .find((sig) => sig.startsWith(ASYMMETRIC_SIGNATURE_PREFIX));
    if (!signature) return false;

    const parsedSignature = Buffer.from(
        signature.substring(ASYMMETRIC_SIGNATURE_PREFIX.length),
        "base64",
    );

    const encoder = new TextEncoder();
    console.log(signatureHeader, parsedSignature.length);
    const toVerify = encoder.encode(`${msgId}.${timestamp}.${payload}`);
    return crypto.verify(null, toVerify, PUBLIC_KEY, parsedSignature);
}

function parseWebhookPublicKey(key) {
    if (!key.startsWith(PUBLIC_KEY_PREFIX))
        throw new Error("Invalid public key");
    const rawKey = Buffer.from(
        key.substring(PUBLIC_KEY_PREFIX.length),
        "base64",
    );
    if (rawKey.length !== 32) throw new Error("Invalid public key");
    const ans1Header = Buffer.from([
        0x30, 42,                 // SEQUENCE, len 42
            0x30, 5,                  // SEQUENCE, len 5
                0x06, 0x03, 0x2b, 101, 112 // ALGORITHM OID 1.3.101.112 (Ed25519)
            0x03, 33, 0               // BIT STRING, len 32, 0 unused bits
    ]);
    return crypto.createPublicKey({
        key: Buffer.concat([ans1Header, rawKey]),
        format: "der",
        type: "spki",
    });
}

function streamToString(req) {
    return new Promise((resolve, reject) => {
        const bufs = [];
        req.on("data", (d) => bufs.push(d));
        req.on("end", () => resolve(Buffer.concat(bufs).toString("utf-8")));
        req.on("error", (err) => reject(err));
    });
```

You can find an example on how to validate webhook requests with a shared secret (HMAC-SHA256) in [the standard webhooks example repository](https://github.com/standard-webhooks/standard-webhooks/blob/main/libraries/javascript/src/index.ts).


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.omnidesk.io/omnidesk/webhooks.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
