Upload an image with Python Requests by opening the file in binary mode and passing it as the multipart file field. Set a timeout, accept HTTP 200 or 202, parse the JSON, and show the server fix when an error occurs. Close the file and each response with context managers.
Before you upload
imgd.dev costs $1 per GB as a one-time storage purchase. It has no free tier. Every imgd.dev image is public to anyone with its URL. Do not upload a private, confidential, or secret image.
Use Python 3.10 or later. Load IMGD_KEY from a secret manager and keep it outside the repository.
The example needs the Requests package. Save this line as examples/core/python/requirements.txt:
requests>=2.32,<3
Create a local environment and install the package:
python3 -m venv .venv
. .venv/bin/activate
python -m pip install -r examples/core/python/requirements.txt
test -n "$IMGD_KEY"
Complete Python example
Save this code as examples/core/python/upload_image.py.
from __future__ import annotations
import json
import os
import sys
from pathlib import Path
from typing import Final
import requests
UPLOAD_URL: Final = "https://imgd.dev/v1/upload"
METADATA_URL: Final = "https://imgd.dev/v1/images"
MAX_UPLOAD_BYTES: Final = 20 * 1024 * 1024
TIMEOUT: Final = (10, 60)
MIME_TYPES: Final = {
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".gif": "image/gif",
".webp": "image/webp",
".avif": "image/avif",
}
STATUS_GUIDANCE: Final = {
401: "Load a valid IMGD_KEY from the secret manager.",
402: "Buy storage or delete unused images, then retry.",
413: "Reduce the image below 20 MB, then retry.",
415: "Use JPEG, PNG, GIF, WebP, or AVIF with the correct media type.",
}
class UploadError(RuntimeError):
"""An upload or verification error with a safe message."""
def read_json(response: requests.Response) -> object:
try:
return response.json()
except ValueError as error:
raise UploadError(
f"The server returned invalid JSON with HTTP {response.status_code}."
) from error
def response_error(response: requests.Response, payload: object) -> UploadError:
error_code = "unknown_error"
fix = STATUS_GUIDANCE.get(
response.status_code, "Read the response and correct the request."
)
if isinstance(payload, dict):
payload_error = payload.get("error")
payload_fix = payload.get("fix")
if isinstance(payload_error, str):
error_code = payload_error
if isinstance(payload_fix, str):
fix = payload_fix
return UploadError(
f"Request failed with HTTP {response.status_code} ({error_code}): {fix}"
)
def require_upload_success(payload: object) -> dict[str, object]:
if not isinstance(payload, dict):
raise UploadError("The upload response is not a JSON object.")
required_types = {
"hash": str,
"url": str,
"status": str,
"mime": str,
"bytes": int,
}
for field, expected_type in required_types.items():
if not isinstance(payload.get(field), expected_type):
raise UploadError(f"The upload response has no valid '{field}' field.")
return payload
def upload_image(
session: requests.Session, image_path: Path, api_key: str
) -> dict[str, object]:
media_type = MIME_TYPES.get(image_path.suffix.lower())
if media_type is None:
raise UploadError("Use a JPEG, PNG, GIF, WebP, or AVIF file name.")
size = image_path.stat().st_size
if size == 0:
raise UploadError("The image file is empty.")
if size > MAX_UPLOAD_BYTES:
raise UploadError("The image is larger than the 20 MB upload limit.")
headers = {"Authorization": f"Bearer {api_key}"}
with image_path.open("rb") as image_file:
files = {"file": (image_path.name, image_file, media_type)}
with session.post(
UPLOAD_URL,
headers=headers,
files=files,
timeout=TIMEOUT,
) as response:
payload = read_json(response)
if response.status_code not in (200, 202):
raise response_error(response, payload)
return require_upload_success(payload)
def verify_upload(
session: requests.Session, upload: dict[str, object], api_key: str
) -> None:
image_hash = upload["hash"]
image_url = upload["url"]
if not isinstance(image_hash, str) or not isinstance(image_url, str):
raise UploadError("The validated upload fields changed type.")
headers = {"Authorization": f"Bearer {api_key}"}
with session.get(
f"{METADATA_URL}/{image_hash}", headers=headers, timeout=TIMEOUT
) as response:
metadata = read_json(response)
if response.status_code != 200:
raise response_error(response, metadata)
if not isinstance(metadata, dict) or metadata.get("hash") != image_hash:
raise UploadError("The metadata response did not confirm the upload hash.")
with session.get(image_url, timeout=TIMEOUT, stream=True) as response:
if response.status_code != 200:
raise UploadError(
f"The public URL returned HTTP {response.status_code}."
)
content_type = response.headers.get("Content-Type", "")
if not content_type.startswith("image/"):
raise UploadError(
f"The public URL returned the unexpected type '{content_type}'."
)
first_chunk = next(response.iter_content(chunk_size=1), b"")
if not first_chunk:
raise UploadError("The public URL returned an empty image body.")
def main() -> int:
if len(sys.argv) != 2:
print("Usage: python3 upload_image.py IMAGE_PATH", file=sys.stderr)
return 64
api_key = os.environ.get("IMGD_KEY")
if not api_key:
print("Set IMGD_KEY in the environment.", file=sys.stderr)
return 78
image_path = Path(sys.argv[1])
try:
with requests.Session() as session:
upload = upload_image(session, image_path, api_key)
verify_upload(session, upload, api_key)
except (OSError, requests.RequestException, UploadError) as error:
print(str(error), file=sys.stderr)
return 1
print(json.dumps(upload, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())
Run the example
Pass one accepted image path:
python3 examples/core/python/upload_image.py ./image.png
The file stays open only during the multipart request. The with image_path.open("rb") block closes it after the request ends.
The Session and all Response objects also use context managers. This releases network resources on success and failure.
Expected JSON
A new PNG can return this output after the verification requests pass:
{
"hash": "0000000000000000000000000000000000000000000000000000000000000000",
"url": "https://i.imgd.dev/i/0000000000000000000000000000000000000000000000000000000000000000",
"status": "processing",
"mime": "image/png",
"bytes": 60412,
"width": 1280,
"height": 720,
"alt_text": null,
"unpublish_at": null,
"note": "the url works immediately, serving a blurred placeholder until the moderation check finishes and it sharpens to the real image"
}
New bytes return HTTP 202. Identical bytes return HTTP 200 and can add deduplicated: true.
The public URL returns an image immediately. It is a blurred placeholder while the status is processing.
How the timeout works
TIMEOUT = (10, 60) sets a 10-second connection timeout and a 60-second read timeout.
Requests documents that a timeout is not a total transfer deadline. It limits how long a socket can wait without the required activity.
The program catches requests.RequestException. A timeout, connection error, or redirect error produces a nonzero exit code.
How status-aware errors work
The program does not treat valid JSON as proof of success. It checks the HTTP status before it accepts the object.
For a non-200 or non-202 upload response, response_error reads error and fix. It includes the server fix in the final message.
A typical quota response is valid JSON:
{
"error": "quota_exceeded",
"fix": "this upload needs more room than the storage left; buy storage or delete images, then retry",
"topup_url": "https://imgd.dev/v1/credit",
"suggested_gb": 1,
"gb_price_usd": "1"
}
The code never prints the authorization header.
Verify the result
verify_upload requests GET /v1/images/:hash with the key. It confirms that the metadata hash matches the upload hash.
It then requests the public URL without authentication. It checks HTTP 200, an image/* content type, and at least one byte.
A later metadata request can report live, review, blocked, or error. The review state is terminal and stays blurred.
Recover from API errors
Use the HTTP status and the server fix value.
| Status | Cause | Action |
|---|---|---|
401 |
IMGD_KEY is missing or invalid. |
Load the correct key from the secret manager. Do not print it. |
402 |
The account has no storage or lacks quota. | Buy storage or delete unused images. Then run the script again. |
413 |
The upload is larger than 20 MB. | Resize or compress the image below 20 MB. |
415 |
The media type is not accepted. | Use JPEG, PNG, GIF, WebP, or AVIF with the matching extension. |
Do not retry these errors without a change. You can retry a timeout with the same file. Content addressing returns the same hash and URL.
The local size check prevents most 413 responses. The server remains the final authority if the file changes during the request.
Security rules
Keep IMGD_KEY in the environment and outside version control. Do not include it in a URL, source file, traceback, or normal output.
The script prints only the upload JSON. Its URL is public and needs no key for access.
Inspect the image before upload. Public storage does not protect private text, customer records, or account data.
Decide if Python Requests fits
This example fits Python scripts that already use Requests. It gives the caller explicit timeouts, file cleanup, and actionable errors.
It does not fit private images, access control, unsupported files, images above 20 MB, or a free tier. Read the service evaluation guide first.
Result
The script uploads one context-managed file, reports the server fix on failure, and verifies both metadata and the public image response.
Compare it with the JavaScript upload guide or the Go upload guide.
Give an agent the integration procedure. A person can start from the home page.