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.

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.
| Duplicate source | Typical gap | Stopped by |
|---|---|---|
| Handler timeout, Sume retries | 30 seconds | Resend key (24 hour lifetime) |
| Attempts exhausted, manual redeliver the same day | Hours | Resend key (24 hour lifetime) |
| Manual redeliver after a week | Days | Your sent table |
| Two events for one job | Seconds | Dedupe 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
- Tavus MCP server and CLI vs Sume hosted MCP: auth and spend caps
Tavus MCP uses browser OAuth plus a tavus CLI; Sume hosted MCP has read-only OAuth, opt-in write, API keys, idempotency_key, dry_run and max_spend_usd.
- Theia AI MCP oauth block and ~{mcp_} tool syntax with Sume tools
Theia AI can sign in to a remote MCP server with an oauth block and call its tools as ~{mcp_server_tool}. Here is how that maps to Sume's jobs_wait and gates.
- Theia AI remote MCP server: serverUrl and serverAuthToken for Sume
Theia AI's remote MCP config takes serverUrl, serverAuthToken and an optional serverAuthTokenHeader. The entry for Sume's hosted MCP with a Bearer API key.
- TikTok catalog feed: image hosts TikTok lists, and where Sume URLs fit
TikTok's catalog page names supported and unsupported image hosts and asks for 720x1280 video. How to prepare a video_link clip with Sume trim.
Written by Sume