Webhooks
Webhooks are how Comvi tells your systems something happened. When a matching event fires in a project, Comvi sends a signed HTTP POST to the URL you registered. Use them to trigger CI builds, invalidate caches, post to Slack, or wire Comvi into any internal pipeline.
When to use them
Section titled “When to use them”- You want a CI build to run when content changes
- You want to mirror content changes into another system (analytics, search index, legal archive)
- You want a team channel alert on specific activity
- You want to drive workflow automation (“translator finished a namespace → notify reviewer”)
If your need is “load fresh translations in my app”, a webhook is not the right tool — apps should read the CDN directly.
Event catalog
Section titled “Event catalog”| Event | Fires when |
|---|---|
translation.created | A target-language value is first set on a key |
translation.updated | An existing target value is edited |
translation.batch_updated | A bulk operation updated many values at once (MT, TM apply, bulk status change) |
key.created | A new key is added |
key.updated | A key’s metadata or source value changes |
key.deleted | A key is removed |
namespace.created | A new namespace is added |
namespace.updated | A namespace is renamed or re-described |
namespace.deleted | A namespace is removed |
import.completed | An import job finishes |
export.completed | An export is generated |
comment.created | A comment is posted on a translation value |
project.locale_added | A language is added to the project |
project.locale_removed | A language is removed from the project |
webhook.test | Fired when you click Send Test |
webhook.disabled | Comvi auto-disabled this webhook after too many failures |
Setting up a webhook
Section titled “Setting up a webhook”-
Open webhook settings
Project Settings → Webhooks. Click Create Webhook.
-
Fill in the basics
- Name — human-readable label for the dashboard list
- URL — your public HTTP or HTTPS endpoint. HTTPS is strongly recommended for production.
- Events — tick the subset you care about (at least one required)
-
Copy the secret
Comvi generates a signing secret (format
whsec_<64 hex>). It is shown once — copy it into your server’s config now. You can regenerate later if you lose it, which invalidates the old one. -
Test delivery
Click Send Test. Comvi dispatches a
webhook.testpayload synchronously and shows the remote status code, response body, and duration. Iterate here until you get a 2xx.
Payload shape
Section titled “Payload shape”Every delivery is a JSON body with the same envelope:
{ "event": "translation.updated", "deliveryId": "del_a1b2c3d4e5f6…", "timestamp": "2026-04-15T14:30:00.000Z", "projectId": 42, "data": { "translation": { "id": 456, "keyId": 123, "key": "nav.home", "namespace": "default", "locale": "de", "value": "Startseite", "status": "translated" } }}The envelope (event, deliveryId, timestamp, projectId) is identical across event types. data varies by event. The dashboard’s Webhook Events reference lists the exact payload shape for each event type — check it when wiring up a new handler.
Signature verification
Section titled “Signature verification”Every request carries an X-TMS-Signature header in Stripe-compatible format:
X-TMS-Signature: t=<unix-seconds>,v1=<hex-hmac>The signed string is <timestamp>.<raw-body>, signed with HMAC-SHA256 using your webhook secret, hex-encoded. Comvi also sends X-TMS-Event and X-TMS-Delivery-Id. Verify the signature before trusting the payload.
import crypto from 'node:crypto';
export function verifyComviWebhook( rawBody: string, header: string, secret: string, toleranceSeconds = 300,): boolean { const parts = Object.fromEntries( header.split(',').map((p) => p.split('=') as [string, string]), ); const { t, v1 } = parts; if (!t || !v1) return false;
const age = Math.abs(Math.floor(Date.now() / 1000) - Number(t)); if (age > toleranceSeconds) return false;
const expected = crypto .createHmac('sha256', secret) .update(`${t}.${rawBody}`) .digest('hex');
const a = Buffer.from(expected); const b = Buffer.from(v1); return a.length === b.length && crypto.timingSafeEqual(a, b);}import hmac, hashlib, time
def verify_comvi_webhook(raw_body: bytes, header: str, secret: str, tolerance=300) -> bool: parts = dict(p.split("=", 1) for p in header.split(",")) t, v1 = parts.get("t"), parts.get("v1") if not t or not v1: return False if abs(int(time.time()) - int(t)) > tolerance: return False expected = hmac.new( secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256, ).hexdigest() return hmac.compare_digest(expected, v1)Delivery, retries, and auto-disable
Section titled “Delivery, retries, and auto-disable”-
Timeout per attempt: 10 seconds. If your endpoint hasn’t responded by then, the attempt is marked failed.
-
Success: any 2xx response.
-
Retries: up to 5 attempts total (1 initial + 4 retries) with exponential backoff plus ±10% jitter:
Attempt Delay after previous failure 2 ~1 minute 3 ~5 minutes 4 ~15 minutes 5 ~1 hour (stop) — -
Auto-disable: after 10 consecutive failures across events, Comvi disables the webhook and fires a
webhook.disabledevent on any other webhooks subscribed to it. The disabled webhook stops receiving events until you fix the endpoint and re-enable it in the dashboard.
You can view and manually re-dispatch any past delivery from Project Settings → Webhooks → <your webhook> → Deliveries.
Idempotency
Section titled “Idempotency”Retries mean your handler may receive the same event twice. Always treat handlers as idempotent:
- Dedupe by
deliveryIdif strict exactly-once matters - Otherwise, ensure the side effect is safe to repeat (e.g. “trigger a CI build” is safe; “charge a credit card” would not be)
Testing locally
Section titled “Testing locally”Webhooks need an HTTPS URL your local server can receive at. Use a tunneling tool:
ngrok http 3000# Copy the HTTPS URL ngrok prints and register it as the webhook URLClick Send Test in the dashboard to fire a webhook.test payload without waiting for a real event.
Examples
Section titled “Examples”Trigger a Vercel rebuild on content change
Section titled “Trigger a Vercel rebuild on content change”Subscribe to translation.created, translation.updated, and translation.batch_updated — and/or key.* events if you want rebuilds on source-key changes too — then call a Vercel Deploy Hook:
import { verifyComviWebhook } from './verify';
export default async function handler(req, res) { const raw = await readRaw(req); if (!verifyComviWebhook(raw, req.headers['x-tms-signature'], SECRET)) { return res.status(401).end(); }
const { event } = JSON.parse(raw); if (event.startsWith('translation.') || event.startsWith('key.')) { await fetch(process.env.VERCEL_DEPLOY_HOOK_URL, { method: 'POST' }); } res.status(200).end();}Debounce on your CI side or inside this handler if you expect bursts (a bulk translate can fire translation.batch_updated once for the whole batch).
Post to Slack on import
Section titled “Post to Slack on import”if (event === 'import.completed') { await fetch(process.env.SLACK_WEBHOOK_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: `Import finished: ${data.keysCreated} created, ${data.keysUpdated} updated.`, }), });}Limits
Section titled “Limits”- Per-attempt timeout: 10 s
- Total attempts per event: 5
- Auto-disable threshold: 10 consecutive failures
- URL protocol: HTTP or HTTPS accepted; HTTPS strongly recommended for production
Troubleshooting
Section titled “Troubleshooting”Signature check fails but secret is correct
Section titled “Signature check fails but secret is correct”You’re probably re-serializing the body before verifying. Capture the raw body before your JSON parser runs.
I’m getting duplicate deliveries
Section titled “I’m getting duplicate deliveries”Retries — normal. Dedupe on deliveryId or make the handler idempotent.
My webhook got auto-disabled
Section titled “My webhook got auto-disabled”Check the deliveries list for the failure reason. Fix your endpoint (5xx responses? timeouts? SSL issues?), then toggle the webhook active again in the dashboard.
I need an event for “translations were published”
Section titled “I need an event for “translations were published””Not available today. Track translation.batch_updated or poll the CDN. Star this request — it is on the roadmap.