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.
| 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
- Install the modulePass webhooks.New to gocommerce.New, as below, or start the reference binary with -webhooks. It adds two tables and a delivery worker.
- Register an endpointOn 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.
- Verify the signatureYour 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.
- Watch the logGET /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 onceA delivery that succeeded but could not be recorded as delivered is sent again. Deduplicate on the event id in the body.
- Order is not guaranteedDeliveries 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 URLOnly 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, unfilteredAn 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 emitsOrders, 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 endpointThe 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 in the GoCommerce repository, MIT licensed. When this page and the code disagree, the code is right and this page is out of date.
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.