Tools: How a Silent API Update Broke Our Billing (And How to Prevent It)
Source: Dev.to
It was 11 PM on a Friday when the alerts started. Orders were failing. By the time I opened my laptop, 23 customers had seen an error screen and abandoned their carts. I spent two hours blaming our own code. I checked every recent deployment and rolled back twice. Nothing had changed on our end. Then I looked at the raw API response from Stripe. A field we had relied on for eight months was just gone. No announcement. No deprecation warning. No 500 error. The response came back as 200 OK with a perfectly valid JSON body. The body just had fewer fields than we expected. Stripe had quietly restructured part of their response under certain conditions. They call this a "non-breaking change." Non-breaking to them. Breaking to us. The Threat of Silent API Drift
If you integrate with any third-party API, you carry this risk right now. Stripe, Shopify, Twilio, QuickBooks they all evolve their response schemas. They do not always version every field-level change. A field disappears. A string becomes an integer. A previously required field becomes optional. PHP silently treats a missing array key as null, and your application processes subtly wrong data without complaint. Your tests won't catch this because your tests use static fixtures you wrote months ago. Standard monitoring won't catch it because the HTTP status code is still 200. Introducing php-sentinel
I built an open-source tool to solve this. It is called php-sentinel. It sits passively between your application and the HTTP responses you receive. It watches the responses flowing through in production and learns what they normally look like. Once it sees enough samples, it hardens that knowledge into a baseline schema. After that, every new response is quietly compared against the baseline. If the shape changes, you get notified immediately not after 23 orders fail. Here is the integration with Guzzle: use Sentinel\Middleware\SchemaWatcher;
use Sentinel\Sentinel;
use Sentinel\Store\FileSchemaStore; $sentinel = Sentinel::create() ->withStore(new FileSchemaStore(storage_path('sentinel'))) ->withSampleThreshold(20) ->withDispatcher($dispatcher) ->build(); $reporter = new DriftReporter($dispatcher);
$watcher = new SchemaWatcher($httpClient, $sentinel, $reporter); $response = $watcher->sendRequest($request);
If you are using Laravel, the event integration lets you alert on drift immediately: Event::listen(SchemaDriftDetected::class, function ($event) { $drift = $event->drift; });
What It Detects
The drift detector understands JSON Schema semantics and categorizes severity (BREAKING, ADDITIVE, or ADVISORY): FieldRemoved: A field that was in every response has stopped appearing. NowNullable: A field that was always an object or string is now sometimes coming back as null. TypeChanged: A field changed from string to integer, or object to array. RequiredNowOptional: A field your code relies on is now only present sometimes. EnumValueAdded / EnumValueRemoved: A status field that used to have three values now has a fourth. Get the Package
We treat our own infrastructure obsessively, but we treat contracts with external APIs as if they are fixed forever. They aren't. Stop waiting for the next silent update to break your production environment. GitHub Repository: malikad778/php-sentinel
Install: composer require php-sentinel/sentinel Built for PHP 8.3+, with native Laravel and Symfony support. MIT Licensed. What silent API changes have burned you in the past? Let me know in the comments. Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse COMMAND_BLOCK:
Log::error("API schema drift detected", [ 'endpoint' => $drift->endpoint, 'severity' => $drift->severity->value, 'changes' => array_map(fn($c) => $c->getDescription(), $drift->changes),
]); Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
Log::error("API schema drift detected", [ 'endpoint' => $drift->endpoint, 'severity' => $drift->severity->value, 'changes' => array_map(fn($c) => $c->getDescription(), $drift->changes),
]); COMMAND_BLOCK:
Log::error("API schema drift detected", [ 'endpoint' => $drift->endpoint, 'severity' => $drift->severity->value, 'changes' => array_map(fn($c) => $c->getDescription(), $drift->changes),
]);