Zod schema for a Sume job webhook: a discriminated union on status
Model Sume's job webhook in Zod as a union on status OK and ERROR so TypeScript narrows to artifacts or error. Verify the signature first, then parse.

A Sume job webhook has two shapes. A completed job has status: "OK" and a payload.artifacts array. A failed or canceled job has status: "ERROR", a null payload and an error object. The request_id and job_id fields hold the same job id on both. In TypeScript that is a textbook discriminated union, and Zod can enforce it at the edge of your service so that handlers never see a half-formed event.
Order matters. Verify the HMAC signature against the raw bytes first, then parse the JSON, then validate it. Validation is a second line of defence for your own code, not a substitute for the signature check.
The shapes
| Field | job.completed | job.failed or job.canceled |
|---|---|---|
| status | OK | ERROR |
| payload | object with artifacts[] | null |
| artifacts[] | id, url, type, content_type | not present |
| error | absent | object with code and, on public errors, retryable and next_action |
The schema
The union below discriminates on status. The OK branch requires at least one artifact whose URL is HTTPS. The ERROR branch requires an error object with a code and lets other fields through with passthrough, since the API may add fields later and a strict schema would turn a harmless addition into a rejected delivery.
import { z } from "zod";
const Artifact = z.object({
id: z.string(),
url: z.string().startsWith("https://"),
type: z.string(),
content_type: z.string(),
});
const Base = { request_id: z.string(), job_id: z.string() };
export const SumeJobWebhook = z.discriminatedUnion("status", [
z.object({
...Base,
event: z.literal("job.completed"),
status: z.literal("OK"),
payload: z.object({ artifacts: z.array(Artifact).min(1) }),
}),
z.object({
...Base,
event: z.enum(["job.failed", "job.canceled"]),
status: z.literal("ERROR"),
payload: z.null(),
error: z.object({
code: z.string(),
retryable: z.boolean().optional(),
next_action: z.string().optional(),
}).passthrough(),
}),
]);Add this helper to the same file to narrow the parsed event. After the status check TypeScript knows which branch it holds, so artifacts and error are never both in play.
export function describe(body: unknown): string {
const r = SumeJobWebhook.safeParse(body);
if (!r.success) return "unrecognised: " + r.error.issues[0]?.path.join(".");
const e = r.data;
return e.status === "OK" ? e.payload.artifacts[0].url : `${e.event} ${e.error.code}`;
}Try it
Append this to the same file and run it with Bun, or with a TypeScript runner after installing zod. It prints the artifact URL for the completed event, job.failed media_invalid for the failed one, and an unrecognised line with the path to the first issue for an event whose artifacts array is empty. The media_invalid code is a placeholder, so use real codes from your logs.
const ok = { event: "job.completed", request_id: "job_1", job_id: "job_1", status: "OK",
payload: { artifacts: [{ id: "a1", url: "https://example.com/v.mp4", type: "video", content_type: "video/mp4" }] } };
const bad = { event: "job.failed", request_id: "job_2", job_id: "job_2", status: "ERROR",
payload: null, error: { code: "media_invalid", retryable: false } };
console.log(describe(ok));
console.log(describe(bad));
console.log(describe({ ...ok, payload: { artifacts: [] } }));Choices worth making on purpose
- Return 2xx for a delivery that verifies but does not parse, and alert on it. Any non-2xx is retried, up to 10 attempts, and a body that will never parse will fail every time.
- Keep
passthroughon the error object. The public error carries more fields than the schema names. - Do not parse
request_idinto a different type. It is the job id, and you will want it as the key for deduplication. - Store
job_idandeventas a pair. A job can reach a delivery more than once, such as after a manual redeliver.
The payload and header rules are in the webhook guide, and the error fields are in the error reference. Zod describes unions on its API page.
Sources
Related posts
More in Developers
- Zod z.toJSONSchema io: "input" for a Sume request body schema
Zod's z.toJSONSchema outputs the output type by default. Pass io: "input" to describe what a client may send, and note which Zod types cannot be represented.
- "1080p is not supported by model": which Sume video ids take 1080p
Sume refuses 1080p on rows outside its allowlist, even kling-3 whose catalog lists it. Which ids accept 1080p, which do not, and a client fallback.
- 19-second AI video with references: Wan 3.0 limits on Sume
A 19 s reference-to-video job on Wan 3.0 allows 10 images, 5 videos and 5 audio clips, with video and audio each capped at 15 s total. Price for 19 s.
- 22-second AI video from a first and last frame on Sume
A 22 s clip that must land on a given last frame can use Wan 3.0 or Seedance 2.5 on Sume, the two end-frame models that run past 15 s.
Written by Sume