Webhook before your DB row? Insert, then submit the Sume video

A Sume job webhook can beat your own write of the job id. Insert a pending row keyed by Idempotency-Key first, then upsert by job_id when the event lands.

5 min readSume
All posts

Yes, a Sume webhook can reach your server before your code has saved the job id, so write your own row first and make the webhook handler an upsert. Key the row by the Idempotency-Key you send, then attach the Sume job_id when the submit response comes back.

The risk is real for short jobs and slow writes. Submit returns a durable job id the moment Sume accepts the request, and with mode: "webhook" the callback is stored right away. If your submit call is retried, queued behind a slow commit, or your database is briefly busy, the terminal event can arrive first.

Why it happens

Sume accepts a submit as soon as it has a job id, and every mode returns that id in the first response. A 2xx means the job exists and paid work is in flight. It does not mean the job finished. The webhook is delivered when the job reaches job.completed, job.failed, or job.canceled.

For a 30-second Seedance 2.5 or Wan 3.0 render this gap is usually minutes, so a race is rare. But queued jobs, test jobs on cheap settings, and quickly canceled jobs can end sooner than you expect, and a handler that does UPDATE ... WHERE job_id = ? against a missing row silently loses the event.

The safe order

  • Generate a stable key for the clip, for example order-8823-clip-3-v1, and insert a row with status submitting and that key.
  • POST to Sume with the same value as Idempotency-Key. A retry returns the original job instead of a second charge.
  • On the response, update the row with job_id, status_url, and result_url.
  • In the webhook handler, upsert by job_id. If no row has that id yet, insert one from the event and let the submit path find it by key later.
  • Treat the event as a hint. Confirm with GET /v1/jobs/:id/status before you do anything irreversible.

The upsert

This SQL keeps the first terminal event and ignores repeats, which also covers retries and redelivers.

CREATE TABLE clip (
  idem_key text PRIMARY KEY,
  job_id   text UNIQUE,
  status   text NOT NULL DEFAULT 'submitting'
);

-- webhook handler, after the signature check:
INSERT INTO clip (idem_key, job_id, status)
VALUES ('webhook:' || :job_id, :job_id, :status)
ON CONFLICT (job_id) DO UPDATE
  SET status = EXCLUDED.status
  WHERE clip.status IN ('submitting', 'queued', 'processing');

What each outcome means

Job webhook events and the row state to write, Sume docs read 2026-10-05
EventSume statusRow status
job.completedcompletedcompleted, then fetch the result
job.failedfailedfailed, read the job error
job.canceledcanceledcanceled

Do not use a delay as the fix

Sleeping before you answer the webhook burns a 10-second attempt budget and Sume retries a slow endpoint. Return 2xx fast, store the event, and reconcile later.

If a row is still submitting after a long time, list recent jobs with GET /v1/jobs or poll the stored id. Replay the submit only with the same Idempotency-Key, which returns the original job, never a new one.

A note on cost

The reason to be careful is money. A 30-second Seedance 2.5 clip at 1080p is $42.65 on Sume, and a duplicate job from a confused retry is a second $42.65. The key-first row and the shared Idempotency-Key are what keep one logical clip as one billed job.

Test it on purpose

You can reproduce the race without waiting for a real render. Send a signed webhook.test body to your handler, or call POST /v1/jobs/{job_id}/webhook/redeliver on a finished job with a key that has jobs:write, while your own table is empty. The upsert should create the row, and the later submit response should find it by key.

That test also shows the other direction: if the submit response comes back second, your code must update the existing row rather than fail on a duplicate.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume