Refuse an empty Sume API key or webhook secret: a fail-fast loader
An empty SUME_API_KEY or webhook secret fails late and confusingly. Load both at boot, reject empty or whitespace values, and never print them. Python, tested.

Load SUME_API_KEY and your webhook signing secret once at boot, reject any value that is missing, empty or only whitespace, and exit before serving traffic. An empty API key produces a 401 on the first call, far from the cause, and an empty webhook secret is worse: an HMAC keyed with an empty string is computable by anyone, so a verifier that accepts it checks nothing. Sume webhooks signs deliveries with HMAC-SHA256 over the timestamp and the raw body, and that only protects you if the secret is real.
The loader is twenty lines, and it removes a whole class of silent failures from deploy day. It also gives you one place to add the next required setting, so new variables inherit the same checks instead of each being read ad hoc somewhere deep in a request handler.
How do empty secrets reach production?
Why does an empty value happen? Container platforms set a variable to an empty string when a secret reference fails to resolve. A shell export SUME_API_KEY=$(cat key) carries a trailing newline into the header. A copy from a password manager brings a space. Each of these gives you a variable that exists, so a plain os.environ["X"] succeeds and the program starts. Sume authentication describes the bearer header the key goes into, and a stray newline in it is a malformed header at best.
What does the loader look like?
import os
class ConfigError(RuntimeError):
pass
def need(env, name):
raw = env.get(name)
if raw is None or not raw.strip():
raise ConfigError(name + " is missing or empty")
value = raw.strip()
if any(ch in value for ch in "\r\n\t "):
raise ConfigError(name + " contains whitespace")
return value
def load(env=os.environ):
return {
"api_key": need(env, "SUME_API_KEY"),
"webhook_secret": need(env, "SUME_WEBHOOK_SECRET"),
}
good = {"SUME_API_KEY": "k_test\n", "SUME_WEBHOOK_SECRET": "whsec_x"}
assert load(good)["api_key"] == "k_test"
for bad in ({}, {"SUME_API_KEY": "", "SUME_WEBHOOK_SECRET": "x"},
{"SUME_API_KEY": "a b", "SUME_WEBHOOK_SECRET": "x"}):
try:
load(bad)
except ConfigError as exc:
print("rejected:", exc)
else:
raise SystemExit("accepted a bad config")What are the deliberate choices?
Two details are deliberate. First, the error names the variable and never the value, so logs stay safe. Second, the loader strips one trailing newline and then rejects interior whitespace, because a key with a space inside is a paste error, not something to repair silently. Pass env as a parameter so the test can hand it a plain dict instead of mutating the process environment.
Keep the returned dict small and pass it down explicitly; do not re-read the environment later. Call load() at the top of main, before you bind a port or start a worker. A crash loop on deploy is a loud, cheap failure, and the platform's health check will refuse to route traffic to a service that never came up.
| Bad value | Without the loader | With the loader |
|---|---|---|
| Empty API key | 401 on the first request | Boot failure naming the variable |
| Key with trailing newline | Malformed header | Stripped, then accepted |
| Empty webhook secret | Signatures verify against nothing | Boot failure |
| Whitespace inside the secret | Every signature mismatches | Boot failure |
What the docs say about the replay window
The webhook verifier has a toleranceSeconds option with a default of 300, and setting it to 0 skips the timestamp check entirely. Read it from configuration like the secret, but refuse 0 in production code: a loader that accepts any number lets a stray 0 turn the replay guard off without anyone noticing. The SDK verifier returns false on a malformed delivery rather than throwing, so one boolean decides whether to answer 401 (Sume SDK webhooks, read 2026-10-06).
If you add this setting to the loader, give it the same treatment as the secret: parse it once, reject anything that is not a positive integer, and keep the default of 300 when the variable is unset. The goal is the same as for the key and the secret, which is that a bad deploy fails at boot with a clear name and not at the first signed request.
Does the same rule apply to the SDK?
Apply the same rule in the SDK path, and in every test fixture too: a test that builds a verifier with an empty secret and expects success teaches the next developer that it is fine. The Sume SDK webhooks verifier needs the secret and the raw body, so pass it the validated value from load() and never a default. A fallback such as os.environ.get("X", "") is the line that turns a config mistake into a security hole. During rotation, Sume webhooks can send more than one signature entry for a window, so the verifier should accept any match, but each configured secret still has to pass the same non-empty check. If you rotate the secret, update the variable and redeploy; do not add a second code path that accepts anything while the new value propagates.
Sources
Related posts
More in Developers
- Save a Sume artifact atomically: write a .part file, then rename
A half-written MP4 that looks finished is worse than none. Download a Sume artifact to a .part file, check the length, rename once. Tested in Python.
- Schedule the next Ideogram 4.5 batch wave from ratelimit-reset
Size each wave from ratelimit-remaining and wave_size_hint, and when the write bucket is empty sleep ratelimit-reset seconds. A pure function you can test.
- SDK waitForJob after a 202 from createImage: TypeScript sample
When createImage returns 202 on a slow gpt-image-2.5 render, pass the job id to waitForJob from @sume-com/sdk and read the terminal job instead of hand-polling.
- Rotate the Sume webhook secret twice in 24 hours: the oldest one dies
One rotation keeps the old secret valid for 24 hours. A second rotation inside that window retires the secret from two rotations ago. Verifier in Python.
Written by Sume