Webhooks - britned/empire-platform-api GitHub Wiki

Webhook Principles

A "webhook" is an event-driven, automated way for a service (in this case Empire) to send real-time data to another service when a specific event occurs, using a standard HTTP request. It eliminates the need for constant polling, making the change detection process efficient and instant.

Starting from version 6.0.0 of Empire, Webhooks can be configured in the Organisation Settings by users with the PART_ADMIN role. The Webhooks are triggered by predefined events such as Auction's specification changes, an Unplanned Outage is created or updated, etc.

❗ Please note that due to security reasons Webhooks can only be managed (created, edited, deleted) through the GUI, and not with API Keys

A single Webhook registered in Empire can "listen" to an arbitrary combination of Webhook Events: Participants can choose to have a single URL receiving all events, or split them as they wish, although a maximum of 20 Webhooks with unique URLs can be registered.

The current list of Webhook Events is the following:

Domain Event
Auctions LONG_TERM_AUCTION_SPECIFICATION_UPDATED
DAY_AHEAD_AUCTION_SPECIFICATION_UPDATED
INTRA_DAY_AUCTION_SPECIFICATION_UPDATED
AUCTION_STATUS_UPDATED
AUCTION_CANCELLED
LONG_TERM_AUCTION_ALLOCATED
DAY_AHEAD_AUCTION_ALLOCATED
INTRA_DAY_AUCTION_ALLOCATED
Outages UNPLANNED_OUTAGE_CREATED
UNPLANNED_OUTAGE_UPDATED
UNPLANNED_OUTAGE_CANCELLED
UNPLANNED_OUTAGE_ENDED
Curtailment CURTAILMENT_EXECUTED
Buy Now Offers BUY_NOW_OFFER_PUBLISHED
Nomination Window Overrides NOMINATION_WINDOW_OVERRIDE_CREATED
NOMINATION_WINDOW_OVERRIDE_UPDATED
Secondary Market LONG_TERM_SECONDARY_MARKET_TRANSFER_REQUEST_RECEIVED
DAY_AHEAD_SECONDARY_MARKET_TRANSFER_REQUEST_RECEIVED
INTRA_DAY_SECONDARY_MARKET_TRANSFER_REQUEST_RECEIVED
Finance FINANCE_INVOICE_SENT
FINANCE_INVOICE_MARKED_AS_PAID

Webhook Requests

Each Webhook is documented in 📚 openapi.yaml and can also be browsed using the 🧭 API Navigator.

The general structure of Webhook Requests are the following:

  • requests are sent with the HTTP POST verb
  • to the exact URL specified on the GUI (no trailing slashes or postfixes added)
  • the following three extra request headers are sent with every request:
    • X-Webhook-Event-Type - Type of the Event being triggered (value from the WebhookEventType schema enum)
    • X-Webhook-Timestamp - Unix timestamp (seconds) when the event was triggered
    • X-Webhook-Signature - HMAC-SHA256 signature for the request
  • a specific request payload is sent with each Webhook (please refer to the documentation)
    • every JSON object will contain a top-level eventType string field with the appropriate value from the WebhookEventType schema

As a reply Empire does not expect any specific response to be returned. Although any response (both code and body) are to be recorded inside Empire and will be visible under the "History" section of the given Webhook. Any 2xx response code will mark the attempt as "Successful", and any non-2xx code will mark the attempt as "Failed" on the GUI.

💡 For a general "positive acknowledgement" a 204 - No Content status code with empty request body may be sent.


Webhook Security

Ensuring the security and integrity of Webhooks is a shared responsibility of the producer (in this case Empire) and the consumers (in this case the Participants).

Empire is enforcing HTTPS URLs (and therefore TLS encryption) for webhooks, and Participants MUST implement at least one of the "IP Whitelisting" or the "Signature Verification" measures.

Mandatory HTTPS URLs

Empire is enforcing HTTPS URLs to be defined for each Webhook on the PRODUCTION environment.

The URLs defined MUST have a valid CA-Signed Certificate issued by a trusted Certificate Authority (no self-signed certificates are allowed, the trusted CAs can be found in this list). Failing to pass the certificate validation will cause Empire not to invoke the Webhook at all (errors will turn up in the "History" section).

💡 In order to ease the testing process, on the TRAINIG environment HTTP URLs are allowed to be specified.

IP Whitelisting

In order to make sure the requests are originated from Empire Participants can use the following IP addresses to whitelist the appropriate environments. Request can be received from any of the defined IP addresses, please make sure to whitelist all of them.

Environment IP Addresses
PRODUCTION 34.241.130.214
52.51.125.60
54.73.152.4
35.157.94.168
3.69.111.192
3.76.67.69
TRAINING 52.211.234.221
52.215.241.30
52.48.169.210

Signature Verification

When Participants receive a request from Empire, your code can check its authenticity by computing a signature.

On each request Empire sends the X-Webhook-Timestamp and X-Webhook-Signature headers, where the signature is created by SHA-256 hashing the request body and teh timestamp, and combining it with an HMAC "signing secret", resulting in a unique signature to each request.

Signature verification follows this pattern:

  • your code receives a request from Empire
  • your code computes the signature based on the request
  • you make sure the signature you've computed matches the signature on the request

Verification Steps

Please follow the logical steps below in any programming language of your choice to perform the signature verification process. We provide pseudocode examples for each step.

1. Prepare your "signing secret"

When you create a Webhook a "signing secret" is automatically generated for you and saved at the same time in Empire.

❗ Make sure to save this "signing secret" to a secure location, as for security purposes it cannot be displayed again in the future

2. Validate the Timestamp

The signature depends on the Request Timestamp to protect against replay attacks.

First get the value of the X-Webhook-Timestamp header (it's a Unix timestamp in seconds) and compare it to the current timestamp.

Check if the request is not older than X minutes, otherwise drop the request and stop processing as it may be a replay attack.

💡 In practice a threshold of 2-5 minutes might be sufficient to avoid attacks

3. Compose the Signature Base

This is how Empire composes the signature the following way, which Participants need to replicate in pseudocode.

Get the necessary components to put together the "signature base" string: concatenate the signature version, the timestamp and the request body with colons.

timestamp = request.headers.get('X-Webhook-Timestamp')  # value of the timestamp header
body = request.body                                     # request body as a string

signature_base = 'v0:' + timestamp + ':' + body

The resulting signature_base should look something like v0:1740492080:{"eventType"...

4. Compose and Verify the Signature

Then compose the final signature use the following logical steps:

signing_secret = 'YOUR_SIGNING_SECRET'

signature = 'v0=' +
            hmac_sha256(
              signing_secret,
              signature_base
            ).hexdigest()

The resulting signature should look something like v0=316235a7b4a8d21a1...

This string should be compared to the value of the X-Webhook-Signature request header and if they does not match, the request should be dropped.


Minimal Working Examples

The following examples can be used to kickstart working with Webhooks, an example implementation of Signature Verification also included.

Node.js & Express

The Node.js example is using express and body-parser libraries, and Node's built-in crypto library for HMAC-256 signature verification.

// configuration
const SIGNING_SECRET = '<YOUR_SIGNING_SECRET_HERE>';
const REPLAY_ATTACK_MINUTES = 2;

// external dependencies
const express = require('express');
const bodyParser = require('body-parser');

// Node dependencies
const crypto = require('crypto');

const app = express();
const port = 3000;

// save raw body on request for signature verification
app.use(bodyParser.json({
  verify: (req, res, buf) => {
    req.rawBody = buf
  }
}));

// webhooks are sent as POST requests
app.post('/', (req, res) => {
  verifySignature(req);

  const eventType = req.headers['x-webhook-event-type'];
  console.log(`Received Webhook Event: ${eventType}`);

  const body = req.body;
  console.log(body);

  // TODO: implement custom webhook logic based on `eventType` or `body.eventType`

  res.status(204).send();
})

// signature verification based on request
function verifySignature(req) {
  const signature = req.headers['x-webhook-signature'];
  const timestamp = Number.parseInt(req.headers['x-webhook-timestamp']);
  const body = req.rawBody; // make sure to use raw body (string)

  // compare timestamps to avoid replay attacks
  const now = Math.round(Date.now() / 1000);

  if ((now - timestamp) > REPLAY_ATTACK_MINUTES * 60) {
    throw new Error('request is too old, possible replay attack');
  }

  // calculate signature to ensure authenticity
  const signatureBase = `v0:${timestamp}:${body}`;
  const hash = crypto.createHmac('sha256', SIGNING_SECRET)
    .update(signatureBase)
    .digest('hex');
  const computedSignature = `v0=${hash}`;

  if (computedSignature !== signature) {
    throw new Error('invalid request signature');
  }
}

// start webhook handler
app.listen(port, () => {
  console.log(`Webhook handler listening on port ${port}`);
})
⚠️ **GitHub.com Fallback** ⚠️