litehook Docs Sign in

Building with Litehook

A prompt for AI assistants, and examples to copy.

New sources use signed requests. Signed examples post to /api/v1/sources/SOURCE_ID/events and sign the exact body with the source's whsec_ secret from LITEHOOK_SECRET. Replace SOURCE_ID and keep the secret in your platform's secret settings. Each example sends JSON and expects 202 Accepted.

Examples marked URL token are for sources with Signed requests turned off.

Build with AI

Fill in your stack and the events you want, then give this prompt to your AI assistant.

Add Litehook notifications to this project.

Project language and toolkit: <describe your stack>
Notify me when: <describe the events you care about>

Litehook API (Standard Webhooks signing):
- POST https://litehook.app/api/v1/sources/SOURCE_ID/events
- Header: Content-Type: application/json
- JSON body: {"title": string (max 120), "message": string (optional, max 2000),
  "level": "info" | "success" | "warning" | "error", "group": string (optional, max 80)}
- The signing secret is read from the LITEHOOK_SECRET environment variable and looks like whsec_<base64>.
  Never hardcode or commit it.
- Header: webhook-id: a unique ID per event, such as "msg_" plus a random UUID. Reuse it when retrying the same event.
- Header: webhook-timestamp: the current Unix time in seconds.
- Header: webhook-signature: "v1," + base64(HMAC-SHA256(key, webhook-id + "." + webhook-timestamp + "." + body)),
  where key is the base64-decoded part of the secret after "whsec_" and body is the exact bytes sent.
  A Standard Webhooks library can produce these headers instead.
- 202: accepted. 401: bad signature, clock over 5 min off, or revoked secret. 409: webhook-id reused
  with a different body. 429: rate limited; honour Retry-After.

Use the project's existing HTTP client where possible. Write one small
notify(title, message, level) helper with a 5 second timeout.
If Litehook is unreachable, log a warning and carry on; never let a notification failure break the app.
Keep titles short and scannable; put detail in the message.
Show where to configure the source ID and secret for this project's deployment platform.

Assistants that browse the web can read litehook.app/llms.txt, a plain-text API summary.

Give an assistant your source ID, never the secret. Put the secret in your environment or your platform's secret store yourself.

curl and shell

Needs bash, openssl and base64.

# Read the secret without putting it in shell history.
read -r -s -p 'Signing secret: ' LITEHOOK_SECRET; echo
BODY='{"title":"Backup finished","message":"All files copied","level":"success"}'
ID="msg_$(date +%s%N)"; TS=$(date +%s)
KEY=$(printf '%s' "${LITEHOOK_SECRET#whsec_}" | base64 -d | od -An -tx1 | tr -d ' \n')
SIG=$(printf '%s' "$ID.$TS.$BODY" | openssl dgst -sha256 -mac HMAC -macopt "hexkey:$KEY" -binary | base64)
curl --max-time 5 -fsS -X POST 'https://litehook.app/api/v1/sources/SOURCE_ID/events' \
  -H 'Content-Type: application/json' \
  -H "webhook-id: $ID" -H "webhook-timestamp: $TS" -H "webhook-signature: v1,$SIG" \
  -d "$BODY"

URL token. Notify only when a cron job fails. Set LITEHOOK_URL to the source's /hook/SOURCE_ID/SOURCE_SECRET address and keep it secret.

0 3 * * * /usr/local/bin/backup.sh || curl -fsS -X POST "$LITEHOOK_URL" \
  -H 'Content-Type: application/json' -d '{"title":"Nightly backup failed","level":"error"}'

Python

Uses only the standard library.

import base64
import hashlib
import hmac
import json
import logging
import os
import time
import urllib.request
import uuid

def notify(title, message="", level="info", group=None):
    body = {"title": title, "message": message, "level": level}
    if group:
        body["group"] = group
    try:
        data = json.dumps(body).encode()
        msg_id, timestamp = f"msg_{uuid.uuid4().hex}", str(int(time.time()))
        key = base64.b64decode(os.environ["LITEHOOK_SECRET"].removeprefix("whsec_"))
        digest = hmac.new(key, f"{msg_id}.{timestamp}.".encode() + data, hashlib.sha256).digest()
        request = urllib.request.Request(
            "https://litehook.app/api/v1/sources/SOURCE_ID/events",
            data=data,
            headers={"Content-Type": "application/json",
                     "webhook-id": msg_id,
                     "webhook-timestamp": timestamp,
                     "webhook-signature": "v1," + base64.b64encode(digest).decode()},
            method="POST")
        with urllib.request.urlopen(request, timeout=5) as response:
            if response.status != 202:
                logging.warning("Litehook returned %s", response.status)
    except Exception as error:
        logging.warning("Litehook notification failed: %s", type(error).__name__)

notify("Import complete", "1,204 rows", level="success")

Node.js

Uses the built-in crypto module and fetch (Node.js 18 or later).

import { createHmac, randomUUID } from 'node:crypto';

async function notify(title, message = '', level = 'info') {
  const body = JSON.stringify({ title, message, level });
  const id = 'msg_' + randomUUID();
  const timestamp = String(Math.floor(Date.now() / 1000));
  try {
    const key = Buffer.from(process.env.LITEHOOK_SECRET.replace(/^whsec_/, ''), 'base64');
    const signature = createHmac('sha256', key).update(`${id}.${timestamp}.${body}`).digest('base64');
    const response = await fetch('https://litehook.app/api/v1/sources/SOURCE_ID/events', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'webhook-id': id,
        'webhook-timestamp': timestamp,
        'webhook-signature': 'v1,' + signature
      },
      body,
      signal: AbortSignal.timeout(5000)
    });
    if (response.status !== 202) console.warn('Litehook returned', response.status);
  } catch (error) {
    console.warn('Litehook notification failed');
  }
}

await notify('New signup', 'From the pricing page', 'success');

Go

Uses only the standard library.

package main

import (
    "bytes"
    "context"
    "crypto/hmac"
    "crypto/sha256"
    "encoding/base64"
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "os"
    "strconv"
    "strings"
    "time"
)

func main() {
    body, err := json.Marshal(map[string]string{
        "title": "Backup complete", "level": "success",
    })
    if err != nil { log.Print(err); return }
    key, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(os.Getenv("LITEHOOK_SECRET"), "whsec_"))
    if err != nil { log.Print("LITEHOOK_SECRET is not a signing secret"); return }
    id := fmt.Sprintf("msg_%d", time.Now().UnixNano())
    timestamp := strconv.FormatInt(time.Now().Unix(), 10)
    mac := hmac.New(sha256.New, key)
    mac.Write([]byte(id + "." + timestamp + "."))
    mac.Write(body)
    signature := "v1," + base64.StdEncoding.EncodeToString(mac.Sum(nil))

    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    req, err := http.NewRequestWithContext(ctx, "POST",
        "https://litehook.app/api/v1/sources/SOURCE_ID/events", bytes.NewReader(body))
    if err != nil { log.Print(err); return }
    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("webhook-id", id)
    req.Header.Set("webhook-timestamp", timestamp)
    req.Header.Set("webhook-signature", signature)
    response, err := http.DefaultClient.Do(req)
    if err != nil { log.Print(err); return }
    defer response.Body.Close()
    if response.StatusCode != http.StatusAccepted {
        log.Printf("Litehook returned %d", response.StatusCode)
    }
}

GitHub Actions

Store the source's signing secret as a repository secret named LITEHOOK_SECRET, replace SOURCE_ID, then add this final step to a job. The status goes through an environment variable, so it can't change the command. See GitHub's status expressions.

- name: Notify Litehook
  if: always()
  env:
    LITEHOOK_SECRET: ${{ secrets.LITEHOOK_SECRET }}
    JOB_STATUS: ${{ job.status }}
  run: |
    case "$JOB_STATUS" in
      success) level=success ;;
      *) level=error ;;
    esac
    BODY="{\"title\":\"Build $JOB_STATUS\",\"level\":\"$level\"}"
    ID="msg_$(date +%s%N)"; TS=$(date +%s)
    KEY=$(printf '%s' "${LITEHOOK_SECRET#whsec_}" | base64 -d | od -An -tx1 | tr -d ' \n')
    SIG=$(printf '%s' "$ID.$TS.$BODY" | openssl dgst -sha256 -mac HMAC -macopt "hexkey:$KEY" -binary | base64)
    curl --max-time 5 -fsS -X POST 'https://litehook.app/api/v1/sources/SOURCE_ID/events' \
      -H 'Content-Type: application/json' \
      -H "webhook-id: $ID" -H "webhook-timestamp: $TS" -H "webhook-signature: v1,$SIG" \
      -d "$BODY" \
      || echo 'Litehook notification failed'

Vercel Functions

Add LITEHOOK_SOURCE_ID and LITEHOOK_SECRET in your Vercel project's Environment Variables. Call this helper only from server-side code. Vercel applies new variables to its Node.js Functions on the next deployment.

// lib/litehook.ts — server-side code
import { createHmac, randomUUID } from 'node:crypto';

export async function notifyLitehook(title: string) {
  const sourceId = process.env.LITEHOOK_SOURCE_ID;
  const secret = process.env.LITEHOOK_SECRET;
  if (!sourceId || !secret) {
    console.warn('Litehook is not configured');
    return;
  }
  const body = JSON.stringify({ title, level: 'info' });
  const id = 'msg_' + randomUUID();
  const timestamp = String(Math.floor(Date.now() / 1000));
  const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
  const signature = createHmac('sha256', key).update(`${id}.${timestamp}.${body}`).digest('base64');
  try {
    const response = await fetch(`https://litehook.app/api/v1/sources/${sourceId}/events`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'webhook-id': id,
        'webhook-timestamp': timestamp,
        'webhook-signature': 'v1,' + signature
      },
      body,
      signal: AbortSignal.timeout(5000)
    });
    if (response.status !== 202) console.warn('Litehook returned', response.status);
  } catch (error) {
    console.warn('Litehook notification failed');
  }
}

For Vercel deployment status events, its account webhooks send Vercel's own payload format. Send them to a URL token source address, or to a small relay that verifies Vercel's signature and sends a Litehook event.

AWS Lambda

In your Lambda function's configuration, set LITEHOOK_SOURCE_ID and LITEHOOK_SECRET. Uses only the standard library. For production, consider Secrets Manager over plain environment variables.

import base64
import hashlib
import hmac
import json
import os
import time
import urllib.request
import uuid

def lambda_handler(event, context):
    # Do your normal work here.
    try:
        body = json.dumps({"title": "Job complete", "level": "success"}).encode()
        msg_id, timestamp = f"msg_{uuid.uuid4().hex}", str(int(time.time()))
        key = base64.b64decode(os.environ["LITEHOOK_SECRET"].removeprefix("whsec_"))
        digest = hmac.new(key, f"{msg_id}.{timestamp}.".encode() + body, hashlib.sha256).digest()
        request = urllib.request.Request(
            "https://litehook.app/api/v1/sources/" + os.environ["LITEHOOK_SOURCE_ID"] + "/events",
            data=body,
            headers={
                "Content-Type": "application/json",
                "webhook-id": msg_id,
                "webhook-timestamp": timestamp,
                "webhook-signature": "v1," + base64.b64encode(digest).decode(),
            },
            method="POST",
        )
        with urllib.request.urlopen(request, timeout=5) as response:
            if response.status != 202:
                print("Litehook returned", response.status)
    except Exception as error:
        print("Litehook notification failed:", type(error).__name__)
    return {"ok": True}

GitLab CI

Add the source's signing secret as a masked CI/CD variable named LITEHOOK_SECRET, replace SOURCE_ID, and add this after_script to the job you want to watch. The runner needs curl, openssl and base64. See GitLab's after_script guide.

build:
  script:
    - ./build.sh
  after_script:
    - |
      case "$CI_JOB_STATUS" in
        success) level=success ;;
        *) level=error ;;
      esac
      BODY="{\"title\":\"Build $CI_JOB_STATUS\",\"level\":\"$level\"}"
      ID="msg_$(date +%s%N)"; TS=$(date +%s)
      KEY=$(printf '%s' "${LITEHOOK_SECRET#whsec_}" | base64 -d | od -An -tx1 | tr -d ' \n')
      SIG=$(printf '%s' "$ID.$TS.$BODY" | openssl dgst -sha256 -mac HMAC -macopt "hexkey:$KEY" -binary | base64)
      curl --max-time 5 -fsS -X POST \
        'https://litehook.app/api/v1/sources/SOURCE_ID/events' \
        -H 'Content-Type: application/json' \
        -H "webhook-id: $ID" -H "webhook-timestamp: $TS" -H "webhook-signature: v1,$SIG" \
        -d "$BODY" \
        || echo 'Litehook notification failed'

Automation builders

URL token. Most automation builders can't sign requests. Send a JSON POST from an HTTP request step, such as n8n's HTTP Request node. Put the token in the tool's credential setting if it has one.

Method: POST
URL: https://litehook.app/api/ingest/SOURCE_ID
Header: Authorization: Bearer SOURCE_SECRET
Header: Content-Type: application/json
JSON body: {"title":"Workflow complete","level":"success"}

If a tool takes only a URL, use the source's /hook/SOURCE_ID/SOURCE_SECRET address. Keep it secret.

Coolify

URL token. Paste the source address into Coolify's webhook notification setting. Coolify's event, message and success fields become the title, message and level.

Home Assistant

URL token. Add a REST command to configuration.yaml, with the source address in secrets.yaml:

rest_command:
  litehook:
    url: !secret litehook_url
    method: POST
    content_type: "application/json"
    payload: '{"title": {{ title | tojson }}, "message": {{ message | tojson }}, "level": {{ level | default(''info'') | tojson }}}'

Then call rest_command.litehook from an automation with title and message.