Kestra webhook trigger has no HMAC: verify Sume deliveries first
Kestra's Webhook trigger is protected by its key alone. Verify Sume's sume-v1 signature in a thin relay, then start the flow, and submit with a stable key.

Do not point a Sume webhook straight at a Kestra Webhook trigger. Kestra's page for the trigger says the key in the URL is the only security mechanism protecting the endpoint (read 2026-10-04), while Sume proves a delivery with an HMAC signature that a Kestra flow never checks. Put a small receiver in between that verifies sume-v1, then calls Kestra's webhook URL, and let Kestra's Request task do the paid submit with a stable Idempotency-Key.
Sume signs the raw body with HMAC-SHA256 over <timestamp>.<raw_body> and sends x-sume-webhook-timestamp and x-sume-webhook-signature: sume-v1=<hex>, per Webhooks. The Kestra trigger exposes {{ trigger.body }} and {{ trigger.headers }}, but it has no step that recomputes that HMAC over the original bytes, and by then the body has been parsed.
What does the flow look like?
Submit with io.kestra.plugin.core.http.Request, then trigger a second flow from a verified relay. The submit flow below uses an input as the business key.
id: sume_submit_image
namespace: media
inputs:
- id: sku
type: STRING
tasks:
- id: submit
type: io.kestra.plugin.core.http.Request
uri: https://api.sume.com/v1/image-1.0/generate
method: POST
headers:
x-api-key: "{{ secret('SUME_API_KEY') }}"
Content-Type: application/json
Idempotency-Key: "hero-{{ inputs.sku }}-v1"
body: >-
{"prompt": "Product hero shot, {{ inputs.sku }}",
"mode": "webhook",
"webhook_url": "https://hooks.example.com/sume"}
- id: log_job
type: io.kestra.plugin.core.log.Log
message: "submitted {{ outputs.submit.body }}"Why a relay rather than the trigger?
The relay at hooks.example.com is about twenty lines: read the raw body, check the timestamp and signature with your signing secret (or verifyWebhook from the SDK), answer 2xx fast, then POST the verified event to Kestra's /api/v1/{tenant}/executions/webhook/{namespace}/{flowId}/{key} URL. Sume allows ten attempts with a 10-second timeout per attempt, so the relay should not wait for the Kestra execution to finish.
Which fields does each side own?
| Concern | Sume sends | Kestra Webhook trigger offers | Put it in |
|---|---|---|---|
| Authenticity | sume-v1 HMAC over timestamp.raw_body | A secret key in the URL | The relay |
| Replay window | x-sume-webhook-timestamp | Nothing | The relay (default 300 s) |
| Dedupe | job_id on job events, run_id on run events | Nothing | The relay or the flow |
| Raw bytes | The signed body | trigger.body, JSON deserialized when applicable | The relay |
What can go wrong?
- Treating the Kestra key as a signature. Anyone who learns the URL can start your flow.
- Verifying
trigger.bodyafter Kestra has deserialized it: re-serialized JSON no longer matches the signed bytes. - Using
{{ execution.id }}as theIdempotency-Key. A replayed execution gets a new id and a second paid job; derive it from the input. - Keeping the relay slow. A receiver that takes over 10 seconds burns an attempt and gets retried.
How should the relay hand over to Kestra?
Forward only what the flow needs: the event name, the job_id or run_id, the status, and the artifact URL. Do not forward the signature headers or your signing secret; the flow has no use for them and Kestra stores trigger data with the execution. Name the flow input after the dedupe key so a retried delivery that reaches the relay twice starts one meaningful execution, not two.
Branch on the event before you forward. A job webhook carries job.completed, job.failed or job.canceled; a Format run webhook carries format.run.terminal with an outcome of ok, degraded or error. Route unknown event names to a 204 instead of a 500, so a new event type never becomes a retry storm. The one verifier covers both surfaces, as the run webhooks page notes.
Where do you go from here?
Keep polling as a backup, as Webhooks recommends, and store the signing secret as SUME_COM_WEBHOOK_SIGNING_SECRET.
Sources
Related posts
More in Developers
- Kling 3 on Sume rejects reference_*_urls: which model takes references
The kling-3 row supports text-to-video and start/end frames only. Send reference_image_urls and you get a 400; here is where references do work.
- Koa receiver for Sume webhooks: verify with koa-bodyparser rawBody
koa-bodyparser keeps the raw string on ctx.request.rawBody. Check the sume-v1 HMAC against it, not against a re-stringified ctx.request.body, then answer 204.
- Korean karaoke captions: korean-ad and language ko on Sume
Burn Korean karaoke-style captions with style korean-ad and language ko on /v1/video-captions: one phrase at a time, the spoken word in a heavier weight.
- Workers KV jurisdictions: keep Sume job records in region
Cloudflare made Workers KV jurisdictions generally available on Oct 2, 2026. Here is how to key Sume job_id records into a region-scoped namespace.
Written by Sume