Webhooks
Overview
Webhooks allow your application to receive real-time notifications about events happening in your Scrydex account. Instead of polling the API for updates, Scrydex can push data to your specified URL via HTTP POST requests with a JSON body.
Purpose of Webhooks
Webhooks are ideal for keeping your local database in sync with Scrydex data, triggering automated workflows, or notifying your users about price changes and new card listings.
Supported Webhooks
We currently support the following webhook events:
<game>.expansions.prices.raw_updated: Triggered when a card's raw market price is updated.<game>.expansions.prices.graded_updated: Triggered when a card's graded market price is updated.<game>.expansions.pop_reports.updated: Triggered when population report data is updated.
Note: The
<game>placeholder should be replaced with the specific TCG slug (e.g.,pokemon.expansions.prices.raw_updated). Each TCG-specific documentation section will outline the exact webhook events and payload details available for that game.
Retry Policy
We understand that your services might occasionally be unavailable. If your endpoint does not return a 2xx success status code, Scrydex will retry the delivery.
We retry failed webhooks up to 4 times with exponential backoff before marking the delivery as failed.
Response Timing
Your webhook receiver should be fast to ensure reliable delivery. Ideally, your endpoint should process the event and respond in less than 1 or 2 seconds. Scrydex will time out if a response is not received within 10 seconds.
Here is a sample webhook payload:
{
"id": "6a0375aea0b689dc8dc57fda",
"name": "magicthegathering.expansions.prices_updated",
"data": {
"expansion_ids": [
"CNS",
"PS14",
"PDP15",
"PPC1",
"PM15",
"CP1",
"V14",
"DDN",
"PKTK",
"OC14"
]
}
}
const express = require('express');
const app = express();
// Use express.raw() to get the raw body for signature verification
app.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-scrydex-signature'];
const payload = req.body; // This is a Buffer
// Verify signature...
console.log('Received webhook:', JSON.parse(payload));
res.status(200).send('OK');
});
app.listen(3000, () => console.log('Server running on port 3000'));
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/webhooks', methods=['POST'])
def webhook():
# Use request.data to get the raw body for signature verification
signature = request.headers.get('X-Scrydex-Signature')
payload = request.data # This is the raw byte string
# Verify signature...
print(f"Received webhook: {payload.decode('utf-8')}")
return 'OK', 200
if __name__ == '__main__':
app.run(port=3000)
Security
To ensure that webhook requests are authentic and haven't been tampered with, Scrydex signs every payload. We recommend that you verify these signatures before processing any event.
Scrydex webhooks use HMAC-SHA256 signatures with a shared secret (prefixed with whsec_...) sent via the X-Scrydex-Signature header.
Verifying Signatures
The X-Scrydex-Signature header contains two parts: a Unix timestamp (t) and the HMAC-SHA256 signature (v1).
Format: t=timestamp,v1=signature
1. Construct the Payload
The string to be signed is constructed by concatenating the timestamp, a dot, and the raw request body:
timestamp + "." + raw_request_body
2. The "Raw Body" Rule
Crucial: You must use the raw, unparsed byte string of the request body for signature verification. Using a re-stringified JSON object will fail because changes in spacing, key order, or encoding (Scrydex sends minified UTF-8 JSON) will result in a different signature.
Security Best Practices
- Replay Protection: Verify that the timestamp
tis within a 5-minute window of the current time. - Constant-time Comparison: Always use "secure compare" methods to prevent timing attacks.
- Secret Handling: Treat webhook secrets like API keys. Store them in environment variables and never hardcode them.
Signature Example
X-Scrydex-Signature: t=1625097600,v1=6a2e8593b000676449f993d5626359f518e38812674251a27e792476b76ed6a6
const crypto = require('crypto');
function verifySignature(payload, header, secret) {
const parts = header.split(',');
const timestamp = parts.find(p => p.startsWith('t=')).split('=')[1];
const signature = parts.find(p => p.startsWith('v1=')).split('=')[1];
const signedPayload = `${timestamp}.${payload}`;
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
import hmac
import hashlib
def verify_signature(payload, header, secret):
parts = dict(item.split("=") for item in header.split(","))
timestamp = parts.get("t")
signature = parts.get("v1")
signed_payload = f"{timestamp}.{payload.decode('utf-8')}"
expected_signature = hmac.new(
secret.encode('utf-8'),
signed_payload.encode('utf-8'),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected_signature)
