mnfst / manifest-php
Turn 4xx API errors into 2xx in real time.
Requires
- php: ^8.2
- ext-curl: *
- psr/http-message: ^1.1 || ^2.0
Requires (Dev)
- cakephp/http: ^5.1
- guzzlehttp/guzzle: ^7.5 || ^8.0
- illuminate/container: ^11.0 || ^12.0 || ^13.0
- illuminate/http: ^11.0 || ^12.0 || ^13.0
- phpunit/phpunit: ^11.0
- rmccue/requests: ^2.0
- symfony/config: ^6.4 || ^7.0 || ^8.0
- symfony/dependency-injection: ^6.4 || ^7.0 || ^8.0
- symfony/http-client: ^6.4 || ^7.0 || ^8.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- dev-main
- v0.6.0
- v0.5.0
- v0.4.0
- v0.3.0
- v0.2.0
- v0.1.0
- dev-feat/http-redact
- dev-release-please--branches--main--components--mnfst/manifest-php
- dev-chore/legal-watch
- dev-feat/php-self-healing-hardening
- dev-fix/restore-query-credentials
- dev-fix/silent-in-test-suites
- dev-feat/wordpress-requests
- dev-feat/symfony-http-client
- dev-refactor/healer-on-plain-data
- dev-docs/laravel-install-notes
- dev-fix/manifest-idempotent
- dev-fix/read-key-from-env-superglobals
- dev-fix/no-key-sends-nothing
- dev-fix/curl-captures-headers-and-outcome
- dev-fix/persistent-backoff
- dev-fix/form-bodies-verbatim
- dev-fix/json-fidelity
- dev-fix/bounded-body-reads
- dev-fix/keep-response-bodies-readable
- dev-fix/keep-query-credentials-on-retry
- dev-fix/guzzle-failed-retry-throws
- dev-fix/non-utf8-error-bodies
- dev-fix/replay-healed-request
- dev-docs/readme-parity
This package is auto-updated.
Last update: 2026-09-28 09:39:27 UTC
README
Manifest for PHP
The API resilience layer for your PHP apps.
What is Manifest
Manifest is the API resilience layer for your apps and agents. It works with every API they call: external services, your internal APIs and MCP tools.
- πΊοΈ See every API your app depends on, and how reliable each one is.
- π― Repair failed API requests on the fly, so your app keeps working.
- π οΈ Know what to fix in your code, with a prompt for your coding agent.
How it works
Every call your app makes is reported to Manifest as metadata only (method, URL without its query string, status and timing), in batches at the end of a web request. A failure Manifest can heal is sent in full, so it can be repaired. What is sent.
Prerequisites
- PHP 8.2 or higher
- The PHP
curlextension, used to contact Manifest
No PHP extension to compile, nothing to change on the server.
Get started
Start with your agent
"Install Manifest in this app: https://dashboard.manifest.build/prompt-php.md"
Start with code
composer require mnfst/manifest-php
Then connect it to your HTTP client. Console commands (artisan, bin/cake,
bin/console, WP-CLI) are covered the same way as web requests.
Laravel β nothing to write. The service provider is discovered automatically
and covers every Http:: call. The key is read from MNFST_KEY, or from
config/services.php:
'manifest' => ['key' => env('MNFST_KEY'), 'url' => env('MNFST_URL')],
CakePHP 5.1+ β in src/Application.php:
public function bootstrap(): void { parent::bootstrap(); $this->addPlugin(\Mnfst\Cake\ManifestPlugin::class); }
Symfony β in config/bundles.php:
Mnfst\Symfony\ManifestBundle::class => ['all' => true],
WordPress β create wp-content/mu-plugins/manifest.php:
<?php require_once ABSPATH . 'vendor/autoload.php'; // wherever Composer installed it \Mnfst\WordPress\listen(); \Mnfst\manifest();
Any other Guzzle client β push the middleware on its handler stack, and start the SDK once:
use GuzzleHttp\Client; use GuzzleHttp\HandlerStack; use function Mnfst\manifest; manifest(); $stack = HandlerStack::create(); $stack->push(\Mnfst\Guzzle\middleware()); $client = new Client(['handler' => $stack]);
A library that accepts a Guzzle or PSR-18 client (the AWS SDK, most API clients) is covered when you pass it that client.
The SDK stays silent under PHPUnit and Pest, so Http::fake() answers are
never reported as failures; MNFST_IN_TESTS=1 opts back in. See
the guide.
Upgrading from 0.4
- Remove the
auto_prepend_fileline and anymanifest()call you added for loading order. - Add the line for your framework above (Laravel: nothing).
- You can remove the
opentelemetryextension if nothing else on the server uses it.
Setup
- Create a project in your Manifest dashboard and copy its project key.
- Set the key as an environment variable, or in the project's
.envfile:
export MNFST_KEY='your-project-key'
Verify the install from your project directory:
vendor/bin/manifest doctor
It reads the key from the shell, then from the project's .env.local, .env or
config/.env. It masks and validates the key, and checks that each framework
the project uses has its adapter in place.
Try it
Send a request that fails with a 4xx error, such as a value the API rejects:
use Illuminate\Support\Facades\Http; $response = Http::post('https://api.example.com/orders', [ 'limit' => 500, // rejected by the API ]);
The failed request appears in your Manifest dashboard, grouped with others like it in an issue. Once Manifest has a patch for that error, the next request that fails the same way is repaired and retried, and your app receives the answer to the retry.
Choosing which calls reach Manifest
Keep calls out of Manifest entirely: they are neither repaired nor tracked, and nothing about them leaves your app. Each entry is a domain or a domain with a path:
MNFST_ALLOWLIST=stripe.com # only Stripe MNFST_ALLOWLIST=stripe.com/v1/payment_intents # only this Stripe endpoint MNFST_DENYLIST=stripe.com/v1/charges,internal.example.com # never these
- A domain covers its subdomains, with or without a path:
stripe.comandstripe.com/v1/chargesboth matchapi.stripe.com. - A path matches whole segments:
/v1/chargescovers/v1/charges/ch_123, not/v1/charges_export. Paths are case-sensitive. - A scheme, port, query or fragment in an entry is ignored.
*in a path is not supported yet: the entry is skipped with a warning, and an allowlist made only of skipped entries lets nothing through. - The denylist wins over the allowlist. With no allowlist, every call is eligible.
- Paths are compared decoded, with
.and..resolved, so/%70rivateand/public/..%2Fprivateboth match/private. A patched retry is filtered too: a repair never moves a call onto an excluded path.
Or in code: \Mnfst\manifest(denylist: ['stripe.com/v1/charges']);. An option overrides its environment variable.
What is covered
| The app calls an API via⦠| Covered |
|---|---|
Laravel's Http facade |
β healed |
CakePHP's Cake\Http\Client (5.1+) |
β healed |
Symfony's HttpClient, including scoped clients |
β healed |
WordPress wp_remote_* |
β healed |
| A Guzzle client that carries the middleware | β healed |
| A Guzzle client built inside a library that does not accept yours | β not seen |
Raw curl_*, such as stripe/stripe-php |
β not seen |
file_get_contents |
β not seen |
Symfony responses consumed through stream() or with buffer => false stay
under the caller's control and are not healed. See the coverage details.
Supported infrastructure
| Infrastructure | Supported |
|---|---|
| Docker, Kubernetes | β |
| A VPS or your own server (Forge, Ploi) | β |
| Heroku, Platform.sh | β |
| Laravel Vapor, Bref | β |
| Shared hosting (cPanel, Hostinger, OVH) | β where Composer runs |
What leaves the machine
| Call | Sent to Manifest | Never sent |
|---|---|---|
| A call Manifest does not heal, whatever its status | method, scheme, host, port, path, status, timing | query string, headers, bodies |
| A failure Manifest can heal (a 4xx other than 401, 402, 403 and 429) | URL, headers, request body and error response. Credential values in the query string and headers are replaced by REDACTED; credential fields at the top level of the body are left out |
the masked values |
| The retry | nothing: it goes to the original API, through your own client, with the real values | β |
A call excluded by MNFST_ALLOWLIST / MNFST_DENYLIST |
nothing | everything |
One known limit: a secret inside a URL path (a webhook URL, /bot<token>/) is
sent as is. The rules are in src/Wire.php and CONTRACT.md.
More
Configuration, limits & development Β· API contract Β· Website

