Kling motion control webhook_url: a Python receiver that verifies

Send mode async plus webhook_url on a Kling motion control submit, then verify x-sume-webhook-signature (HMAC SHA-256 over timestamp.body) in Python.

5 min readSume
All posts

Kling motion control on Sume takes mode: "async" with a webhook_url, and Sume posts the terminal job to that URL with an x-sume-webhook-signature header. Verify it with HMAC SHA-256 over <timestamp>.<raw_body>, and refuse the call when the secret is empty.

Two names for the same idea

This is easy to mix up. The motion control body uses webhook_url. The OpenRouter-shaped POST /v1/videos body uses callback_url, and it must be HTTPS. Both give you the same signed job envelope on completion, not a video.generation.* event. If you move a Kling job between the two surfaces, rename the field.

The submit

Add the field next to the usual motion body: one visual source, the motion_video_url, and duration_seconds (1 to 30). Keep polling as a backup; Sume's docs say to keep polling even when you use a webhook.

The receiver

The signature header looks like sume-v1=<hex>. During a secret rotation it holds one entry per live secret, newest first, separated by commas, so accept the delivery when any entry matches. Reject timestamps that are older than your replay window; five minutes is the documented default.

import hashlib, hmac, os, time

def verify(raw: bytes, ts: str, header: str, secret: str) -> bool:
    if not secret:
        raise RuntimeError("signing secret is empty")
    if abs(time.time() - int(ts)) > 300:
        return False
    mac = hmac.new(secret.encode(), ts.encode() + b"." + raw,
                   hashlib.sha256).hexdigest()
    for part in header.split(","):
        k, _, v = part.strip().partition("=")
        if k == "sume-v1" and hmac.compare_digest(v, mac):
            return True
    return False

SECRET = os.environ.get("SUME_COM_WEBHOOK_SIGNING_SECRET", "")

What to check on each delivery

Webhook signature facts from Sume docs (read 2026-10-05)
ItemValue
Signed string<timestamp>.<raw_body>
AlgorithmHMAC SHA-256, hex
Headersx-sume-webhook-timestamp, x-sume-webhook-signature
Rotationone sume-v1= entry per live secret
Failed or canceled jobstatus ERROR with an error object

Next step

Call verify on the raw bytes, not on parsed JSON that you re-serialised, because the signature covers the exact body. Read the secret from the dashboard Webhooks tab or GET /v1/webhooks/signing-secret, then fetch the video from GET /v1/jobs/:id/result.

Common failures

The most common failure is a handler that parses the JSON, then dumps it again and verifies that string. Any change in spacing or key order breaks the HMAC. Read the raw request body once, verify it, and only then parse it.

The second is a clock that drifts. The five-minute window is on the timestamp header, so a server that is ten minutes slow will reject every delivery. Sync the clock before you blame the secret.

The third is a missing secret in a new environment. The code above raises when the secret is empty, because an empty key would make the signature trivial to forge.

Where the receiver fits

Keep the handler thin: verify, write the job id and status to a table, return 200 fast, and do the download in a worker. A job that finishes while your server is down is not lost, because the job stays readable at GET /v1/jobs/:id/status and GET /v1/jobs/:id/result. A small sweeper that polls jobs older than a few minutes with no terminal row closes the gap.

For a 30-second motion clip that is a good fit. The render takes long enough that a poll loop wastes requests, and the delivery tells you the moment it is ready.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume