craterview
Python client for the CraterView image enhancement and restoration API.
pip install craterview
Python 3.9 or newer. Three dependencies: requests for the API, and Pillow with pi-heif to
read an image's size from its header — nothing is decoded — so a job's wait can be estimated
for the picture you actually send, HEIC included.
Quickstart
from craterview import CraterView
cv = CraterView(api_key="cv_...")
result = cv.run("photo.jpg", scale=4)
result.save("photo-4x.jpg")
run() collapses the upload → submit → poll sequence into one call. That sequence is the
API's shape rather than an accident, but it is not something every caller should have to
reimplement.
Build against echo first. It costs no credits, needs no GPU and returns a real result — a
plain upscale — so the request, the parameters and the response are the ones a paid model
gives, and your integration needs no change when you switch. When you are ready, swap the
model name for the one you want.
job = cv.run(
# echo is free, for building against. Swap in a paid model when you are ready —
# cv-enhance-v3 to enlarge, cv-restore-v1 to repair, cv-headshot-v1 for portraits.
"image.jpg", model="echo",
scale=2, wait=30,
)
job.save("image-2x.jpg")
A parameter one model publishes is not one another accepts — cv.models() says which — and
a value a model does not publish is refused at submit rather than ignored.
An API key
Keys begin with cv_ and are issued from your dashboard. Pass one to the constructor; every
call but the model catalogue needs one.
cv = CraterView(api_key=os.environ["CV_API_KEY"])
A key carries your whole allowance and does not expire, so keep it server-side. It is a credential for programs; it is not a login.
The one call
job = cv.run(
image, # path, str, bytes, or a file object
model="cv-enhance-v3", # default
wait=30.0, # seconds to hold the connection open
timeout=600, # total seconds before giving up
raise_on_failure=True,
**params, # model parameters, e.g. scale=4
)
It waits server-side first, so a job that finishes quickly costs a single round trip, then
falls back to polling for anything slower. With raise_on_failure=False a failed job is
returned rather than raised, and job.error says why.
Which parameters a model accepts is published by the API rather than hard-coded here — see
cv.models().
The explicit path
Useful when you want to hold the key, submit later, or fan out.
input_key = cv.upload("photo.jpg") # → "inputs/..."
job = cv.submit("cv-enhance-v3", input_key, scale=4)
job = cv.wait_for(job, timeout=600)
data = job.bytes()
Image bytes never pass through the API. upload() PUTs them straight to object storage on
a presigned URL and bytes() GETs the result the same way; the API moves keys, not pixels.
Retries are yours to write
This client does not retry. That is deliberate: only the caller knows where one logical request ends and the next begins, and a retry the client invented would be a second job you are billed for.
What it gives you instead is a key generator. Make one key per logical request and reuse it for every attempt at that request:
from craterview import new_idempotency_key
key = new_idempotency_key() # once, before the first attempt
for attempt in range(3):
try:
job = cv.submit("cv-enhance-v3", input_key, idempotency_key=key, scale=4)
break
except (ConnectionError, TimeoutError):
continue # same key, so at most one job is ever created
The two it catches are ConnectionFailed and TimedOut, raised when a request got no
answer; each is also the builtin the loop names. An answer from the API, even a refusal, is
not a reason to send the request again.
Generating a fresh key per attempt defeats the point entirely — the server has nothing to match against and every attempt starts its own job. Reusing a key with a different body is rejected with 409 rather than quietly handing back the earlier result. Claims are kept for as long as the job's record is, which is not deleted; the same key always returns the same job.
Errors
Everything raised by this client descends from CraterViewError, so one except catches
the lot. .status carries the HTTP status where there was one, and .trace_id the API's
X-Trace-Id for a request it answered and refused — the id to quote when you ask us about
it.
| Exception | Meaning |
|---|---|
RateLimited |
429 — one of five limits, and the message says which: the key's request rate, the account's upload URLs a minute, the account's new keys an hour, the account's jobs in flight, or a full queue. .retry_after is seconds to wait: until the window rolls over for the two per-minute limits, until a new key can be made for the hourly one, or an interval to poll on for the other two. |
JobFailed |
The job ran and did not succeed. .args[0] says what you can do about it; .error_code is the half to branch on. |
ConnectionFailed |
The request never reached the API or storage: refused, reset, or a name that would not resolve. Also a builtin ConnectionError. Safe to repeat with the same idempotency key. |
TimedOut |
No answer within the client's timeout. Also a builtin TimeoutError. The request may have arrived, so repeat a submission with the same idempotency key, never a new one. |
CraterViewError |
Everything else, including 4xx and 5xx from the API, and storage refusing an upload or a download. |
Job
Every field the API publishes on a job is exposed here.
id, model, status |
status is one of queued, running, succeeded, failed |
created_at |
Timezone-aware datetime |
succeeded, done |
done covers both terminal states |
error |
Set when the job failed. Safe to show a user |
error_code |
The same fact, as a stable identifier. Branch on this, show the other |
credits |
What you were billed |
eta_seconds |
Seconds until the job is expected to finish, recomputed on every read — it counts down while the job runs. Absent once the job has settled. Estimated for your image's size when upload() could read it — its header is read with Pillow, nothing is decoded — or when you pass input_megapixels to submit(); for a typical image otherwise |
community |
True when the job is on the community queue, which runs on shared, free capacity and can wait longer at busy times |
output_url, download_url |
The result, presigned. One to display, one to save |
thumbnail_url |
A small JPEG of the job's picture, for listings — the result, or what the model worked from when it produced no file. Null when none was drawn |
input_url |
The picture the model worked from — the region, where you named one |
alpha_url |
Only for a JPEG result of a picture with transparency, which JPEG cannot hold: the transparency as a grayscale JPEG, white where opaque. The result is then the colour alone |
output_content_type |
The result's media type |
output_bytes |
The result's size in bytes |
bytes(), save(path) |
Download the result |
result is the whole of what the job produced, and the rows above it that describe the
result's file are accessors onto result["output"] rather than separate fields — the API
states those links once. A model with no file to hand back returns its answer in result and
leaves every one of them null; the fields it answers with are its result_schema in
cv.models().
credits is the only figure about cost the API states, and the price is fixed and published
per model, so an invoice reconciles against credits alone. For how long a job took, subtract
created_at from finished_at — that is the wait you had, queue included.
Uploads declare their size. upload() and run() measure the bytes they are about
to send and declare that figure; the API signs it into the presigned URL, so storage accepts
exactly that length and nothing else. You do not pass it — it is read off the data in hand,
which is what makes it impossible to get wrong.
Running out of credit does not stop you. A job submitted against a balance of zero is
accepted and run, and charged when it succeeds — it goes to the community queue, which runs
on shared, free capacity and can wait longer at busy times. It comes back with community
set. There is no payment error to handle: paying — with credit, or with a subscription —
moves work onto paid compute that scales with demand, rather than buying the right to
submit.
eta_seconds covers the whole wait, queue time included, so a community job simply
reports a longer one.
output_url and download_url are the same object signed two ways; the content
disposition is signed in, so the second cannot be derived from the first. Both expire, so
fetch the result rather than storing the link.
thumbnail_url and input_url are for building a job listing: a few-hundred-pixel preview so a
page of jobs costs kilobytes, and the picture the model worked from so a result can be
shown against it. Where you named a region, input_url is that region — so a before-and-after
is a true pair, and what was used is something you can look at rather than something to take
on trust. It is not the file you uploaded: yours stays yours and is removed on its own
schedule. Both expire with the job's other pictures. A model that produces no file has no
result to preview, so thumbnail_url is a preview of the picture it answered about.
Webhooks
Pass webhook_url on submit — an absolute http or https URL, refused at submit if it is
not one — and we POST to it once the job settles. Verify every delivery before you act on
it: anyone who learns your URL can send you a plausible-looking body.
from craterview import CraterView, verify_webhook, InvalidSignature
secret = CraterView(api_key="cv_...").webhook_secret() # created the first time you ask
@app.post("/hooks/craterview")
def hook(request):
try:
event = verify_webhook(request.body, request.headers, secret)
except InvalidSignature:
return 400 # not from us; do nothing at all
# Answer now and work afterwards. A slow endpoint is retried while it is still
# working, so treat deliveries as at-least-once and make this idempotent on `id`.
enqueue(event["id"])
return 200
Every copy of a delivery states the same thing, so the first one you accept is the whole
answer — later copies of an id you have already handled can be dropped rather than
reconciled.
Pass the raw bytes. Most frameworks parse JSON for you, and re-serializing it changes the
bytes the signature was computed over. In Flask that is request.get_data(), in Django
request.body, in FastAPI await request.body().
The body carries the same fields a job read does, by the same names — including result and
the links inside it. The one difference is what happens to a field that does not apply: a
callback states every key and sets it null, where a job read leaves it out. So a subscriber
parses one shape and never has to ask which keys arrived.
The links expire like any others. A delivery normally arrives within seconds of the job settling and they are good for an hour, but a subscriber that queues work for later should read the job again rather than storing them.
Rotating the secret
new = cv.rotate_webhook_secret()
The old secret keeps being sent alongside the new one for a day, and verify_webhook accepts
either — so deploy the new one whenever suits rather than the moment you asked for it.
Managing keys
cv.keys() # every key on the account, by prefix; revoked ones stay listed
cv.create_key("staging") # the value is in this response and nowhere else
cv.revoke_key("key_...") # immediate, and not reversible
Several keys on one account is the ordinary arrangement — one per service or environment, so revoking one leaves the others working. They share the account's credits and history.
To rotate without downtime: create the new key, move your clients onto it, then revoke the old one. You cannot revoke the key you are calling with.
Everything else
cv.models() # models, what each takes and answers with, and which queue you are on
cv.jobs(limit=50, status="succeeded") # your history, newest first, paging transparently
cv.job("job_...") # one job by id
cv.usage() # credit balance, spend and job counts
jobs() is a generator and pages for you. limit is how many jobs one request fetches, not
how many you get: it keeps going until your history runs out. 200 is the largest page the
API serves, and a bigger number is fetched as 200.
Configuration
CraterView(
api_key=None,
base_url="https://api.craterview.ai",
timeout=60.0, # per-request, seconds
)
base_url is what you change to point at a local server.
From Claude Code
This repository is also a Claude Code marketplace. Two plugins: craterview connects
CraterView's hosted tools so the assistant enhances images in the conversation, and
craterview-api teaches an agent to call the API from code with this client. See
plugins/ for what each does and how it finds a key.
/plugin marketplace add craterviewai/craterview-python
/plugin install craterview@craterviewai
/plugin install craterview-api@craterviewai
Versioning
Semver, from 0.1.0 on. A release that removes a public name, renames an attribute on Job
or changes the type of one, gets a minor bump while this is 0.x and a major bump after
1.0.0; anything purely additive gets a patch. Pin what you depend on.
The API this wraps adds fields to its responses without warning, so treat an unfamiliar key in
result or a status you do not recognize as something to ignore rather than to fail on.
License
Apache-2.0. See LICENSE.
Metadata
Release files for craterview 0.4.11
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| craterview-0.4.11.tar.gz | 29.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| craterview-0.4.11-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 54.3 kB
Release files / craterview-0.4.11.tar.gz
| Download URL | craterview-0.4.11.tar.gz |
|---|---|
| Size | 29.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c6279f840919fe475f4e1d503cf4e327f6b7d06ec2a78d5c09d8c423313cd17b
|
|
BLAKE2b-256 checksum How to use checksums |
fde8282f1bf0c0fa08f75e837bd6a7c2dcea79de90a63e9f5d580a7e2447bc40
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 5, 2026.
Transparency logRelease files / craterview-0.4.11-py3-none-any.whl
| Download URL | craterview-0.4.11-py3-none-any.whl |
|---|---|
| Size | 24.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
4f7b3f611f78514ff8464278536b6d63b1bb8c4edfdbcc504e1be42b8880d249
|
|
BLAKE2b-256 checksum How to use checksums |
a284c18eeb0db6ec03f22e4a8521e7497fbeb60bfadfdec67f684bfb42e5e0eb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 5, 2026.
Transparency log