← All guides

πŸ“‘ 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.

Dog API (dog.ceo) alternative for tests β€” the same dog every time

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:

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.

1. Host your pack in one curl

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.)

2. Replay their exact endpoints β€” deterministically

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

3. Error drills β€” their exact 404s, plus flakiness on demand

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 β†’ Mockbird, side by side

dog.ceoBehaviorDeterministic mock
/api/breeds/image/randomdifferent URL every callsame URL every call (route, Β§2)
/api/breeds/image/random/100silently returns 50exactly what you seeded
/api/breeds/list/all108 breeds, can changefrozen verbatim capture
/api/breed/<b>/images/randomrandom per calltemplate route: same breed β†’ same image
image bytessecond host (images.dog.ceo)same host, deterministic SVG that renders
404 bodiesreal, but only for real mistakesbyte-exact, on demand (Β§3)
POST a dogread-onlyfull CRUD on your dogs resource
outage / flakinesshope notmock_seq=503,503,200

Where dog.ceo wins β€” and belongs

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.

Start now

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 β†’

⚑ Skip the terminal: this link opens the dashboard's import panel β€” paste the dogs.json from section 1 and you're done. No signup.