Plugin

Webhooks

Tell other people's systems when something happened in yours. Signed to the Standard Webhooks spec, retried with backoff, and logged so you can prove what you sent.

This is the outgoing direction. For verifying webhooks other people send you, which the core already handles, see receiving webhooks.

grit plugin add webhooks
cd apps/api && go build ./...

Sending one

One call, from anywhere a handler can reach. It does not block on the network: the dispatch is queued, and a consumer that takes nine seconds to answer is the queue's problem rather than your request's.

func (h *InvoiceHandler) MarkPaid(c *gin.Context) {
inv, err := h.Invoices.MarkPaid(c.Request.Context(), c.Param("id"))
if err != nil {
respond.WriteError(c, err, "Could not mark the invoice paid")
return
}
// Everyone subscribed to invoice.paid hears about it.
handlers.DispatchWebhook("invoice.paid", gin.H{
"id": inv.ID,
"total": inv.Total,
})
respond.OK(c, inv)
}

Why the signature is shaped like that

The signed content is {id}.{timestamp}.{body}, not the body alone. Signing only the body produces a signature that stays valid for ever: anybody who captures one request can send it again tomorrow, and it verifies. With the id and timestamp inside the signature, a replay is a message the consumer has already seen, or one whose timestamp is outside its tolerance.

Following the spec also means your consumers do not have to trust your crypto. They verify with any off-the-shelf Standard Webhooks library, in whatever language they use, and the integration instructions you owe them are one paragraph.

What happens when it fails

Consumers go down. Each attempt is recorded with its status, retries back off exponentially with jitter so a recovering endpoint is not knocked over by everything at once, and a delivery that exhausts its budget lands in the dead letter rather than disappearing. The admin page lists them and resends in one click.

That log is the part worth having before you need it. "We never got the callback" is unanswerable without it, and unarguable with it.

Using it, step by step

Standing up outbound webhooks on a real project, and then breaking one on purpose, which is the only way to find out whether the retry path works.

Install it

grit plugin add webhooks

Writes the wiring into internal/handlers and the admin page. The runtime itself is the grit-webhooks Go module, so upgrades are go get rather than a regenerate.

Rebuild and restart the API

cd apps/api && go build ./...

The subscriptions and delivery_attempts tables are migrated on boot, so there is no separate migration to run.

Add a subscription

Admin, then System, then Webhooks. A subscription is a URL, the events it wants, and a generated secret that starts whsec_. Show the secret to the consumer once: it is theirs, not yours.

Dispatch an event from your own code

handlers.DispatchWebhook("invoice.paid", gin.H{"id": inv.ID, "total": inv.Total})

Returns immediately. Delivery, retries and the log all happen behind it, so a slow consumer never slows your handler down.

Watch it arrive, then break it on purpose

The delivery log shows every attempt with its status. Point a subscription at a URL that 500s and watch the backoff, the dead letter when the budget runs out, and the resend button putting it back in the queue.

Related plugins

Near neighbours, and what each one is for instead.

Everything else is on the marketplace, and four ways to extend says when a plugin is the wrong tool for the job.