Firestore create() with job_id as document id for Sume webhooks
Name the Firestore document after the Sume job_id and call create(). ALREADY_EXISTS marks a retry. Node Admin SDK sample, plus a claim state for crashes.

Use the Sume job_id as the Firestore document id and write with create(), not set(). create() fails with ALREADY_EXISTS (gRPC code 6) when the document exists, so the first delivery succeeds and every retry is rejected by the database itself. Return 2xx in both cases, and start follow-up work only when the create succeeded.
That matches the rule in Sume's webhook docs: job_id is the idempotency key. Delivery makes up to 10 attempts, and Redeliver re-sends the real terminal event on request, so duplicates are normal and not an error.
The write
Job ids look like job_..., which are valid document ids. The sample stores the event as a JSON string so that nested payload.artifacts fields do not turn into queryable noise, and it records a status you can index.
import { initializeApp } from "firebase-admin/app";
import { getFirestore } from "firebase-admin/firestore";
initializeApp();
const jobs = getFirestore().collection("sumeJobs");
export async function firstDelivery(event) {
if (!event.job_id) return false; // webhook.test has no job_id
try {
await jobs.doc(event.job_id).create({
event: event.event,
status: event.status,
body: JSON.stringify(event),
state: "claimed",
receivedAt: new Date(),
});
return true;
} catch (err) {
if (err.code === 6) return false; // ALREADY_EXISTS: retry or Redeliver
throw err;
}
}
export const markDone = (jobId) => jobs.doc(jobId).update({ state: "done" });What each branch returns to Sume
The document is the lock, so the order of operations decides what survives a crash. Create first, respond second, work third, and flip state last.
| Situation | firstDelivery() | HTTP answer |
|---|---|---|
| New job_id | true | 204 after the create succeeds |
| Retry of a delivered event | false | 204, no new work |
| Redeliver of a finished job | false | 204, no new work |
| Firestore unavailable | throws | 500 so Sume retries |
| Bad or missing signature | not called | 401, before any write |
Recover from crashes and missed deliveries
A webhook can fail ten times while the job still reached its real terminal state, and the jobs docs say to keep status_url polling available for that case. A scheduled function that lists claimed documents older than a few minutes, and a second one that polls jobs you expected but never saw, covers both crash and missed-delivery cases.
Failed and canceled jobs deliver on the same route with status: "ERROR" and an error object. They create a document like any other, so a failed render shows up in your data instead of vanishing.
Caveats
- Verify the signature on the raw body before the
create()call. A rejected body must not leave a document behind. - A document id cannot contain a slash. Sume job ids do not, but never build an id from user input.
- Each attempt has a 10 second budget at Sume. Keep the handler to verify, create and respond.
Sources
Related posts
More in Developers
- Fit a voiceover to a 30-second slot with Sume TTS speed
Measure the first take, divide by the slot length, and set generation_config.speed (0.6 to 1.5). Why a big speed-up is better solved by cutting the script.
- Fit narration to a fixed slot: measure first, then set TTS speed
A 45-second cap or a 30-second slot decides your script. Render once with word timings, compute the speed ratio, and rewrite only if outside 0.6 to 1.5.
- Flare draft, Sunburst final: a two-pass image edit loop on Sume
Find the edit on GPT Image 2.5 Flare at low quality, then send the same request to Sunburst at high for the keeper. Costs and limits from Sume docs.
- format_not_forkable 409 in the Sume API: what it means and the fix
Sume returns 409 format_not_forkable when the id you called names a built-in capability, not a Format card. How to tell, and which ids to call instead.
Written by Sume