Email on Sume job done: Resend with a job-keyed Idempotency-Key

Verify the Sume webhook with the SDK, then send via Resend keyed sume-<job_id>. Resend keys expire after 24 hours, so keep your own sent table too.

4 min readSume
All posts

To email a user when a Sume job finishes, verify the delivery with verifyWebhook from @sume-com/sdk, then call Resend with an Idempotency-Key of sume-<job_id>. A redelivery of the same Sume event then cannot send a second email, as long as it arrives within Resend's key lifetime of 24 hours.

After 24 hours the Resend key is gone, and a manual Sume redeliver later would send again. Close that gap with a small table of job ids you have already emailed.

Two idempotency windows, side by side

Sume retries a failed delivery up to 10 attempts, 30 seconds apart, and you can ask for a redelivery of a job at any time, even after the attempts are used up. That means the same job_id can arrive minutes or weeks apart. Resend's send-email reference says the idempotency key expires after 24 hours and may be up to 256 characters, so the job id fits easily but the window is short.

So there are two layers. The Resend key protects against the common case, a quick retry after your handler timed out. The sent table protects against the rare one, a manual redeliver for a job emailed last week. Both are cheap; only the first is free of a database.

Which layer stops which duplicate (read 2026-10-03)
Duplicate sourceTypical gapStopped by
Handler timeout, Sume retries30 secondsResend key (24 hour lifetime)
Attempts exhausted, manual redeliver the same dayHoursResend key (24 hour lifetime)
Manual redeliver after a weekDaysYour sent table
Two events for one jobSecondsDedupe on job_id

The route

The handler refuses to run without a signing secret, uses the SDK for verification and sends only a link, which keeps the message small and well under Resend's attachment limit.

import { verifyWebhook } from "@sume-com/sdk";

export async function POST(request: Request) {
  const secret = process.env.SUME_COM_WEBHOOK_SIGNING_SECRET;
  if (!secret) return new Response("not configured", { status: 500 });
  const body = await request.text();
  if (!(await verifyWebhook({ body, headers: request.headers, secret }))) {
    return new Response("bad signature", { status: 401 });
  }
  const evt = JSON.parse(body);
  if (evt.event !== "job.completed") return new Response(null, { status: 204 });
  const url = evt.payload.artifacts[0].url;
  const res = await fetch("https://api.resend.com/emails", {
    method: "POST",
    headers: { Authorization: `Bearer ${process.env.RESEND_API_KEY}`, "Content-Type": "application/json",
               "Idempotency-Key": `sume-${evt.job_id}` },
    body: JSON.stringify({ from: "Studio <hi@example.com>", to: ["user@example.com"],
      subject: "Your render is ready", html: `<p><a href="${url}">Open your render</a></p>` }),
  });
  return new Response(null, { status: res.ok ? 204 : 502 });
}

Failure behavior

A non-2xx from Resend becomes a 502 from your route, which makes Sume retry in 30 seconds with the same job id and so the same Resend key. That is the behavior you want for a transient Resend outage. For a permanent error, such as an unverified sender domain, log it and return 204; retrying ten times will not help. Record the result artifact URL in your own table too, so the email is not the only copy.

Sources

Related posts

More in Integrations

All Integrations posts

Written by Sume