Higgsfield hf_webhook query parameter vs Sume callback_url

Higgsfield takes the webhook as an hf_webhook query parameter; Sume takes callback_url in the body and signs the delivery. Payload shapes and a Python verifier.

6 min readSume
All posts

On Higgsfield you register a webhook per request by passing an HTTPS endpoint in the hf_webhook query parameter when you submit. On Sume's /v1/videos route you pass callback_url in the request body, which must be HTTPS, and Sume POSTs a signed job event to it when the job reaches a terminal state.

The two envelopes

Higgsfield's envelope has four fields: request_id, status (completed, failed or nsfw), error (null on success) and payload, where video outputs sit under payload.video with a URL and content type. The webhook page read for this post does not describe a signature header.

Webhook shape, read 2026-10-02
ItemHiggsfieldSume
Where you set ithf_webhook query parametercallback_url body field on /v1/videos; webhook_url with mode: "webhook" on generate routes
EventsTerminal status in the envelopejob.completed, job.failed, job.canceled
Identifiersrequest_idrequest_id and job_id
Resultpayload.videopayload.artifacts[] with url, type, content_type
SigningNot described on the pageHMAC SHA 256 over <timestamp>.<raw_body>

Sume's signed delivery

Sume sends terminal events only, with no progress deliveries. A failed or canceled event uses status: "ERROR" with an error object. Two headers carry the proof: x-sume-webhook-timestamp and x-sume-webhook-signature with a value like sume-v1=<hex>. During a secret rotation the header can carry several comma-separated entries, and you accept the delivery if any one matches. Reject a timestamp outside a replay window; five minutes is the documented default.

A Python verifier

Verify against the raw bytes of the body, before any JSON parsing. This function refuses an empty secret and handles rotation.

import hashlib, hmac, time

def verify(raw_body: bytes, timestamp: str, header: str, secret: str, tolerance: int = 300) -> bool:
    if not secret:
        raise ValueError("signing secret is empty")
    try:
        ts = int(timestamp)
    except ValueError:
        return False
    if abs(time.time() - ts) > tolerance:
        return False
    msg = f"{ts}.".encode() + raw_body
    digest = hmac.new(secret.encode(), msg, hashlib.sha256).hexdigest()
    expected = f"sume-v1={digest}"
    return any(hmac.compare_digest(e.strip(), expected) for e in header.split(","))

if __name__ == "__main__":
    body = b'{"event":"job.completed"}'
    ts = str(int(time.time()))
    sig = "sume-v1=" + hmac.new(b"s", ts.encode() + b"." + body, hashlib.sha256).hexdigest()
    print(verify(body, ts, sig, "s"))

Where the secret comes from

Your signing secret comes from the dashboard Webhooks tab or from GET /v1/webhooks/signing-secret with a key that carries account:read. Use job_id as your idempotency key, and keep status polling available in case a delivery never arrives.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume