uv run a single-file Python script against the Sume API (PEP 723)
A one-file Sume script with inline PEP 723 dependencies runs with uv run and no virtualenv. Submit, poll next_poll_after_seconds, print the result.

Put a # /// script block at the top of one Python file, list httpx under dependencies, and run it with uv run script.py. PEP 723 (read 2026-10-10) defines that inline metadata with requires-python and dependencies, and a runner like uv builds the environment on the fly.
That makes a Sume job script one file you can paste into a ticket, a gist or a CI step, with no requirements.txt and no virtualenv to document.
The whole script
The script below submits an image job in async mode with an Idempotency-Key, then polls /v1/jobs/{id}/status. It sleeps for next_poll_after_seconds when the API sends one and for 2 seconds otherwise, stops on terminal, and prints the final status and the result body. The Python here is 3.14 because it uses uuid.uuid7(); change requires-python and use uuid4 if you must stay older.
# /// script
# requires-python = ">=3.14"
# dependencies = ["httpx"]
# ///
import os, sys, time, uuid
import httpx
def main() -> None:
headers = {"x-api-key": os.environ["SUME_API_KEY"]}
with httpx.Client(base_url="https://api.sume.com", headers=headers, timeout=30) as c:
r = c.post(
"/v1/image-1.0/generate",
json={"prompt": sys.argv[1], "mode": "async"},
headers={"Idempotency-Key": str(uuid.uuid7())},
)
r.raise_for_status()
job = r.json()["data"]["request_id"]
while True:
s = c.get(f"/v1/jobs/{job}/status").json()["data"]
if s["terminal"]:
break
time.sleep(s["next_poll_after_seconds"] or 2)
print(s["sume_status"], c.get(f"/v1/jobs/{job}/result").json())
main()What each part maps to in the Sume docs
Every line of the polling logic follows the Jobs and results page.
| Script behaviour | Sume field or rule | Why |
|---|---|---|
| Sends x-api-key | Authentication accepts Bearer or x-api-key, never both | Both headers at once returns 401 |
| Same Idempotency-Key for the submit | Key echoed as idempotency_key on the job | Lets you find the job after a crash |
| Stops on terminal | Status field terminal | Do not infer done from the status word alone |
| Sleeps next_poll_after_seconds | Hint on the status body; back off when absent | Fewer wasted reads against the read budget |
Running it and keeping the key out of the file
Export SUME_API_KEY in your shell and run uv run script.py "a bottle on marble". The script reads the key from the environment only, so the file is safe to commit. If you use a secrets manager, wrap the command in its run helper instead of pasting the key into a shell profile.
Where a single file stops being enough
A practical middle step is to keep the script and add a small JSON file next to it that stores the key for each prompt you submitted. Rerunning the script then resumes the same job instead of paying twice, which is the idempotency contract working for you at the smallest possible scale.
Once you need persistence for idempotency keys, a webhook receiver, or retries across restarts, a script is the wrong shape. The same polling loop belongs in a small module, and the key belongs in a database row. Until then, one file with pinned inline dependencies is the fastest way to get a first paid job through the API and to share a reproducible example with a teammate who has never installed your repo.
Sources
Related posts
More in Developers
- Veo 3.1 image_url rejected on Sume: text-only error and what to use
Sume's Veo rows refuse image, frame, reference and video inputs as 'text-to-video only here'. The refused fields, other messages, and rows that take an image.
- Veo 3.1 request on Sume: 720p, 4, 6 or 8 seconds, in Python
A working POST /v1/videos call for veo-3.1-lite with 6 seconds, 9:16 and no audio, the rates for the three Veo rows, and what Sume rejects.
- Vercel skipMiddlewareRequestBody and a Sume webhook route
Vercel's Oct 8 skipMiddlewareRequestBody stops sending request bodies to Routing Middleware. What it means for a Sume webhook route that signs the raw body.
- Verify a canceled Sume job was refunded: read the usage ledger by job
After you cancel a queued job, check GET /v1/usage with job_id and read the row status: reserved, captured or refunded. A Python script prints the answer.
Written by Sume