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.

5 min readSume
All posts

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?

Who verifies what in a Sume to Kestra chain, Sume and Kestra docs read 2026-10-04
ConcernSume sendsKestra Webhook trigger offersPut it in
Authenticitysume-v1 HMAC over timestamp.raw_bodyA secret key in the URLThe relay
Replay windowx-sume-webhook-timestampNothingThe relay (default 300 s)
Dedupejob_id on job events, run_id on run eventsNothingThe relay or the flow
Raw bytesThe signed bodytrigger.body, JSON deserialized when applicableThe relay

What can go wrong?

  • Treating the Kestra key as a signature. Anyone who learns the URL can start your flow.
  • Verifying trigger.body after Kestra has deserialized it: re-serialized JSON no longer matches the signed bytes.
  • Using {{ execution.id }} as the Idempotency-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

All Developers posts

Written by Sume