π‘ We check dog.ceo (and 80+ other public dev APIs) with a plain GET every 30 minutes β see the live status page. As of publishing it's up and answering normally. This guide is not a "dog.ceo is down" story: it's about determinism β what a random-by-design API does to your test suite, and how to pin it.
The Dog API at dog.ceo is one of the nicest free APIs on the internet: no key, no signup, real HTTPS, Access-Control-Allow-Origin: *, an open-source backend, and images from the Stanford Dogs dataset β 108 breeds and 99 sub-breeds when we counted the live list. curl https://dog.ceo/api/breeds/image/random answers instantly with a photo of a dog. That's why it's in a thousand fetch tutorials, React starters, and coding-bootcamp exercises.
And that's exactly the problem once those exercises grow assertions. Four things we verified live on September 18, 2026:
/api/breeds/image/random returns a different image URL every call. Snapshot tests, visual-regression diffs, "expect the same render twice" β all impossible against the real thing. The API is doing its job perfectly; your test just can't pin it.message is a string, an array, or an object β depending on the endpoint. Single random β string. /random/3 or a breed's image list β array of strings. /breeds/list/all β an object keyed by breed. The tutorial's data.message works until the endpoint changes, and a TypeScript client needs string | string[] | Record<string,string[]> to be honest about it. (Errors reuse the same envelope with status:"error" plus a code field that success responses don't have.)/api/breeds/image/random/100 returns 50 URLs β HTTP 200, no warning. And /random/0 returns 1. Ask for N, assert N, watch it fail at 51.dog.ceo; the bytes come from images.dog.ceo. Every test render is a second, real network fetch you don't control β and you can't make it fail on demand to test your broken-image fallback.None of this is a complaint β a free, keyless random-dog API with open CORS is a gift, and randomness is the feature. The fix is to spend zero real calls on CI: host a deterministic pack β their exact response envelope, but the same dog every time, with image URLs that actually render from the same host. One curl, no signup.
dog.ceo is read-only β you can't POST a dog. Your app's favourites/adoption/gallery feature needs records it can write. Start with a real collection:
cat > dogs.json <<'EOF'
{
"dogs": [
{"id":1,"breed":"beagle","subBreed":null,"name":"Maple","image":"https://mockbird.mockbird.workers.dev/m/PROJECT_ID/img/400x300?text=beagle&seed=beagle","goodDog":true},
{"id":2,"breed":"hound","subBreed":"afghan","name":"Sable","image":"https://mockbird.mockbird.workers.dev/m/PROJECT_ID/img/400x300?text=afghan+hound&seed=hound-afghan","goodDog":true},
{"id":3,"breed":"kuvasz","subBreed":null,"name":"Willow","image":"https://mockbird.mockbird.workers.dev/m/PROJECT_ID/img/400x300?text=kuvasz&seed=kuvasz","goodDog":true}
]
}
EOF
curl -X POST "https://mockbird.mockbird.workers.dev/api/projects/import?name=dogs" \
-H 'content-type: application/json' --data-binary @dogs.json
# response includes your project id + admin key β put the id into the image URLs above
# (or just re-run the import after substituting; projects are free)
curl "https://mockbird.mockbird.workers.dev/m/PROJECT_ID/dogs?breed=beagle"
# β [{"id":1,"breed":"beagle","name":"Maple",β¦}] X-Total-Count: 1
# dog.ceo can't do this:
curl -X POST "https://mockbird.mockbird.workers.dev/m/PROJECT_ID/dogs" \
-H 'content-type: application/json' \
-d '{"breed":"corgi","subBreed":"cardigan","name":"Pippin","goodDog":true}'
# β persists; GET it back by id
Those image URLs are Mockbird's own deterministic SVG placeholders β same URL, same pixels, forever, served from the same host as the JSON. Your <img> renders in tests with zero third-party fetches. (Want real dog photos in a demo? Keep dog.ceo for the demo and the mock for the assertions β see the honest split below.)
If your code calls dog.ceo's URL shapes directly, mirror them with custom routes and point the client's base URL at the mock. Same envelope, same paths, same dog every time:
# the classic: random image β pinned
curl -X POST "https://mockbird.mockbird.workers.dev/api/projects/PROJECT_ID/routes" \
-H "x-admin-key: ADMIN_KEY" -H 'content-type: application/json' -d '{
"method":"GET","path":"/api/breeds/image/random","status":200,
"contentType":"application/json",
"body":"{\"message\":\"https://mockbird.mockbird.workers.dev/m/PROJECT_ID/img/400x300?text=beagle&seed=beagle\",\"status\":\"success\"}"}'
# exact-count multi β ask for 3, get exactly 3 (no silent cap)
curl -X POST "https://mockbird.mockbird.workers.dev/api/projects/PROJECT_ID/routes" \
-H "x-admin-key: ADMIN_KEY" -H 'content-type: application/json' -d '{
"method":"GET","path":"/api/breeds/image/random/3","status":200,
"contentType":"application/json",
"body":"{\"message\":[\"https://mockbird.mockbird.workers.dev/m/PROJECT_ID/img/400x300?text=beagle&seed=beagle\",\"https://mockbird.mockbird.workers.dev/m/PROJECT_ID/img/400x300?text=afghan+hound&seed=hound-afghan\",\"https://mockbird.mockbird.workers.dev/m/PROJECT_ID/img/400x300?text=kuvasz&seed=kuvasz\"],\"status\":\"success\"}"}'
For the breeds list, freeze the real thing: fetch it once and serve it verbatim (it's ~2.4 KB β well under the 16 KB route-body limit):
export BODY="$(curl -s https://dog.ceo/api/breeds/list/all)"
curl -X POST "https://mockbird.mockbird.workers.dev/api/projects/PROJECT_ID/routes" \
-H "x-admin-key: ADMIN_KEY" -H 'content-type: application/json' \
--data-binary "$(python3 -c "import json,os;print(json.dumps({'method':'GET','path':'/api/breeds/list/all','status':200,'contentType':'application/json','body':os.environ['BODY']}))")"
# now /m/PROJECT_ID/api/breeds/list/all === the list as captured β it never changes under your tests
And one template route answers any breed, deterministically per breed β same breed in, same image out:
curl -X POST "https://mockbird.mockbird.workers.dev/api/projects/PROJECT_ID/routes" \
-H "x-admin-key: ADMIN_KEY" -H 'content-type: application/json' -d '{
"method":"GET","path":"/api/breed/:breed/images/random","status":200,
"contentType":"application/json",
"body":"{\"message\":\"https://mockbird.mockbird.workers.dev/m/PROJECT_ID/img/500x400?text={{params.breed}}&seed={{params.breed}}\",\"status\":\"success\"}"}'
curl "https://mockbird.mockbird.workers.dev/m/PROJECT_ID/api/breed/pug/images/random"
# β {"message":"β¦/img/500x400?text=pug&seed=pug","status":"success"} β and that URL renders
dog.ceo's two 404 shapes differ by one word β main breed vs sub-breed. Byte-exact drill routes:
curl -X POST "https://mockbird.mockbird.workers.dev/api/projects/PROJECT_ID/routes" \
-H "x-admin-key: ADMIN_KEY" -H 'content-type: application/json' -d '{
"method":"GET","path":"/api/breed/notadog/images","status":404,
"contentType":"application/json",
"body":"{\"status\":\"error\",\"message\":\"Breed not found (main breed does not exist)\",\"code\":404}"}'
curl -X POST "https://mockbird.mockbird.workers.dev/api/projects/PROJECT_ID/routes" \
-H "x-admin-key: ADMIN_KEY" -H 'content-type: application/json' -d '{
"method":"GET","path":"/api/breed/hound/notasub/images/random","status":404,
"contentType":"application/json",
"body":"{\"status\":\"error\",\"message\":\"Breed not found (sub breed does not exist)\",\"code\":404}"}'
Note the trap these drills exist for: the error envelope carries a code field that success responses don't have, and message β a URL string a moment ago β is now a human sentence. If your client destructures message blindly, the 404 path renders an English sentence where an image should be.
Retry and fallback logic gets a scripted outage β no waiting for the real API to have a bad day:
curl -i "https://mockbird.mockbird.workers.dev/m/PROJECT_ID/api/breeds/image/random?mock_seq=503,503,200&mock_seq_key=ci1"
# 1st call β 503, 2nd β 503, 3rd β 200, then stays 200 β deterministic retry testing
| dog.ceo | Behavior | Deterministic mock |
|---|---|---|
/api/breeds/image/random | different URL every call | same URL every call (route, Β§2) |
/api/breeds/image/random/100 | silently returns 50 | exactly what you seeded |
/api/breeds/list/all | 108 breeds, can change | frozen verbatim capture |
/api/breed/<b>/images/random | random per call | template route: same breed β same image |
| image bytes | second host (images.dog.ceo) | same host, deterministic SVG that renders |
| 404 bodies | real, but only for real mistakes | byte-exact, on demand (Β§3) |
| POST a dog | read-only | full CRUD on your dogs resource |
| outage / flakiness | hope not | mock_seq=503,503,200 |
Real dog photos. A mock serves placeholders (or URLs you froze); dog.ceo serves the Stanford Dogs dataset, gloriously. For a portfolio demo, a workshop, or anything a human actually looks at for delight, use the real thing β it's free, fast, CORS-open, and community-run (their donation link keeps the servers up). The honest split: demos and delight β dog.ceo. Assertions, CI, visual regression, error paths, and anything that must answer the same twice β your mock. They compose: point the base URL at dog.ceo in the demo build and at the mock in the test build.
The one-curl import in section 1, or zero setup at all:
curl https://mockbird.mockbird.workers.dev/m/demo/products?limit=3
Facts checked live on September 18, 2026: /api/breeds/image/random answered a different URL on consecutive calls with Access-Control-Allow-Origin: *; /api/breeds/list/all counted 108 breeds / 99 sub-breeds; /api/breeds/image/random/100 returned 50 URLs with HTTP 200 and /random/0 returned 1; unknown breed answered HTTP 404 {"status":"error","message":"Breed not found (main breed does not exist)","code":404} and unknown sub-breed the matching (sub breedβ¦) variant. Every Mockbird command on this page was run against a live project before publishing (import, image URLs render as SVG, deterministic random route answering identically twice, exact-count multi, verbatim breeds list parse-equal to the live capture, template route for an unseeded breed with a rendering image, both 404 drills byte-exact, mock_seq 503/503/200, POST-a-dog read-back), and the scratch project was deleted after.
Full API reference in the docs. More guides: Shibe.online alternative (the shiba-picture API is unreachable) Β· Placeholder image API Β· Lorem Picsum alternative Β· Custom endpoints Β· Deterministic test data Β· Mock any third-party API. Create your API β