Next.js after() runs even on errors: guard Sume submits with a key
Next.js after() runs after the response, even when the handler threw. Put a Sume submit there only with an Idempotency-Key, and cap it by route maxDuration.

Next.js after() schedules work to run once the response has finished, and the docs say it also runs when an error is thrown or notFound or redirect is called. If the callback submits a paid Sume run, send an Idempotency-Key built from your own request so a replay or a retry cannot create a second run.
What does after() do in a Route Handler?
The Next.js reference (version 16.3.8) says after allows you to schedule work to execute after a response or prerender is finished, for side effects that should not block the response such as logging and analytics. It works in Server Components, Server Functions, Route Handlers and Proxy, and became stable in 15.1.0.
In Route Handlers you can call cookies and headers inside the callback. In Server Components you cannot; read them first and pass the values in.
| Question | Answer on the Next.js page |
|---|---|
| Where can it run | Server Components, Server Functions, Route Handlers, Proxy |
| How long can it run | The platform default or configured max duration of the route |
| Does it run after an error | Yes, including on throw, notFound and redirect |
| Static export | Not supported |
| Serverless mechanism | waitUntil, which extends the invocation |
Why is a Sume submit risky inside it?
Because the callback fires on failure paths too. A handler that throws after queuing after() still triggers your Sume submit, and a client that retries the failed request queues it again. Without a key that is a second paid run per retry.
The duration is also bounded: after runs for the route's default or configured max duration, set with the maxDuration route segment config where your platform supports it. Keep the submit to one short call.
What does the guarded version look like?
Derive the key from a value the client controls and that is the same on every retry, such as an order id. Sume's Format run call returns 202 for a fresh run and 200 with idempotency_hit: true for a replay with the same body, so both mean you have a run id.
Ask for a communication.webhook_url and let the signed format.run.terminal POST hit a second Route Handler, which verifies with verifyWebhook on await request.text().
import { after } from "next/server";
export async function POST(request: Request) {
const { orderId, brief } = await request.json();
after(async () => {
const res = await fetch("https://api.sume.com/v1/formats/acme/promo/runs", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SUME_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": `order-${orderId}-v1`,
},
body: JSON.stringify({
instruction: brief,
generation_spend_cap_usd: 5,
communication: { webhook_url: "https://example.com/api/sume" },
}),
});
if (res.status !== 200 && res.status !== 202) console.error("sume", res.status);
});
return Response.json({ accepted: true }, { status: 202 });
}Should the order record exist before the submit?
Yes. Write the order row first, in the handler body, so that a missed or failed after leaves evidence. A sweep that finds orders without a Sume run id can resubmit with the same key. Because replays return the original run, the sweep never double bills.
Nothing in after is retried by Next.js itself. If the platform ends the invocation, the work is gone, and only your record tells you it was never sent.
What does Sume not do?
Sume has no Next.js package that wraps after, and its TypeScript SDK covers verifyWebhook and waiting on runs rather than scheduling. Treat after as your own side effect, not a delivery guarantee, and treat the Sume key as the only thing that makes repeating it safe.
Sources
Related posts
More in Integrations
- Notion 2,000-character text limit: write Sume results back in chunks
Notion caps one text value at 2,000 characters and a request at 1,000 blocks or 500KB. Split Sume's structured output into blocks instead of one property.
- OpenHands MCP server: add Sume as a Streamable HTTP server
Add Sume's hosted MCP to OpenHands as a Streamable HTTP server with a bearer API key, and know why OAuth does not suit unattended OpenHands runs.
- Perplexity Agent API MCP tool: connect Sume with allowed_tools
Perplexity's Agent API runs every MCP tool call with no approval step. Connect Sume's hosted MCP with an allowed_tools list so a model cannot reach paid tools.
- Pinterest video Pin 400: the cover image is required, here is a frame
Creating a Pinterest video Pin needs a valid cover_image_url or you get a 400. Pull a still from your Sume avatar clip with video frames and host it publicly.
Written by Sume