Webhooks
Automette POSTs a JSON payload to your endpoint when something happens in your team — a render finishes, a render fails, or a form is submitted. Every payload from a registered endpoint is signed so you can verify it came from us.
Manage endpoints under Platform → Webhooks, on a template's page (template-scoped), in a form's builder (form-scoped), or via the webhooks API.
Events
| Event | Fires when |
|---|---|
render.completed | A single render finished successfully (API, on-demand, automation, form). Bulk/CSV rows do not fire this — use job.completed. |
render.failed | A single render failed or errored. |
job.completed | A bulk/CSV job finished — one event per job with row counts, instead of a delivery per row. |
form.submitted | A submission landed on a hosted form. |
Delivery tiers
An endpoint's scope decides which events reach it. The tiers are additive — a single render can notify more than one endpoint:
- Team-wide — an unscoped endpoint receives every matching event for your team.
- Object-scoped — a template-scoped endpoint receives render events for that template only; a form-scoped endpoint receives that form's submissions only. A scoped endpoint never receives another object's events.
- Per-request — pass
webhook_urlonPOST /api/v1/rendersfor a one-off callback tied to that single render. See per-request callbacks below.
Payload envelope
Registered endpoints receive this envelope. event_id is stable across retries — use it to deduplicate.
POST /your-endpoint HTTP/1.1
Content-Type: application/json
webhook-id: evt_9f2a...
webhook-timestamp: 1714000000
webhook-signature: v1,K5o0N...
{
"event_id": "evt_9f2a...",
"event": "render.completed",
"render_id": "cm4rnd...",
"job_id": "cm4job...",
"self": "https://automette.com/api/v1/renders/cm4rnd...",
"status": "completed",
"url": "https://cdn.automette.com/.../invoice.pdf",
"format": "pdf",
"visibility": "public",
"source": "api",
"template_id": "cm4tpl...",
"template_name": "Invoice",
"data": { "name": "Jane Cooper", "amount": "42" },
"created_at": "2026-04-22T12:00:00.000Z"
}render.failed has the same shape with status: "failed" and an error string instead of url/format.
job.completed
A bulk/CSV job emits one summary event when it finishes — not a delivery per row. A partial run reports status: "completed" with a non-zero failed count.
{
"event_id": "evt_...",
"event": "job.completed",
"job_id": "cm4job...",
"template_id": "cm4tpl...",
"template_name": "Invoice",
"source": "csv",
"status": "completed",
"total": 5000,
"completed": 4998,
"failed": 2,
"created_at": "2026-04-22T12:00:00.000Z",
"completed_at": "2026-04-22T12:03:20.000Z"
}To act on each document in a bulk run individually, open the job in the dashboard or list the template's renders via GET /api/v1/renders?template_id=… — bulk runs deliberately don't send a webhook per row.
form.submitted
{
"event_id": "evt_form_cm4sub...",
"event": "form.submitted",
"form_id": "cm4frm...",
"submission_id": "cm4sub...",
"answers": { "email": "jane@example.com", "message": "Hi!" },
"submitted_at": "2026-04-22T12:00:00.000Z"
}If the form also generates a document, subscribe to render.completed / render.failed for each file's outcome.
Verifying signatures
Signing follows the Standard Webhooks spec. Compute an HMAC-SHA256 over {webhook-id}.{webhook-timestamp}.{raw-body} using your endpoint's secret, base64-encode it, and compare with the webhook-signature header (format v1,<base64>). The secret is shown once, on creation.
import { createHmac, timingSafeEqual } from "crypto"
function verify(req, secret) {
const id = req.headers["webhook-id"]
const ts = req.headers["webhook-timestamp"]
const sig = req.headers["webhook-signature"] // "v1,<base64>"
const key = Buffer.from(secret.replace(/^wh_/, ""), "base64url")
const expected = createHmac("sha256", key)
.update(`${id}.${ts}.${req.rawBody}`)
.digest("base64")
const got = sig.split(",")[1]
return timingSafeEqual(Buffer.from(got), Buffer.from(expected))
}Reject anything that doesn't verify, and reject stale timestamps (e.g. older than 5 minutes) to prevent replay.
Retries and auto-disabling
A non-2xx response or a timeout (10s) is a failure. Automette retries with exponential backoff — up to 5 attempts per event. After 10 consecutive failures the endpoint is auto-disabled; re-enable it (which resets the counter) once your endpoint is healthy. Deliveries are logged and viewable under Platform → Webhooks.
Per-request callbacks
Passing webhook_url on a render request delivers that render's result to the given URL once. It rides the same delivery pipeline (retries, delivery log) but is unsigned — it carries no webhook-signature header, because an ad-hoc URL has no registered secret. Use a registered endpoint when you need signature verification.
Last updated on