Turn any OpenAPI spec into dedicated Laravel artisan commands. Each endpoint gets its own command with typed options for path parameters, query parameters, and request bodies.
OpenApiCli::register('https://api.bookstore.io/openapi.yaml', 'bookstore')
->baseUrl('https://api.bookstore.io')
->bearer(env('BOOKSTORE_TOKEN'))
->banner('Bookstore API v2')
->cache(ttl: 600)
->followRedirects()
->yamlOutput()
->showHtmlBody()
->useOperationIds()
->onError(function (Response $response, Command $command) {
return match ($response->status()) {
429 => $command->warn('Rate limited. Retry after '.$response->header('Retry-After').'s.'),
default => false,
};
});List all endpoints:
php artisan bookstore:listBookstore API v2
GET bookstore:get-books List all books
POST bookstore:post-books Add a new book
GET bookstore:get-books-reviews List reviews for a book
DELETE bookstore:delete-books Delete a book
Human-readable output (default):
php artisan bookstore:get-books --limit=2# Data
| id | title | author |
|----|--------------------------|-----------------|
| 1 | The Great Gatsby | F. Fitzgerald |
| 2 | To Kill a Mockingbird | Harper Lee |
# Meta
total: 2
YAML output:
php artisan bookstore:get-books --limit=2 --yamldata:
-
id: 1
title: 'The Great Gatsby'
author: 'F. Fitzgerald'
-
id: 2
title: 'To Kill a Mockingbird'
author: 'Harper Lee'
meta:
total: 2We invest a lot of resources into creating best in class open source packages. You can support us by buying one of our paid products.
We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on our contact page. We publish all received postcards on our virtual postcard wall.
You can install the package via composer:
composer require spatie/laravel-openapi-cliThe package will automatically register its service provider.
Register your OpenAPI spec in a service provider (typically AppServiceProvider). You can use a local file path or a remote URL:
use Spatie\OpenApiCli\Facades\OpenApiCli;
public function boot()
{
OpenApiCli::register(base_path('openapi/bookstore-api.yaml'), 'bookstore')
->baseUrl('https://api.example-bookstore.com')
->bearer(env('BOOKSTORE_TOKEN'));
}The second argument is the namespace - it groups all commands under a common prefix. For a spec with GET /books, POST /books, and GET /books/{book_id}/reviews, you get:
bookstore:get-booksbookstore:post-booksbookstore:get-books-reviewsbookstore:list
If you omit the namespace, commands are registered directly without a prefix. This is useful for Laravel Zero CLI tools or any app where a single API is the primary interface:
OpenApiCli::register(base_path('openapi/api.yaml'))
->baseUrl('https://api.example.com')
->bearer(env('API_TOKEN'));For the same spec, you get:
get-bookspost-booksget-books-reviews
Note: The list command is not registered when no namespace is set, since it would conflict with Laravel's built-in list command.
You can register a spec directly from a URL. The spec is fetched via HTTP on every boot by default:
OpenApiCli::register('https://api.example.com/openapi.yaml', 'example')
->baseUrl('https://api.example.com')
->bearer(env('EXAMPLE_TOKEN'));To enable caching (recommended for production), call cache():
OpenApiCli::register('https://api.example.com/openapi.yaml', 'example')
->cache(); // cache for 60 seconds (default)You can customize the TTL, cache store, and key prefix:
OpenApiCli::register('https://api.example.com/openapi.yaml', 'example')
->cache(ttl: 600); // 10 minutes
OpenApiCli::register('https://api.example.com/openapi.yaml', 'example')
->cache(ttl: 600, store: 'redis', prefix: 'my-api:');Commands are named {namespace}:{method}-{path} where path parameters are stripped. Without a namespace, the command is just {method}-{path}:
| Method | Path | Command (namespaced) | Command (direct) |
|---|---|---|---|
| GET | /books |
bookstore:get-books |
get-books |
| POST | /books |
bookstore:post-books |
post-books |
| GET | /books/{book_id}/reviews |
bookstore:get-books-reviews |
get-books-reviews |
| DELETE | /authors/{author_id}/books/{book_id} |
bookstore:delete-authors-books |
delete-authors-books |
When two paths would produce the same command name (e.g. /books and /books/{id} both yield get-books), the trailing path parameter is appended to disambiguate:
| Path | Command (namespaced) | Command (direct) |
|---|---|---|
/books |
bookstore:get-books |
get-books |
/books/{id} |
bookstore:get-books-id |
get-books-id |
If your spec includes operationId fields, you can use those for command names instead of the URL path:
OpenApiCli::register(base_path('openapi/api.yaml'), 'api')
->baseUrl('https://api.example.com')
->useOperationIds();With operationId: listBooks in the spec, the command becomes api:list-books instead of api:get-books. Endpoints without an operationId fall back to path-based naming.
Path parameters become required --options:
php artisan bookstore:get-books-reviews --book-id=42
php artisan bookstore:delete-authors-books --author-id=1 --book-id=7Parameter names are converted to kebab-case (book_id becomes --book-id, bookId becomes --book-id).
Query parameters defined in the spec become optional --options:
# If the spec defines ?genre and ?limit query params for GET /books
php artisan bookstore:get-books --genre=fiction --limit=10Bracket notation is converted to kebab-case (e.g. filter[id] becomes --filter-id).
Use --field to send key-value data:
php artisan bookstore:post-books --field title="The Great Gatsby" --field author_id=1Fields are sent as JSON by default. If the spec declares application/x-www-form-urlencoded as the content type, fields are sent as form data instead.
Send raw JSON with --input:
php artisan bookstore:post-books --input '{"title":"The Great Gatsby","metadata":{"genre":"fiction","year":1925}}'--field and --input cannot be used together.
Upload files using the @ prefix on field values:
php artisan bookstore:post-books-cover --book-id=42 --field cover=@/path/to/cover.jpg
php artisan bookstore:post-books-cover --book-id=42 --field cover=@/path/to/cover.jpg --field alt="Book Cover"When any field contains a file, the request is sent as multipart/form-data.
Every namespaced API gets a {namespace}:list command:
php artisan bookstore:listGET bookstore:get-books List all books
POST bookstore:post-books Add a new book
GET bookstore:get-books-reviews List reviews for a book
DELETE bookstore:delete-books Delete a book
POST bookstore:post-books-cover Upload a cover image
Note: The list command is only registered when a namespace is provided. Direct registrations (without a namespace) do not get a list command.
Add a banner that displays above the endpoint list when running {namespace}:list. This is useful for branding, ASCII art logos, or contextual information.
OpenApiCli::register(base_path('openapi/api.yaml'), 'api')
->baseUrl('https://api.example.com')
->banner('Bookstore API v1.0');For full control over styling, pass a callable that receives the Command instance:
OpenApiCli::register(base_path('openapi/api.yaml'), 'api')
->baseUrl('https://api.example.com')
->banner(function ($command) {
$command->info('=== My API ===');
$command->comment('Environment: ' . app()->environment());
});The banner only appears in the list command output, not when running individual endpoint commands.
OpenApiCli::register(base_path('openapi/api.yaml'), 'api')
->baseUrl('https://api.example.com')
->bearer(env('API_TOKEN'));OpenApiCli::register(base_path('openapi/api.yaml'), 'api')
->baseUrl('https://api.example.com')
->apiKey('X-API-Key', env('API_KEY'));OpenApiCli::register(base_path('openapi/api.yaml'), 'api')
->baseUrl('https://api.example.com')
->basic('username', 'password');Use a closure for tokens that may rotate or need to be fetched dynamically:
OpenApiCli::register(base_path('openapi/api.yaml'), 'api')
->baseUrl('https://api.example.com')
->auth(fn () => Cache::get('api_token'));The closure is called fresh on each request.
php artisan bookstore:get-books
# # Data
#
# | ID | Title |
# |----|-------------------|
# | 1 | The Great Gatsby |
#
# # Meta
#
# Total: 1JSON responses are automatically converted into readable markdown-style output: tables for arrays of objects, key-value lines for simple objects, and section headings for wrapper patterns like {"data": [...], "meta": {...}}.
php artisan bookstore:get-books --json
# {
# "data": [
# { "id": 1, "title": "The Great Gatsby" }
# ]
# }The --json flag outputs raw JSON (pretty-printed by default).
php artisan bookstore:get-books --yaml
# data:
# -
# id: 1
# title: 'The Great Gatsby'The --yaml flag converts JSON responses to YAML. If --json or --minify is also passed, those take priority.
php artisan bookstore:get-books --minify
# {"data":[{"id":1,"title":"The Great Gatsby"}]}The --minify flag implies --json — no need to pass both.
If you prefer YAML output as the default for a specific registration, use the yamlOutput() config method:
OpenApiCli::register(base_path('openapi/api.yaml'), 'api')
->baseUrl('https://api.example.com')
->yamlOutput();With yamlOutput(), commands output YAML by default. The --json and --minify flags override this and produce JSON output instead.
If you prefer JSON output as the default for a specific registration, use the jsonOutput() config method:
OpenApiCli::register(base_path('openapi/api.yaml'), 'api')
->baseUrl('https://api.example.com')
->jsonOutput();With jsonOutput(), commands output pretty-printed JSON by default. The --json and --minify flags still work as expected.
php artisan bookstore:get-books --headers
# HTTP/1.1 200 OK
# Content-Type: application/json
# X-RateLimit-Remaining: 99
#
# # Data
#
# | ID | Title |
# |----|-------------------|
# | 1 | The Great Gatsby |When an API returns HTML (e.g., an error page), the body is hidden by default to avoid flooding the terminal. You'll see a hint instead:
Response is not JSON (content-type: text/html, status: 500, content-length: 1234)
Use --output-html to see the full response body.
Pass --output-html to show the body:
php artisan bookstore:get-books --output-htmlTo always show HTML bodies for a specific registration, use the showHtmlBody() config method:
OpenApiCli::register(base_path('openapi/api.yaml'), 'api')
->baseUrl('https://api.example.com')
->showHtmlBody();Non-HTML non-JSON responses (e.g., text/plain) are always shown in full.
JSON and human-readable output are syntax-highlighted by default when running in a terminal. JSON output gets keyword/value coloring via tempest/highlight, and human-readable output gets colored headings, keys, and table formatting.
To disable highlighting (e.g. when piping output), use the built-in --no-ansi flag:
php artisan bookstore:get-books --no-ansi
php artisan bookstore:get-books --json --no-ansiHighlighting is automatically disabled when output is not a TTY (e.g. piped to a file or another command).
By default, HTTP redirects are not followed. This means a 301 or 302 response is returned as-is, so you can see exactly what the API responds with. To opt in to following redirects:
OpenApiCli::register(base_path('openapi/api.yaml'), 'api')
->baseUrl('https://api.example.com')
->followRedirects();- 4xx/5xx errors: Displays the status code and response body in human-readable format by default (or JSON with
--json, minified with--minify). HTML responses suppress the body by default (use--output-htmlto see it). Other non-JSON responses show the raw body with a content-type notice. - Network errors: Shows connection failure details.
- Missing path parameters: Tells you which
--optionis required. - Invalid JSON input: Shows the parse error.
All error cases exit with a non-zero code for scripting.
Register an onError callback to handle HTTP errors in a way that's specific to your API:
OpenApiCli::register(base_path('openapi/api.yaml'), 'api')
->baseUrl('https://api.example.com')
->bearer(env('API_TOKEN'))
->onError(function (Response $response, Command $command) {
return match ($response->status()) {
403 => $command->error('Your API token lacks permission for this endpoint.'),
429 => $command->warn('Rate limited. Try again in ' . $response->header('Retry-After') . 's.'),
500 => $command->error('Server error — try again later.'),
default => false, // fall through to default handling
};
});The callback receives the Illuminate\Http\Client\Response and the Illuminate\Console\Command instance, giving you access to all Artisan output methods (line(), info(), warn(), error(), table(), newLine(), etc.).
Return a truthy value to indicate "handled" — this suppresses the default "HTTP {code} Error" output. Return false or null to fall through to default error handling. The command always exits with a non-zero code regardless of whether the callback handles the error.
Register as many specs as you need with different namespaces:
OpenApiCli::register(base_path('openapi/bookstore.yaml'), 'bookstore')
->baseUrl('https://api.example-bookstore.com')
->bearer(env('BOOKSTORE_TOKEN'));
OpenApiCli::register(base_path('openapi/stripe.yaml'), 'stripe')
->baseUrl('https://api.stripe.com')
->bearer(env('STRIPE_KEY'));The base URL is resolved in this order:
- The URL set via
->baseUrl()on the registration - The first entry in the spec's
serversarray - If neither is available, the command throws an error
Pass -vvv to any command to see the full request before it's sent:
php artisan bookstore:get-books -vvv Request
-------
GET https://api.example-bookstore.com/books
Request Headers
---------------
Accept: application/json
Content-Type: application/json
Authorization: Bearer sk-abc...
This shows the HTTP method, resolved URL, request headers (including authentication), and request body when present.
Every endpoint command supports these universal options:
| Option | Description |
|---|---|
--field=key=value |
Send a form field (repeatable) |
--input=JSON |
Send raw JSON body |
--json |
Output raw JSON instead of human-readable format |
--yaml |
Output as YAML |
--minify |
Minify JSON output (implies --json) |
-H, --headers |
Include response headers in output |
--output-html |
Show the full response body when content-type is text/html |
Path and query parameter options are generated from the spec and shown in each command's --help output.
composer testPlease see CHANGELOG for more information on what has changed recently.
Please see CONTRIBUTING for details.
Please review our security policy on how to report security vulnerabilities.
The MIT License (MIT). Please see License File for more information.
