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.

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 statussubmittingand 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, andresult_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/statusbefore 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
| Event | Sume status | Row status |
|---|---|---|
| job.completed | completed | completed, then fetch the result |
| job.failed | failed | failed, read the job error |
| job.canceled | canceled | canceled |
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
- Webhook events arrive out of order: Sume sends one event per job
Stripe does not guarantee event order. Sume job webhooks send terminal events only, so key on job_id, dedupe, and poll status when a callback never arrives.
- Webhook receiver that queues finished SKU videos for human approval
Verify the format.run.terminal signature, dedupe on run_id, and park each finished SKU video as pending review before anything is published. Runnable Python.
- Max text length for Gemini and OpenAI TTS: the docs name none
The OpenAI TTS guide and the Gemini speech page I read state no input limit. Sume publishes 20,000 characters and 1,200 s; here is a splitter.
- What to log from a TTS job result so you can recreate a voiceover
A finished Sume TTS 1.0 job echoes model_id, voice, language, output_format and generation_config. Save them with the audio URL to rebuild the same take.
Written by Sume