Webhooks & Events

Set up webhooks to react to Encore events

Webhooks provide a way for notifications to be delivered to an HTTP endpoint of your choice whenever certain events happen within Encore. For example, you can set up a webhook to be notified whenever a deployment starts or finishes.

Webhooks are defined on a per-application basis, and are configured under Settings -> Webhooks in Encore's Cloud Dashboard.

To simplify using webhooks, Encore provides a Go module, go.encore.dev/webhooks, that provides type definitions and documentation of all supported webhook events. This module is kept up to date as new events are added.

Webhook Deliveries

Each time an event occurs that matches one of your defined webhooks, Encore will send a HTTP POST request to the webhook's configured URL with information about the event.

If the HTTP request fails, the delivery is marked as failed and won't be retried.

Each event is given a unique event id, which is shared across all webhooks.

Within each webhook, each event is given a sequence number, which is incremented for each event that matches that webhook. The sequence number allows for a linear ordering of events within a webhook, making it easy to determine if an event was missed.

These are provided in the X-Encore-Event-Id and X-Encore-Sequence-Id headers respectively, and are also part of the event payload itself.

Parsing webhook events

To parse a webhook event, use the webhooks.ParseEvent function.

As you'll see in the example below, to parse the webhook event you'll need access to the webhook secret. This is a secret value that is generated by Encore and is used to sign each webhook request. More about this in the next section.

For example, to process rollout started and completed webhook events, you could do something like this:

package service import ( "net/http" "go.encore.dev/webhooks" ) var secrets struct { EncoreWebhookSecret string } //encore:api public raw func Webhook(w http.ResponseWriter, req *http.Request) { payload, err := io.ReadAll(req.Body) if err != nil { // ... handle error } event, err := webhooks.ParseEvent(payload, req.Header.Get("X-Encore-Signature"), secrets.EncoreWebhookSecret) if err != nil { // ... handle error } switch data := event.Data.(type) { case *webhooks.RolloutCreatedEvent: // ... handle rollout created event case *webhooks.RolloutCompletedEvent: // ... handle rollout completed event } }
Please note

Note that the example above is written as an Encore API endpoint, but that's not required. The same code works in any Go HTTP server, and the go.encore.dev/webhooks library does not depend on any Encore-specific functionality.

Checking webhook signatures

Since the webhook endpoint is publicly accessible, it is important to validate that the request is coming from Encore. To do so, Encore generates a secret for each webhook, which is used to sign each request.

The webhook secret can be found on the webhook details page by admins.

If you use the go.encore.dev/webhooks library then signature validation is handled automatically, but it's also possible to verify the signature manually (see below).

Preventing replay attacks

A replay attack occurs when an attacker intercepts a valid request, including the payload and signature, and re-transmits it one or more times, causing unintended side effects.

To mitigate such attacks, Encore includes a timestamp in the X-Encore-Signature header. This timestamp is part of the the signed payload, which means that it can't be changed by the attacker without invalidating the signature. This makes it possible to mitigate replay attacks by ensuring the timestamp isn't older than a certain threshold (the go.encore.dev/webhooks library defaults to 5 minutes).

Verifying signatures manually

The X-Encore-Signature header included in each webhook event contains a timestamp and one or more schemes. The timestamp is prefixed with t=, and each scheme is prefixed by a v and a version number. Currently only the v1 scheme is supported.

For example, a valid signature header might look like this:

X-Encore-Signature: t=1623345600,v1=0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b

The v1 scheme is using a hash-based message authentication code (HMAC) with SHA-256. To prevent downgrade attacks, ignore all schemes that are not v1.

It's possible the signature contains multiple signatures, for example if the webhook secret has been rotated recently.

When rotating the webhook secret, Encore lets you define for how long the old secret should continue to be valid for. During that window, each webhook event will be signed with the new and the old secret.

To validate the webhook signature, follow the algorithm below:

Step 1: Extract the timestamp and signatures from the header Split the header on , to get a list of fields, then split each field on = to get the key and value.

The value of the t key is the timestamp, and represents the UNIX timestamp (in seconds) when the signature was created. The fields with the v1 key (possibly several, in case of secret rotation) are the signatures.

Discard any other fields.

Step 2: Prepare the payload to sign Create the payload to sign by concatenating the timestamp (as a string) and the request body, separated by . like so:

payloadToSign := timestamp + "." + string(payload)

Step 3: Compute the expected signature Compute the HMAC with the SHA256 hash function, using the webhook secret as the key and the payloadToSign as the message.

Then, encode the resulting HMAC using the base64 URL encoding, and trim any trailing = characters. In Go, this can be done like so:

h := hmac.New(sha256.New, []byte(webhookSecret)) h.Write([]byte(payloadToSign)) digest := h.Sum(nil) expectedSignature := base64.RawURLEncoding.EncodeToString(digest)

Step 4: Compare the signatures Compare each signature with the v1 field in the header with the expected signature. To protect against timing attacks, use a constant-time comparison function (like crypto/hmac.Equal in Go).

If none of the signatures match, reject the request.

If a match is found, compare the timestamp with the current time. If the difference is greater than the allowed threshold (5 minutes is a reasonable default), reject the request.

Otherwise, accept the request.