Python urllib gets 403 'error code: 1010' from api.sume.com: set a UA
Python's default urllib User-Agent got a plain-text HTTP 403 from api.sume.com in my test while other clients passed. Add a User-Agent and parse errors safely.

If a urllib.request call to https://api.sume.com returns 403 Forbidden and the body is the plain text error code: 1010, add a User-Agent header. In my test on 2026-10-10 the default Python-urllib agent was refused, and the same request with any custom agent reached the API and got its normal JSON 401.
This is something I measured, not something the docs describe, so treat it as an observation that can change. The Sume errors page documents JSON error bodies with a request_id; the 403 below does not have one.
What I tested
I sent GET /v1/me with a deliberately invalid key, so the expected answer is 401 unauthorized from Sume. Only the User-Agent changed between runs. A blocked client never reaches the key check, so the 403 is not about your key.
| User-Agent sent | HTTP status | Reached Sume's key check |
|---|---|---|
| Python-urllib/3.14 (the stdlib default) | 403 | No |
| my-app/1.0 (custom, urllib) | 401 | Yes |
| python-requests/2.32.0 | 401 | Yes |
| python-httpx/0.27.0 | 401 | Yes |
| Go-http-client/1.1 | 401 | Yes |
| axios/1.7.0, okhttp/4.12.0, curl/8.7.1 | 401 | Yes |
Why this bites in scripts
Most examples for a REST API in Python use requests or httpx, which send their own agent string and passed in my test. The failure shows up when someone writes a dependency-free script, as in a CI job or a minimal container, with only urllib.request. The unauthenticated GET /v1/health and /v1/catalog calls also returned 403 with the default agent, so a health probe written this way will look like an outage.
The second trap is the error handler. The 403 body is not JSON, so json.load(e) raises a JSONDecodeError that hides the real status. Read the body as text first, then try to parse it, as the sample does.
import json, os, urllib.error, urllib.request
def sume_get(path):
req = urllib.request.Request(
"https://api.sume.com" + path,
headers={"x-api-key": os.environ["SUME_API_KEY"],
"User-Agent": "my-service/1.0 (ops@example.com)"})
try:
with urllib.request.urlopen(req, timeout=30) as r:
return json.load(r)["data"]
except urllib.error.HTTPError as e:
raw = e.read().decode("utf-8", "replace")
try:
err = json.loads(raw)["error"]
except ValueError:
raise RuntimeError(f"HTTP {e.code} with a non-JSON body: {raw[:60]!r}") from e
raise RuntimeError(f"{e.code} {err['code']} request_id={err['request_id']}") from e
print(sume_get("/v1/me")["account"]["api_key"]["prefix"])What to put in the header
Use a stable string that names your service and a contact, for example my-service/1.0 (ops@example.com). It costs nothing, and it gives anyone reading edge logs a way to find you. Do not impersonate a browser agent to get around a block; if a custom agent is ever refused too, report it to Sume with the time and the source IP.
Remember the other header rule while you are there: send exactly one of x-api-key or Authorization: Bearer. Sending both returns 401 unauthorized with Send only one API key credential., per the Authentication page.
- Check the body type before parsing; a non-JSON error did not come from the Sume API handler.
- Log the status, the first 60 characters of the body, and your agent string.
- Add the header once in a helper, not at every call site.
Sources
Related posts
More in Developers
- Recover Sume jobs after a crash: match your key to GET /v1/jobs
Your worker died after submit and lost the job ids. Page GET /v1/jobs, match idempotency_key to your own keys, stop at the last page. A 29-line sample.
- Silent reference clip: stt_skipped_silent, no-audio and caption errors
A silent clip does not fail a Sume reference ingest, and STT settles to zero. Video inspect and captions do raise errors on silence.
- Retry or not: a decision table for failed Sume submit calls
Which Sume errors deserve a retry with the same Idempotency-Key, which mean poll the job, and which mean fix the request. One table and a 15-line classifier.
- Revoked a Sume API key and it still works? Up to 15 seconds
After you revoke a Sume API key, the API can keep accepting it for up to 15 seconds by default, because the key lookup is cached. What that means for a leak.
Written by Sume