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.

4 min readSume
All posts

Yes: if you store Sume job records in Workers KV and need them kept inside a region, use a jurisdiction-scoped namespace and key each record by job_id. Cloudflare's changelog says jurisdictions for Workers KV namespaces became generally available on Oct 2, 2026.

What changed

The Cloudflare entry says that when you create a namespace you can set a jurisdiction so the namespace's data is only durably stored within that region. This post does not restate which jurisdictions exist or how to create one; read the changelog entry and the linked Cloudflare docs for that. What matters for a Sume integration is where the data you hold about a job will live.

What Sume sends you

A job webhook is a terminal event. Sume documents job.completed, job.failed and job.canceled, and the payload carries request_id, job_id, status and, on success, payload.artifacts[] with id, url, type and content_type. Media URLs point at https://media.sume.com/artifacts/....

What to store

The Sume docs tell receivers to treat job_id as the idempotency key. That is also the natural KV key. Store only what you need to answer your own users: the job id, the status, and the artifact ids and URLs.

Keep in mind that the artifact itself stays on Sume's media host. A jurisdiction on your KV namespace governs your record of the job, not where Sume stores the file. Do not describe it to your customers as moving the media.

  • Key: job:<job_id>
  • Value: status, artifact ids, artifact URLs, the timestamp you received the event
  • Never store: your API key, your webhook signing secret, signed URLs copied into logs

Receiver sketch

Verify the signature on the raw body first, then write the record, then answer 2xx. Sume retries up to 10 times with a 10 second timeout per attempt, so answer quickly and do slow work afterwards.

async function verify(raw, headers, secret) {
  if (!secret) return false;
  const ts = headers.get("x-sume-webhook-timestamp") ?? "";
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret),
    { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
  const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(`${ts}.${raw}`));
  const hex = [...new Uint8Array(sig)].map((b) => b.toString(16).padStart(2, "0")).join("");
  return (headers.get("x-sume-webhook-signature") ?? "").split(",").some((e) => e.trim() === `sume-v1=${hex}`);
}

export default {
  async fetch(request, env) {
    const raw = await request.text();
    if (!(await verify(raw, request.headers, env.SUME_COM_WEBHOOK_SIGNING_SECRET))) {
      return new Response("bad signature", { status: 401 });
    }
    const evt = JSON.parse(raw);
    await env.JOBS.put(`job:${evt.job_id}`, JSON.stringify({
      status: evt.status,
      artifacts: evt.payload?.artifacts ?? [],
      received_at: Date.now(),
    }));
    return new Response("ok");
  },
};

Facts at a glance

Facts used in this post (read 2026-10-03)
ItemFactSource
Cloudflare changeWorkers KV namespace jurisdictions generally available, Oct 2, 2026Cloudflare changelog
Sume eventsjob.completed, job.failed, job.canceledSume webhook docs
Idempotency keyjob_idSume webhook docs
RetriesUp to 10 attempts, 10 s timeout eachSume webhook docs

Keep a polling fallback

Delivery is an optimization. If all ten attempts fail, the job still finished on Sume's side, so keep GET /v1/jobs/{id}/status available to fill gaps in your KV table.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume