Guides / Tutorials

Download dog breed images with Python

By the Dog Image API team. Updated .

You can download dog breed images with Python in one small script built on the requests library. Previews need no key and arrive as transparent WebP files. Full-size 2048 x 2048 PNGs need a key and a plan, and the script below reads the quota headers, retries 502 and 503, and stops on 429.

Set up

Install requests and put your key in the environment. Create the key on the account page after you sign in. It is shown once, so store it somewhere safe.

pip install requests
export DOG_IMAGE_API_KEY="paste-your-key-here"

Run the script on a server or your own machine. Never ship the key inside a web page or a mobile app. The images are AI-generated illustrations of one dog per set, in nine views, on a transparent background.

The whole script

Save this as fetch_dogs.py. It downloads three previews and three full-size files for each of three breeds.

import os
import sys
import time
from pathlib import Path

import requests

BASE = "https://dogimage.dev"
RETRY_CODES = {502, 503}


class QuotaUsedUp(Exception):
    """The monthly full-size count is spent. Retrying will not help."""


def describe(resp):
    try:
        err = resp.json()["error"]
        return f"{resp.status_code} {err['code']}: {err['message']}"
    except (ValueError, KeyError):
        return f"{resp.status_code} {resp.reason}"


def list_breeds():
    resp = requests.get(f"{BASE}/api/v1/breeds", timeout=30)
    resp.raise_for_status()
    return resp.json()


def download_preview(breed, variant, view, out_dir, size="1024"):
    url = f"{BASE}/api/v1/images/{breed}/{variant}/{view}"
    resp = requests.get(url, params={"size": size}, timeout=30)  # follows the 302
    if resp.status_code != 200:
        raise RuntimeError(describe(resp))
    path = out_dir / f"{breed}-{variant}-{view}-{size}.webp"
    path.write_bytes(resp.content)
    return path


def download_full(breed, variant, view, out_dir, attempts=4):
    url = f"{BASE}/api/v1/images/{breed}/{variant}/{view}"
    headers = {"Authorization": f"Bearer {os.environ['DOG_IMAGE_API_KEY']}"}
    for attempt in range(1, attempts + 1):
        resp = requests.get(url, params={"size": "full"}, headers=headers, timeout=60)
        if resp.status_code == 200:
            break
        if resp.status_code == 429:
            raise QuotaUsedUp(describe(resp))
        if resp.status_code in RETRY_CODES and attempt < attempts:
            time.sleep(2 ** attempt)
            continue
        raise RuntimeError(describe(resp))
    path = out_dir / f"{breed}-{variant}-{view}.png"
    path.write_bytes(resp.content)
    return path, resp.headers.get("X-Quota-Remaining")


def main():
    breeds = ["pug", "beagle", "samoyed"]
    views = ["front", "left", "head"]
    out_dir = Path("dogs")
    out_dir.mkdir(exist_ok=True)

    catalog = list_breeds()
    known = {b["slug"] for b in catalog["breeds"]}
    print(f"The catalog lists {len(known)} breeds.")

    for breed in breeds:
        if breed not in known:
            sys.exit(f"Unknown breed slug: {breed}")
        for view in views:
            print("preview", download_preview(breed, "default", view, out_dir))

    for breed in breeds:
        for view in views:
            try:
                path, remaining = download_full(breed, "default", view, out_dir)
            except QuotaUsedUp as err:
                print(f"Stopping: {err}")
                return
            print("full", path, "remaining this month:", remaining)


if __name__ == "__main__":
    main()

Run it with python fetch_dogs.py.

How each part works

Listing breeds

GET /api/v1/breeds returns a views list and a breeds list. Each breed has a slug, a name, a default_variant and its variants. The script reads the slugs from that reply instead of hardcoding the catalog size, because the live count changes as sets are added.

Seven breeds have several colors. For those, default is the breed's main set, and a color slug such as blue picks another one. The catalog lists every slug. Browse them on the breed index or on a single page such as /dogs/pug.

Previews without a key

A preview request answers with a 302 redirect to a WebP file. The requests library follows redirects for GET, so resp.content is the image itself. Valid sizes are 512 and 1024, and 1024 is the default.

If you show a preview on a page, add the credit link the license asks for. Each breed page prints the snippet:

<a href="https://dogimage.dev/dogs/pug">Pug image: Dog Image API</a>

Full-size PNGs with a key

Full-size files are PNG, transparent, 2048 x 2048 pixels, and about 1 to 7 MB each. The script sends Authorization: Bearer <key> and writes the bytes to disk. Files you download are meant to be stored on your side, so do not link to the API from a live page.

Reading the quota headers

Every successful full-size reply carries X-Quota-Limit, X-Quota-Used and X-Quota-Remaining. On the All images plan the value is the string unlimited, so do not cast it to an integer without a check. If you want to act on the number, do it like this:

remaining = resp.headers.get("X-Quota-Remaining")
if remaining not in (None, "unlimited") and int(remaining) < 10:
    print("Fewer than 10 images left this month.")

Stopping on 429, retrying 502 and 503

A 429 carries the code quota_used_up. The script raises QuotaUsedUp, prints it, and returns. Files already saved stay on disk.

A 502 (file_unavailable) and a 503 (plan_check_failed) are the two codes to retry. The loop waits 2, 4, then 8 seconds. A 502 is not counted against your quota. Every other error, such as 401, 402 or 404, stops with a readable message, because trying again will not change the answer. The full list is in the errors and quotas reference.

Budget before you loop

Each distinct full-size image uses one unit of your plan for the month. Starter allows 200 and Pro allows 700. Check the plan before a large run, as shown in the Node.js version of this guide, or read /api/v1/usage with the same header. The pricing page lists the plans, and the license says what you may do with the files.

Questions

Do I need an API key to download dog images with Python?

Not for previews. The breed list, the breed detail and the 512 and 1024 pixel WebP previews are keyless. Only full-size PNGs (size=full) and the usage endpoint need a key.

Does downloading the same full-size image twice use my quota twice?

No. A plan counts distinct full-size images per calendar month (UTC), so fetching the same image again in the same month is free. A failed 502 fetch is not counted either.

What should a Python script do on a 429?

Stop. The 429 quota_used_up error means the monthly count is spent, and retrying does not help. Save what you have and run again after the month rolls over or on a larger plan.

Can I use the preview images without paying?

Yes, with a visible link that credits Dog Image API next to the image or on the same page. The images are AI-generated illustrations, so describe them that way.

Next steps