Appearance
Using Webhooks
The first step to adding webhooks is to build your own custom endpoint. The webhook endpoint is just more code on your server, which could be written in Python, Java, Node.js, or whatever.
Before looking at the code, there are several key considerations regardless of the technology involved.
Key considerations
For each event occurrence, Spark POSTs the webhook data to your endpoint in JSON format. The webhook data is in the same format as "Latest Price Release" endpoint, can be used directly after parsing the JSON. Thus, at minimum, the webhook endpoint needs to expect data through a POST request and confirm successful receipt of that data. Beyond those two concepts, you should also…
Return a 2xx status code quickly
To acknowledge receipt of an event, your endpoint must return a 2xx HTTP status code to Spark. All response codes outside this range, including 3xx codes, indicate to Spark that you did not receive the event.
If Spark does not receive a 2xx HTTP status code, the notification attempt is repeated. After multiple failures to send the notification, Spark marks the event as failed and stops trying to send it to your endpoint.
Because properly acknowledging receipt of the webhook notification is so important, your endpoint should return a 2xx HTTP status code prior to any complex logic that could cause a timeout.
Test that your endpoint works
As your webhook endpoint is used asynchronously, its failures may not be obvious to you. Always test that your endpoint works:
- Upon initial creation
- After taking it live
- After making any changes
Example code
python
import hashlib
import hmac
import json
import os
from json import JSONDecodeError
from typing import Union
from flask import Flask
from flask import request
app = Flask(__name__)
SIGNING_SECRET = os.environ["SPARK_WEBHOOK_SIGNING_SECRET"]
def verify_signature(
raw_payload_data: Union[bytes, str], spark_signature: str, signing_secret: str
):
if isinstance(raw_payload_data, bytes):
raw_payload_data = raw_payload_data.decode("utf-8")
signature_items = dict()
for item in spark_signature.split(","):
k, v = item.split("=")
signature_items[k] = v
timestamp = signature_items["t"]
expected_signature = signature_items["v1"]
signed_payload = f"{timestamp}.{raw_payload_data}"
actual_signature = hmac.new(
key=signing_secret.encode("utf-8"),
msg=signed_payload.encode("utf-8"),
digestmod=hashlib.sha256,
).hexdigest()
if hmac.compare_digest(actual_signature, expected_signature):
print("Signature is valid, continue...")
else:
raise ValueError(
f"Signature is invalid, actual_signature={actual_signature}, "
f"expected_signature={expected_signature}"
)
@app.route("/my-webhook", methods=["POST"])
def my_webhook():
spark_signature = request.headers["Spark-Signature"]
raw_payload_data = request.data
# Verify signature
verify_signature(
raw_payload_data=raw_payload_data,
spark_signature=spark_signature,
signing_secret=SIGNING_SECRET,
)
try:
event = json.loads(raw_payload_data)
except JSONDecodeError:
raise ValueError("Invalid json")
# process event
event_type = event["type"]
event_data = event["data"]
if event_type == "new-price-release":
print(f"New price received: {event_data}")
# process the raw_payload_data
elif event_type == "price-release-revision":
print(f"Price revision received: {event_data}")
# process the raw_payload_data
else:
print(f"Unhandled raw_payload_data type: {event_type}")
return "Ok"
Test webhooks
To test your webhook endpoint using the Spark integrations Dashboard:
- First, add the endpoint to your account in the Dashboard’s Webhooks settings section.
- After adding the endpoint, click its name in the list to access its details.
- On the endpoint details page, click "Send test webhook".
- In the modal that appears, select the event type and then click Send test webhook.
- The resulting notification and your endpoint’s response are viewable by clicking on the event in the logs below.
Verify the Sparks requests
Check signatures
Spark always signs the webhook events it sends to your endpoints by including a signature in each event’s Spark-Signature header. This allows you to verify that the events were sent by Spark, not by a third party. You should always verify the signature.
Before you can verify signatures, you need to retrieve your endpoint’s secret from your Dashboard’s Webhooks settings. Select an endpoint that you want to obtain the secret for, then click the "Copy text"
Spark generates a unique secret key for each endpoint. if you use multiple endpoints, you must obtain a secret for each one you want to verify signatures on.
Verifying signatures
The Spark-Signature header included in each signed event contains a timestamp and one or more signatures. The timestamp is prefixed by t=
, and each signature is prefixed by a scheme. Schemes start with v
, followed by an integer. Currently, the only valid live signature scheme is v1
.
Spark-Signature:
t=1628668982,
v1=7c9f876294aeb88ce9f54e4323510ee5325f4da1ecdd88a90ae67b26b40e4043
Spark generates signatures using a hash-based message authentication code (HMAC) with SHA-256. To prevent downgrade attacks, you should ignore all schemes that are not v1
.
Step 1: Extract the timestamp and signatures from the header
Split the header, using the ,
character as the separator, to get a list of elements. Then split each element, using the =
character as the separator, to get a prefix and value pair.
The value for the prefix t
corresponds to the timestamp, and v1
corresponds to the signature. You can discard all other elements.
Step 2: Prepare the signed_payload string
The signed_payload
string is created by concatenating:
- The timestamp (as a string)
- The character
.
- The raw JSON payload data (i.e. the request body)
Step 3: Determine the expected signature
Compute an HMAC with the SHA256 hash function. Use the endpoint’s signing secret as the key, and use the signed_payload
string as the message.
Step 4: Compare the signatures
Compare the signature (or signatures) in the header to the expected signature. For an equality match, compute the difference between the current timestamp and the received timestamp, then decide if the difference is within your tolerance.
To protect against timing attacks, use a constant-time string comparison to compare the expected signature to each of the received signatures.
To protect against replay attacks, check the timestamp in the Spark-Signature header. Because this timestamp is part of the signed payload, it is also verified by the signature, so an attacker cannot change the timestamp without invalidating the signature. If the signature is valid but the timestamp is too old, you can have your application reject the payload.