createSumeClient sends x-api-key only: an extra header gives 401
The Sume API accepts Bearer or x-api-key but rejects both at once with 401. A fetch wrapper for createSumeClient that drops the extra header, tested offline.

createSumeClient sends the x-api-key header and nothing else. If a gateway, proxy, or interceptor also adds Authorization: Bearer ..., the Sume API fails the request with 401 unauthorized and the message Send only one API key credential. There is no precedence rule; neither header wins. Remove one of them in a custom fetch wrapper.
What the API accepts
The Authentication and SDK pages agree on the rules.
| Request carries | Outcome |
|---|---|
| Authorization: Bearer only | Accepted |
| x-api-key only | Accepted |
| Both | 401 unauthorized: Send only one API key credential. |
| Neither, or a revoked key | 401 unauthorized |
A wrapper that keeps one
The SDK's fetch option lets you supply your own function. This wrapper deletes Authorization whenever x-api-key is present. The block at the bottom checks the wrapper offline with a fake fetch, so it prints false true without calling Sume. I ran it with Bun.
import { createSumeClient } from "@sume-com/sdk";
// Some gateways add `Authorization` to every outgoing request. The SDK already
// sends `x-api-key`, and the API rejects both together with 401.
const oneCredential: typeof fetch = (input, init) => {
const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined));
if (headers.has("x-api-key")) headers.delete("authorization");
return globalThis.fetch(input, { ...init, headers });
};
export const client = createSumeClient({
apiKey: process.env.SUME_API_KEY ?? "missing",
fetch: oneCredential,
});
// Offline check of the wrapper itself:
const seen: Headers[] = [];
globalThis.fetch = (async (_i: RequestInfo | URL, init?: RequestInit) => {
seen.push(new Headers(init?.headers));
return new Response("{}");
}) as typeof fetch;
const sent = new Headers({ "x-api-key": "k", Authorization: "Bearer other" });
await oneCredential("https://api.sume.com/v1/me", { headers: sent });
console.log(seen[0].has("authorization"), seen[0].has("x-api-key")); // false trueWhere the extra header comes from
The docs name the usual sources: a session token, the credential of a gateway, or an interceptor that you forgot. The problem shows only on a wrapped client, which is why a plain curl with the same key works and the app does not.
If you do not use the SDK, pick one header and use it consistently. Keep the key on the server. There is no browser-safe variant, so never put it in client JavaScript or a NEXT_PUBLIC_* variable; call your own endpoint, which attaches the key.
Debugging checklist
When a call works from curl and fails from the app, print the outgoing headers at the last hop. Look for Authorization added by an HTTP client default, a corporate proxy, or an SDK for another service sharing the same fetch.
The error text is explicit, so match on it in logs. A 401 with a different message means the key is missing, revoked or malformed.
Sources
Related posts
More in Developers
- curl -w http_code: branch a Sume video submit in bash on 202, 402, 429
A bash submit that saves the body, reads the HTTP code with curl -w and branches on 202, 402, 429 and 5xx. Three Wan 3.0 payloads cost $1.875, $3.75 and $7.50.
- Cut dead air before the first word: STT start time, $0.01 split
Read words[0].start from a Sume STT job, then split the recording from that second for $0.01. For a 3-minute take the whole fix is $0.04. A copyable request.
- Deno fetch: 25 s Wan 3.0 clip costs $3.125, 20-minute deadline
Deno script: submit a 25-second Wan 3.0 clip to Sume at 720p ($3.125), poll with backoff, stop at a 20-minute deadline. Run with deno run -A.
- Deno.serve webhook receiver for Sume jobs: Web Crypto HMAC, rotation
Verify Sume job webhooks in Deno with Web Crypto: refuse an empty secret, accept any rotation entry, 300 s tolerance, return 204. Retry window is 370 s.
Written by Sume