Skip to content

Using Webhooks

This document explains how to receive real-time event notifications from Spark using webhooks. Webhooks allow Spark to send HTTP POST requests to your server when configured events occur, such as a price release.

Overview

A webhook integration consists of:

  • A webhook endpoint on your server (a publicly accessible URL).
  • A registered webhook endpoint in the Spark Dashboard that Spark will call.
  • Logic in your endpoint to receive and process events.

When Spark triggers an event, it sends the event payload as JSON using an HTTP POST request. The event payload matches the structure of the “Latest Price Release” API response.

Triggers

You can create webhooks to receive the following four event types:

  • new-lng-freight-physical-price-release — when a new LNG Freight physical price release is published
  • new-lng-freight-financial-price-release — when a new LNG Freight financial price release is published
  • new-lng-cargo-physical-price-release — when a new LNG Cargo physical price release is published
  • new-lng-cargo-financial-price-release — when a new LNG Cargo financial price release is published

Register a New Webhook

Go to the Spark application to create a webhook. Save the Signing Secret in a safe place. It is only visible on the application for a few minutes.

You must provide:

  • Your server URL (note: only HTTPS endpoints are allowed).
  • The event type that will trigger the webhook. As you cannot select more than one type, you may need to create multiple webhooks.

You can optionally provide a description for your own reference.

Payload

A webhook payload follows this structure:

json
{
  "webhookUuid": "<unique webhook id>",
  "type": "<trigger type>",
  "releaseDate": "<price release date, ISO format>",
  "attempt": 1,
  "data": [{ "...": "..." }, { "...": "..." }],
  "dataFormat": "/value.0/contracts/<ticker>/price-releases/"
}
  • attempt starts at 1 and increments on retries.
  • data contains the actual price release payload.
  • dataFormat indicates which API endpoint specification the data follows.

Your logic should inspect type and process the content accordingly.

Verifying Request Authenticity

Spark HTTP Headers

Every webhook request includes two headers:

  • Spark-Webhook-Delivery-Id
  • Spark-Webhook-Signature

Spark-Webhook-Delivery-Id is the unique ID of the current delivery. Two webhook executions containing the same data will always have different delivery IDs.

The Spark-Webhook-Signature header contains:

  • A timestamp (ts=...)
  • A signature hash (value=...)

You should always both verify this signature and this timestamp to ensure the request originated from Spark.

Verification Steps

  1. Parse the header to extract the timestamp and the value signature.
  2. Construct the signed payload string: signed_payload = {timestamp}.{raw_request_body}
  3. Compute the expected signature using HMAC SHA-256 with your endpoint’s secret as the key.
  4. Compare the computed signature with the one from the header using a constant-time comparison.
  5. To protect against replay attacks, verify that the timestamp is recent (e.g. less than 5 minutes old).

Important: Only use the value signature. Any other schemes, if present, must be ignored.

Testing Your Webhook

Use the Spark application to test your webhook integration:

  1. Select the webhook to open its details page.
  2. Click Send test webhook.
  3. Check the webhook logs.
  4. Check your server logs to inspect the received payload.

This helps validate that your logic correctly receives and processes events.

Best Practices

Monitor Delivery and Failures

Implement logging around webhook deliveries, signature verification, and event processing for troubleshooting.

Secure Your Endpoint

  • Always verify the signature.
  • Use HTTPS for your webhook endpoints.

Example Implementation (Python)

Below is an example using Flask that:

  • Verifies the Spark signature
  • Parses the JSON payload
  • Handles basic events
python
import hashlib
import hmac
import json
import os
from time import time

from flask import Flask, request

app = Flask(__name__)

SIGNING_SECRET = os.environ["SPARK_WEBHOOK_SIGNING_SECRET"]
MAX_TIMESTAMP_AGE = 300  # 5 minutes in seconds


def verify_signature(payload: bytes, signature_header: str) -> bool:
    # Parse header string: "ts=...,value=..."
    items = dict(item.split("=", 1) for item in signature_header.split(","))
    timestamp = items.get("ts")
    signature = items.get("value")

    if not timestamp:
        print(f"No timestamp found. {signature_header=}")
        return False

    if not signature:
        print(f"No signature found. {signature_header=}")
        return False

    signed_payload = f"{timestamp}.{payload.decode()}"
    computed_signature = hmac.new(
        key=SIGNING_SECRET.encode(),
        msg=signed_payload.encode(),
        digestmod=hashlib.sha256,
    ).hexdigest()

    if not hmac.compare_digest(computed_signature, signature):
        print("Signatures differ")
        return False

    # Verify timestamp is recent to prevent replay attacks
    try:
        timestamp_int = int(timestamp)
    except ValueError:
        print(f"Timestamp should be an int: {timestamp}")
        return False

    age = int(abs(time() - timestamp_int))
    if age > MAX_TIMESTAMP_AGE:
        print(f"The payload is too old: {age}s")
        return False

    return True


@app.route("/webhooks", methods=["POST"])
def webhook():
    raw = request.data
    signature_header = request.headers.get("Spark-Webhook-Signature", "")

    if not verify_signature(raw, signature_header):
        return "Invalid signature", 400

    event = json.loads(raw)
    event_type = event.get("type")
    delivery_uuid = request.headers.get("Spark-Webhook-Delivery-Id", "")

    print(f"New '{event_type}' webhook, {delivery_uuid=}")

    return "", 200

Save this code sample as webhook_server.py. Make sure to not call your application flask.py because this would conflict with Flask itself.

To run the application, use the flask command or python -m flask: $ flask --app webhook_server run.