Skip to main content

Webhooks

A webhook is an address on your server that Lettrove calls when something happens in your project. Your server hears about it at once, without asking over and over.

EventWhendata
design.savedA design was created or saved{ designId, userId, revision }
design.deletedA design was deleted{ designId, userId }
export.createdA design was exported, from the page or your server{ exportId, designId, userId, revision, source: 'editor' | 'server', format }
user.erasedA person's data was erased{ userId }
pingYou clicked Send a test{ message }

userId is your id for the person. Events name things by id only, never content: fetch a design with designs.get when you need it.

1. Add an endpoint to your server​

The body must reach your code exactly as it arrived — parse it only after checking its signature. With Express, read it as text:

webhooks.mjs
import express from 'express';
import { verifyWebhook } from '@lettrove/node';

const app = express();

app.post('/lettrove/webhooks', express.text({ type: 'application/json' }), async (req, res) => {
let event;
try {
event = await verifyWebhook(req.body, req.get('lettrove-signature'), process.env.LETTROVE_WEBHOOK_SECRET);
} catch {
return res.sendStatus(400); // not from Lettrove, changed on the way, or too old
}

switch (event.type) {
case 'design.saved':
// e.g. refresh your own copy: await lettrove.designs.get(event.data.designId, { user: { id: event.data.userId } })
console.log('saved', event.data.designId, 'revision', event.data.revision);
break;
case 'user.erased':
console.log('erased', event.data.userId);
break;
case 'ping':
console.log('ping:', event.data.message);
break;
}
res.sendStatus(200);
});

app.listen(4000, () => console.log('Listening on http://localhost:4000'));

Answer with any 2xx status within 10 seconds. Do slow work after answering (or in a queue).

2. Add the webhook in the dashboard​

  1. lettrove.com → Settings → Embed → your project → Webhooks.
  2. Paste your endpoint's address, e.g. https://api.acme.com/lettrove/webhooks, and tick the Events you want. Click Add webhook.
  3. Copy the signing secret (whsec_…). It is shown once. Put it in your server's environment as LETTROVE_WEBHOOK_SECRET.
  4. Click Send a test. Your endpoint receives a ping; Deliveries shows what was sent and how your server answered.

The address must be https:// and reachable on the public internet. A project has up to five webhooks.

Trying it on your own machine​

Lettrove cannot reach localhost. To receive real deliveries while you build, expose your local server with a tunnel such as cloudflared tunnel --url http://localhost:4000 or ngrok http 4000, and add the https://… address it prints.

What a delivery looks like​

POST /lettrove/webhooks HTTP/1.1
Content-Type: application/json
User-Agent: Lettrove-Webhooks/1
Lettrove-Signature: t=1791338400,v1=5f2b8c1e9a…

{
"id": "01J9ZV6K2D5S8N1P3Q7R4T0W9X",
"type": "design.saved",
"createdAt": "2026-10-07T16:00:00.000Z",
"projectId": "01J9X…",
"data": { "designId": "01J9ZQ3W8D2K7M5T1V4XG6HB0R", "userId": "u_123", "revision": 12 }
}

Verifying the signature yourself​

verifyWebhook does this for you. In another language: the header is t=<unix seconds>,v1=<hex>, and v1 is the HMAC-SHA256 of "<t>.<the raw body>" with your signing secret as the key. Compare in constant time, and refuse a t more than five minutes from now, so a recorded delivery cannot be replayed.

verify_webhook.py
import hashlib, hmac, os, time

def verify_webhook(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(item.split("=", 1) for item in header.split(","))
timestamp, signature = parts.get("t", ""), parts.get("v1", "")
if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > 300:
return False
expected = hmac.new(secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)

Retries, order and duplicates​

  • A delivery that does not get a 2xx in time is retried with growing waits — 30 seconds, then 1, 2, 4 minutes and so on — eight tries in all. Deliveries in the dashboard shows each attempt and your server's answer, for 30 days.
  • A delivery may arrive more than once. Its id stays the same: skip an id you have already handled.
  • Deliveries can arrive out of order. Use revision (it only goes up) to ignore an older design.saved after a newer one.
  • While a person edits, their design is saved often. Saves of one design that are still waiting to be delivered are merged into one: you receive its newest revision.

Rotating the secret​

New secret (beside the webhook in the dashboard) gives a new signing secret, shown once, and the old one stops at once. Update your server's environment straight away. Remove deletes the webhook; the switch beside it pauses deliveries without removing it.