# Webhooks for GoCommerce — Signed Events With Retries

> Send a GoCommerce store’s events to any server: register a URL and the events it wants, and each is POSTed signed, retried with backoff and logged.

- Canonical: https://kitcommerce.store/integrations/webhooks/
- Last updated: 2026-09-25

---

Platform

## Webhooks for GoCommerce

A Go package that delivers the store’s events to somebody else’s server. Register a URL and the events it cares about, and every matching event is POSTed to it, signed with HMAC-SHA256, retried with backoff for up to twelve attempts, and recorded in a delivery log an operator can read.

- **Platform** module
- **1** setting
- **12** tests
- **8** API operations
- **ext/webhooks**

### What it does

In the package’s own words it is “the door for a consumer that is not written in Go”. The engine’s events are durable and in-process; this module turns each one into an HTTP POST for every endpoint that asked for it.

An endpoint is a URL, a list of event patterns — an exact name such as order.paid, a prefix such as order.\*, or \* for everything — an active flag, and a signing secret the store generates. The body is the engine’s own event: its id, name, version, time, what it is about, and its data.

Subscribing and sending are kept apart on purpose. The subscriber writes one delivery row per matching endpoint and returns; a background worker does the sending. A slow or unreachable server therefore never holds up the engine’s outbox, and never makes another subscriber — invoices, notifications — run a second time.

### Configuration

One setting, with a default. Endpoints are not configuration: they are rows an operator creates on the admin’s Webhooks screen or through the API, each with its own secret.

*Webhooks module settings — 1 setting, 0 required*

| Setting | Environment variable | Required | What it does |
| --- | --- | --- | --- |
| `Timeout` | — | No | Bounds one POST to one endpoint: 10 seconds when zero. Short on purpose — in the package’s words, a merchant that takes longer is doing work it should have queued. Go only. |

The module reads its Config struct, not the environment. The variable names are the ones the package’s own example or the reference binary uses; in your own `main()` you choose where each value comes from. Where a setting has a panel label, the admin’s settings drawer can hold it too, and a value typed there wins over Config.

### Setting it up

1. **Install the module** — Pass webhooks.New to gocommerce.New, as below, or start the reference binary with -webhooks. It adds two tables and a delivery worker.
2. **Register an endpoint** — On the admin’s Settings › Webhooks screen, or POST /api/admin/x/webhooks/endpoints with a url and events such as order.\*. The reply carries the signing secret — the only time it is shown.
3. **Verify the signature** — Your server recomputes HMAC-SHA256 over the timestamp, a dot and the raw body with that secret, and compares it with v1 in the X-GoCommerce-Signature header.
4. **Watch the log** — GET /api/admin/x/webhooks/deliveries lists each delivery as pending, delivered or dead, with its last status and error. A dead one can be queued again by hand.

main.go

```
import (
	"github.com/itswadesh/gocommerce/core"
	webhooks "github.com/itswadesh/gocommerce/ext/webhooks"
)

app, err := gocommerce.New(cfg,
	// Timeout bounds one POST to one endpoint; zero means 10 seconds.
	webhooks.New(webhooks.Config{}),
)
```

The package doc has no example of its own; this is how the reference binary’s -webhooks flag installs it. cfg is your gocommerce.Config. Import path `github.com/itswadesh/gocommerce/ext/webhooks`.

### How it works

- **A familiar signature**

  X-GoCommerce-Signature is t=\<unix time>,v1=\<hex HMAC-SHA256>, signed over the time, a dot and the body — the shape Stripe uses. The time is inside the signed material, so it cannot be rewritten on a replay; checking its age is the receiver’s job.

- **Retries with backoff**

  A non-2xx answer or a network error schedules another attempt, the wait doubling from two seconds to a cap of 15 minutes. After twelve attempts the delivery is marked dead and kept, not deleted.

- **One row per endpoint per event**

  A unique key on endpoint and event id means the engine redelivering an event — its outbox guarantees at least once — never becomes a second row, or a second POST, for the same endpoint.

- **Safe across instances**

  The worker claims 20 deliveries at a time with FOR UPDATE SKIP LOCKED and hides them for 60 seconds, so several instances send without coordinating and a process that dies mid-POST releases its work.

- **Redirects are failures**

  A 3xx is recorded as a failed delivery, not followed: an endpoint that redirects is misconfigured, and following it would send the signed payload somewhere the operator did not name.

- **Secrets shown twice**

  The secret, whsec\_ and 64 hex characters, is returned when the endpoint is created and when it is rotated, and never again. Lists show its last four characters.

### What it does not do

Read these before an order depends on it. No store is known to run GoCommerce in production yet, so these come from the code, not from anyone’s experience.

- **At least once, not exactly once** — A delivery that succeeded but could not be recorded as delivered is sent again. Deduplicate on the event id in the body.
- **Order is not guaranteed** — Deliveries go out oldest first, but a failed one waits out its backoff while later events are sent — an endpoint can see order.shipped before a retried order.paid.
- **Any http or https URL** — Only the scheme and host are checked. Plain http is accepted, and so is an address on a private network; whether that is wise is the operator’s call.
- **The whole event, unfiltered** — An endpoint subscribed to cart.abandoned, or to \*, receives the cart token — which the engine treats as a credential for that basket — and the shopper’s email.
- **Only the events the engine emits** — Orders, abandoned carts, products and collections. There is no event for a new customer or a stock change, so there is no webhook for either.
- **Nothing to tune per endpoint** — The retry count, backoff, batch size and poll interval are constants in the package. Changing them is an edit to the code.

FAQ

### Questions about the Webhooks module

**Which events can I subscribe to?**

Every event the engine emits: order.created, order.paid, order.shipped, order.delivered, order.cancelled, order.refunded, order.edited and order.returned, the reversals order.unpaid, order.unshipped, order.undelivered and order.unreturned, cart.abandoned, product.created, product.updated, product.deleted and collection.updated. An endpoint names exact events, prefixes such as order.\*, or \* for all.

**How do I verify a delivery?**

Split X-GoCommerce-Signature into t and v1, compute HMAC-SHA256 with the endpoint’s secret over t, a dot and the raw request body, and compare it with v1 in constant time. Refuse a t that is too old, so a captured request cannot be replayed.

**What happens when my server is down?**

Each delivery is retried, the wait doubling up to 15 minutes, for twelve attempts in all. Then it is marked dead but kept; the delivery log shows it, and an operator can queue it again from the admin or with POST /api/admin/x/webhooks/deliveries/{id}/retry.

**Who can register an endpoint?**

An operator with webhooks.write — the owner, until the right is granted to another role — or anyone holding a static admin token. Reading endpoints and the delivery log needs webhooks.read.

### Source

Everything on this page is read from [`ext/webhooks`](https://github.com/itswadesh/gocommerce/tree/main/ext/webhooks) in the GoCommerce repository, MIT licensed. When this page and the code disagree, the code is right and this page is out of date.

- [ext/webhooks on GitHub](https://github.com/itswadesh/gocommerce/tree/main/ext/webhooks)
- [All GoCommerce modules](https://kitcommerce.store/integrations/)

### Try it against a store of your own

The one-command stack gives you GoCommerce’s API and admin on your own machine in minutes. Add this module to it and try it on test orders before a real one depends on it.

[Deploy in minutes](https://kitcommerce.store/#one-command) · [Read the module](https://github.com/itswadesh/gocommerce/tree/main/ext/webhooks) · [All integrations](https://kitcommerce.store/integrations/)
