SpacDesk logoSpacDesk

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:

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

TypeWhen
filing.extractedNew EDGAR filing processed, with extracted fields summary
alert.trippedA saved alert query matched a new row
news.publishedNew news article ingested matching tracked symbols
trust_value.refreshedDaily 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", 200

Testing

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.