Sume 400: Idempotency-Key must be 255 printable characters or less
The Sume API rejects an Idempotency-Key over 255 characters or with control characters. Why it happens (JSON blobs, newlines) and a TypeScript key builder.

The error Idempotency-Key must be 255 printable characters or less. is a 400 invalid_request from the Sume API. It has two causes: the key is longer than 255 characters, or it contains a control character such as a newline, a tab or a null byte. Both are caught before the job is created, so nothing is spent.
The rule
The jobs documentation gives the valid range as 1 to 255 characters. The API trims whitespace at both ends first and then checks the length and the control characters (the range from \u0000 to \u001f, plus \u007f). Interior spaces and normal punctuation are allowed.
Where long keys come from
- A key made from the whole prompt text, which can run to thousands of characters.
- A key made from a serialized request body, with newlines from pretty printing.
- A key read from a file or a secret store, where a trailing line feed is common. Trimming removes that one, but a newline inside the value still fails.
The fix
Do not put content in the key. Put a digest of it there. A SHA-256 hex digest is 64 characters and has no control characters, so it is always valid. Prefix it with a short readable namespace if you want to find the job in a log.
| Input | Result |
|---|---|
| No header, or blank after trim | Accepted, no idempotency |
| 1 to 255 printable characters | Accepted |
| More than 255 characters | 400 invalid_request |
| A control character inside | 400 invalid_request |
TypeScript builder
It uses Web Crypto, which is available in Node 18 and later, Bun, Deno and Workers.
export async function idempotencyKey(namespace: string, work: unknown): Promise<string> {
const text = `${namespace}\n${JSON.stringify(work)}`;
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text));
const hex = [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
const key = `${namespace.slice(0, 40)}-${hex}`;
if (key.length > 255 || /[\u0000-\u001f\u007f]/.test(key)) throw new Error("bad idempotency key");
return key;
}
console.log((await idempotencyKey("batch-1", { sku: "A-100", prompt: "x".repeat(5000) })).length); // 72One more rule
A retry must reuse the exact same key. If you rebuild the key from a prompt that you trimmed or reformatted between attempts, the key changes and the replay protection is lost. Compute it once, store it with the job record, and send that stored value every time.
Sources
Related posts
More in Developers
- Sume 429 body: parse details.scope and window_seconds in TypeScript
A Sume rate_limited 429 names the read or write bucket and gives limit, window_seconds and retry_after_seconds. Parse them in TypeScript and wait.
- Sume 429 retry with a deadline: give up when retry-after is too long
Retrying a Sume 429 forever blocks a request. Cap the total wait, give up when retry-after exceeds what is left, then surface the error. TypeScript wrapper.
- Sume API 503 codes: which ones are retryable and which are not
Four 503 responses look alike but differ in the retryable flag: deploy_draining, database_busy, provider_capacity_exceeded and any _not_configured code.
- Sume API 503 deploy_draining: retry after 5 seconds in Python
A redeploy answers 503 deploy_draining with retry-after. Why it is safe to retry, why a POST still needs an Idempotency-Key, and a stdlib Python retry loop.
Written by Sume