Kling motion control with mode webhook: a Python verifier for rotation

Submit a Kling 3.0 motion control job with mode webhook and verify the sume-v1 signature in Python, including the two-entry header during a secret rotation.

5 min readSume
All posts

A Kling 3.0 Motion Control job can take longer than the 30-second sync wait, so submit it with mode: "webhook" and a webhook_url and let Sume call you when it ends. The delivery carries job.completed, job.failed, or job.canceled, signed with HMAC SHA 256 over <timestamp>.<raw_body>. Verify it against every sume-v1= entry in the header, because during a secret rotation the header carries two.

Keep a polling fallback on GET /v1/jobs/:id/status. Webhook mode stores a callback for terminal delivery only.

Submit the job

The webhook URL has to be public HTTPS. Sume rejects localhost, private-network, and non-HTTPS URLs. The route body is strict, so send only the documented fields.

curl -X POST https://api.sume.com/v1/kling/3.0/motion-control \
  -H "Authorization: Bearer $SUME_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: mc-hook-001" \
  -d '{"image_url": "https://example.com/mascot.png", "motion_video_url": "https://example.com/dance.mp4", "duration_seconds": 12, "mode": "webhook", "webhook_url": "https://hooks.example.com/sume"}'

What arrives

Delivery is terminal only. There are no progress events, so do not build a progress bar on the webhook.

Terminal events for a Kling motion control job in webhook mode
EventstatusBody carriesYour action
job.completedOKpayload.artifacts with a media.sume.com URLCopy the MP4 to your own storage
job.failedERRORan error objectFix the input, resubmit with a new key; the reservation is refunded
job.canceledERRORan error objectTreat as ended, no output

Verify the signature

The headers are x-sume-webhook-timestamp and x-sume-webhook-signature. Reject the delivery when the timestamp is more than five minutes from your clock. Compare each sume-v1= entry in constant time, and refuse to run at all if the secret is empty. Your secret is on the Webhooks tab of the dashboard, and the delivery worker reads it from SUME_COM_WEBHOOK_SIGNING_SECRET.

import hashlib
import hmac
import time

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

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

Handle it safely

Run webhooks for Actions, Formats, and Agent Completions share the same secret and signature scheme, so one verifier covers all of them.

  • Verify against the raw bytes, not a re-serialized JSON object.
  • Answer 2xx fast and do the copy of the MP4 in the background.
  • Make the handler idempotent on job_id; this keeps a repeated delivery harmless.
  • Keep polling GET /v1/jobs/:id/status for any job that has not reported after your own deadline.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume