dog-ceo-api: the PHP service behind the dog.ceo breed and image API
The API hosted at dog.ceo
At a glance
- What is it?
- dog-ceo-api is the Symfony and PHP implementation behind the dog.ceo API, a public read-only service that lists dog breeds and sub-breeds and returns image URLs for them. The repository is the PHP side of a system that is being migrated to a Rust service, and the README is candid about the two places where the two implementations disagree.
- Who is it for?
- dog-ceo-api is worth reading if you are building something against dog.ceo and need to know what the endpoints actually guarantee, because the README documents the response shape, the error format, the breed data gap and the two places where the PHP and Rust implementations disagree, which is more than most public API documentation gives you.
- Can I use it commercially?
- Not without permission. GitHub finds no licence file in the repository, and without a licence all rights are reserved by default: you may read the code but not reuse it. Check the README, or ask the authors, before using it.
- Is it still maintained?
- Yes. The repository last received commits 3 days ago.
- What is it written in?
- Mainly PHP, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 24, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Two implementations of the same API, and the README says where they differ
The dog-ceo-api repository is the code for the API hosted at dog.ceo, and it is written in PHP with Symfony 6 on PHP 8.3 or newer.
The interesting part is that the production service is not this code. The README states that some API requests are cached from an AWS Lambda, linking to a separate Go implementation, and it describes the Rust API as the production one in the endpoint documentation. So the repository is the PHP implementation, and a Rust service is what actually answers requests in production, with some responses served from the Lambda cache.
That means the README is a specification for two implementations, and it is unusually careful to flag the places where they behave differently.
One is the random image count. The production Rust API silently caps random image count requests at 1,000, while the PHP implementation in this repository silently caps the same endpoint at 50. Invalid numbers, meaning non-numeric, zero or negative, default to one image in both.
The other is the random breed selection. The production Rust API chooses a main breed uniformly, and then chooses an image from that breed's image pool including its sub-breeds. The PHP documentation does not make the same claim about its own selection, so a client that assumes uniform breed selection should not rely on it here.
Neither difference is documented as a bug. They are just the facts of two codebases, and a client that reads the README rather than the source will at least know that the two exist.
Every response carries the data in a message field, and errors add a code
The response format is one shape for everything. All endpoints return JSON in this format:
{
"message": "...",
"status": "success"
}The message field contains the actual data and its type varies by endpoint. It is a string for a single image URL, an array for lists of breeds, sub-breeds or multiple image URLs, and an object for breeds with their sub-breeds as key and value pairs.
Errors follow the same envelope with the status changed and a code field added:
{
"status": "error",
"message": "Breed not found (main breed does not exist)",
"code": 404
}That the HTTP status and the code field carry the same value is a small detail, but it means a client can rely on the code field being present on errors rather than having to parse the message to work out what went wrong. The messages themselves are descriptive rather than machine-readable, so the code is what you should branch on.
The topics attached to the repository list xml as well as json and php and symfony, which suggests an XML representation is served alongside the JSON, though the visible documentation only covers the JSON format. There is a .htaccess file at the repository root, which is the usual Apache entry for a Symfony project that sits behind a virtual host.
The breed list endpoints are complete and the breed info endpoints are not
The endpoint catalogue splits cleanly into two halves, one that works and one that mostly does not.
The list endpoints are solid. /breeds/list/all returns an object with breed names as keys and sub-breed arrays as values, and an empty array indicates a breed with no sub-breeds. The example shows bulldog with boston, english and french, and hound with afghan, basset, blood, english, ibizan, plott and walker, and notes 98 breeds in total.
{
"message": {
"affenpinscher": [],
"bulldog": ["boston", "english", "french"],
"hound": ["afghan", "basset", "blood", "english", "ibizan", "plott", "walker"],
// ... 98 breeds total
},
"status": "success"
}The variants on it are all about randomness: /breeds/list/all/random for a random breed including sub-breeds, /breeds/list/all/random/10 for ten of them, and the same pair for main breeds only through /breeds/list/random and /breeds/list/random/10.
Sub-breed operations hang off /breed/{breed}/list, which returns sub-breed names without the main breed prefix, an empty array if the breed has none, and random variants of the same.
Then the information endpoints, and here the README is blunt. /breed/{breed} gets main breed info, and the documentation says the data is incomplete, see the content folder. It then says most breeds return 404 with the message No info file for this breed exists, and calls this a known limitation. The same applies to the sub-breed version.
So a client should treat the breed info endpoints as best effort. The list endpoints are the ones to build against.
Image endpoints return every sub-breed's images with no pagination
The image endpoints are the most used part of the API and the most likely to surprise, because of a single design decision.
/breed/{breed}/images returns images from the main breed and all its sub-breeds combined. The README gives the example: /breed/hound/images returns 808 images from all the hound sub-breeds, which are afghan, basset, blood, english, ibizan, plott and walker. No pagination is applied.
That is the key thing to know before you build on it. A response can be eight hundred URLs, and the only control you have is which breed you ask for. Asking for a main breed is asking for its whole family.
The random variants take a count, so /breed/{breed}/images/random and /breed/{breed}/images/random/4 give you a single random image or four, and the sub-breed equivalents work the same way at /breed/{breed}/{breed2}/images and its random variants.
The count is where the two implementations part company, as mentioned earlier. The PHP code caps at 50 and the Rust production service caps at 1000, and both are silent about it, so a request for 500 images returns 50 from this repository and 500 from production. Invalid counts fall back to one.
For a client that wants a single image, /breeds/image/random is the endpoint to use, and it has the same uniform breed selection behaviour in the Rust implementation.
Running it locally takes composer, a Symfony serve command and two secrets
The setup is short. Clone the repository, run composer install, change into the public directory, and start PHP's built-in server on 127.0.0.1 at port 6969 with php -S.
The environment is two variables in a .env.local file, and the names make their purpose clear:
DOG_CEO_CACHE_KEY="something-really-secure"
DOG_CEO_LAMBDA_URI=https://example.execute-api.us-east-1.amazonaws.com/dev/The cache key is the shared secret that guards the cache clear endpoint, which is called with the key in a header:
$ curl -X GET http://127.0.0.1:8000/cache-clear -H 'auth-key: something-really-secure'Note the port difference: the local server is started on 6969 and the cache clear example calls 8000. The port is not fixed by the application, so treat both as the developer's own environment rather than a documented constant.
The dependency requirements are checked with a Composer command, and the README includes the output of that check for a PHP 8.1 environment, with the yaml extension present and the mbstring extension provided by a Symfony polyfill:
$ composer check-platform-reqs
Checking platform requirements for packages in the vendor dir
composer-plugin-api 2.3.0 success
composer-runtime-api 2.2.2 success There is a note above that output asking whether the yaml module is actually required, which is the kind of honest uncertainty that appears when a dependency list has accreted.
The Makefile runs everything through a Docker image
The build tooling is a set of Make targets that all shell out to a Docker image, which means a contributor does not need PHP installed at all.
The Dockerfile is the basis for it, built from a php:8.5-cli base with Composer copied in from the composer image, and libyaml-dev installed for the yaml extension. The pecl install of the yaml extension is present but commented out, and pcov is installed and enabled for code coverage, which is how the codecov badge in the README gets its numbers.
The Makefile then defines the targets. The help target greps the Makefile itself for the comment pattern and prints a formatted list, which is a common trick for keeping the help text in sync with the targets. The build target builds the image once and tags it php-dev, and every other target runs a container against it with the current directory mounted at /app.
install: ## Install dependencies
docker run --rm -v $$(pwd):/app php-dev composer installThe test target is a two step command, first running composer dump-env test and then running the PHPUnit binary with the --display-deprecations flag, so deprecation notices are surfaced rather than buried. The requirements target runs the platform check inside the container, and the upgrade target runs composer update, which is where a dependency bump happens. The clean target removes the vendor directory.
The repository also has a .php-version file, a phpstan.neon for static analysis, a phpunit.xml.dist for the test configuration, a symfony.lock, and a Dockerfile alongside the Makefile, plus CircleCI and Codecov configuration. The last push was on 2026-10-05 and the repository is not archived, and the licence field is recorded as unknown even though the README badge says MIT, which is another small discrepancy to resolve before you reuse any of it.
Editorial conclusion
dog-ceo-api is worth reading if you are building something against dog.ceo and need to know what the endpoints actually guarantee, because the README documents the response shape, the error format, the breed data gap and the two places where the PHP and Rust implementations disagree, which is more than most public API documentation gives you. It is the wrong tool if you need an API you can rely on for production data, because the breed information endpoints return 404 for most breeds by the project's own admission. Before you depend on it, verify three things yourself: that the endpoint you need is one of the breed list and image endpoints rather than the breed information ones, that the random image count you request is within the cap, since the PHP implementation caps it at 50 while the production Rust API caps it at 1000, and that the caching behaviour suits you, since some responses are cached from an AWS Lambda and the cache key is a shared secret you have to set yourself.
Frequently asked questions
What is the dog-ceo-api repository?
It is the PHP and Symfony 6 implementation of the API hosted at dog.ceo, a public service that lists dog breeds and sub-breeds and returns image URLs. The README notes that the production API is a Rust service and that some requests are cached from an AWS Lambda.
What is the response format of the dog.ceo API?
All endpoints return a message field with the data and a status of success, where the message is a string for one image URL, an array for lists of breeds or image URLs, or an object for breeds with sub-breeds. Errors use the same envelope with the status set to error and a code field added, such as 404 with a message saying the main breed does not exist.
How many random images can I request from the dog.ceo API?
The production Rust API silently caps random image count requests at 1,000, while the PHP implementation in this repository caps the same endpoint at 50. Invalid numbers, whether non-numeric, zero or negative, default to one image.
Why do the dog.ceo breed information endpoints return 404?
The data is incomplete and the README says so, pointing at the content folder. Most breeds and sub-breeds return 404 with the message No info file for this breed exists, which the project describes as a known limitation, so the breed list and image endpoints are the reliable ones.
How do I run the dog-ceo-api locally?
Clone the repository, run composer install, change into the public directory and start the built-in server with php -S 127.0.0.1:6969. You also need a .env.local with DOG_CEO_CACHE_KEY and DOG_CEO_LAMBDA_URI, or you can use the Makefile targets which run composer, PHPUnit and the platform checks inside a Docker image.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/elliottlandsborough-dog-ceo-api)Was this useful?