Documentation
Webhook Signature Verification
Every webhook delivery includes a Spacdesk-Signature header. Verify it before processing the payload to ensure the request originated from SpacDesk and hasn't been tampered with.
Signature format
The header value has the form t=1716249600,v1=5257a869e7...b3f2:
t— Unix timestamp (seconds) when the signature was generated.v1— HMAC-SHA256 hex digest of the signed content.
Signed content
The signed content is the timestamp and the raw request body joined by a period:
signed_content = "<timestamp>.<raw_json_body>"
Compute HMAC-SHA256(your_signing_secret, signed_content) and compare with the v1 value. Use a constant-time comparison to prevent timing attacks.
Replay protection
Reject any request where t is more than 5 minutes old. This prevents replay attacks from intercepted payloads.
Event envelope
{
"id": "evt_01HX...",
"type": "filing.extracted",
"created": "2026-05-19T22:00:00Z",
"api_version": "1.0.0",
"data": { ... },
"livemode": true
}Deduplicate by the id field — retried deliveries carry the same event ID.
Event types
| Type | When |
|---|---|
| filing.extracted | New EDGAR filing processed, with extracted fields summary |
| alert.tripped | A saved alert query matched a new row |
| news.published | New news article ingested matching tracked symbols |
| trust_value.refreshed | Daily trust-value modeling run completed |
Delivery & retries
SpacDesk sends a POST to your endpoint URL with Content-Type: application/json. If your server returns a 5xx or times out (30 s), SpacDesk retries on an exponential schedule: 1m, 5m, 30m, 2h, 6h, 12h, 24h — then gives up. You can manually redeliver via the POST /v1/events/{id}/redeliver endpoint.
TypeScript verification
import crypto from "node:crypto";
function verifyWebhook(
body: string,
signature: string,
secret: string,
toleranceSec = 300,
): boolean {
const parts = Object.fromEntries(
signature.split(",").map((p) => {
const [k, ...v] = p.split("=");
return [k, v.join("=")];
}),
);
const ts = Number(parts.t);
if (Math.abs(Date.now() / 1000 - ts) > toleranceSec) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${ts}.${body}`)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(parts.v1, "hex"),
);
}
// Express / Next.js handler
app.post("/webhooks/spacdesk", (req, res) => {
const raw = req.body; // must be the raw string, not parsed JSON
const sig = req.headers["spacdesk-signature"] as string;
if (!verifyWebhook(raw, sig, process.env.SPACDESK_WEBHOOK_SECRET!)) {
return res.status(401).send("Invalid signature");
}
const event = JSON.parse(raw);
console.log("Received:", event.type, event.id);
res.status(200).send("ok");
});Python verification
import hashlib, hmac, time
def verify_webhook(
body: bytes,
signature: str,
secret: str,
tolerance_sec: int = 300,
) -> bool:
parts = dict(p.split("=", 1) for p in signature.split(","))
ts = int(parts["t"])
if abs(time.time() - ts) > tolerance_sec:
return False
signed = f"{ts}.".encode() + body
expected = hmac.new(
secret.encode(), signed, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, parts["v1"])
# Flask handler
@app.route("/webhooks/spacdesk", methods=["POST"])
def handle_webhook():
sig = request.headers.get("Spacdesk-Signature", "")
if not verify_webhook(
request.data, sig, os.environ["SPACDESK_WEBHOOK_SECRET"]
):
abort(401)
event = request.get_json()
print(f"Received: {event['type']} {event['id']}")
return "ok", 200Testing
Use webhook.site or requestbin.com to inspect deliveries during development. Register the HTTPS URL as your webhook endpoint, trigger an event, and verify the signature header matches. You can redeliver events from the webhook dashboard.