DEV Community

Cover image for Third-Party API Deprecations: Catch Them Before Prod
Muhammad Ali Mughal
Muhammad Ali Mughal

Posted on

Third-Party API Deprecations: Catch Them Before Prod

Many third-party API outages are announced months in advance. A payment gateway emailed a deprecation notice to an address nobody reads, a courier API started returning a Sunset header that no code looked at, an OAuth provider retired a scope on a date sitting in a changelog. The fix is not vigilance. It is plumbing: detect deprecation signals in code, record every external dependency with its version and owner, and test against the next version before the old one disappears.

I run Loop Origin, a software company that builds and maintains custom web applications, and post-launch maintenance is where this problem lives. I wrote this with help from AI tools and checked every command and claim myself. The code below is Laravel because that is what most of our client systems run, but the pattern carries straight over to Node or .NET.

How do you catch third-party API deprecations early?

Direct answer: Log the Deprecation and Sunset HTTP response headers on every outbound call, pin explicit API versions, and keep a dependency register with version, owner and end-of-life date for every integration. Run contract tests against the provider's next version in CI, and route all calls through one adapter so upgrades touch one class.

Where deprecation signals actually show up

Providers announce changes through different channels, and each one fails differently.

Signal Example Machine-readable? Typical failure
Deprecation response header (RFC 9745) Deprecation: @1767225600 Yes Nobody logs response headers
Sunset response header (RFC 8594) Sunset: Wed, 30 Jun 2027 23:59:59 GMT Yes Same as above
Version pinning with dated versions Stripe's dated API versions, Meta Graph API v21.0 Partly Account default silently differs from what code expects
Changelog or status page Provider developer blog RSS sometimes Nobody subscribed
Email to account owner Action required notices No Sent to a former employee or a shared inbox
Warning fields in response body "warnings": [...] in JSON Yes, provider-specific Parsed and discarded
Hard failure 410 Gone, 400 unsupported version Yes Arrives on the sunset date, in production

The goal is to move every integration from the bottom rows to the top rows.

Step 1: Log deprecation headers on every outbound call

Laravel's HTTP client fires a ResponseReceived event for every response, with both the request and the response attached. One listener can record any Deprecation or Sunset header once per host and path per day, so it does not flood the logs:

// app/Providers/AppServiceProvider.php
use Illuminate\Http\Client\Events\ResponseReceived;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Log;

public function boot(): void
{
    Event::listen(function (ResponseReceived $event) {
        $dep = $event->response->header('Deprecation');
        $sun = $event->response->header('Sunset');
        if ($dep === '' && $sun === '') {
            return;
        }

        $url  = parse_url($event->request->url());
        $host = $url['host'] ?? 'unknown';
        $path = $url['path'] ?? '/';
        $key  = 'api-deprecation:' . sha1($host . $path) . ':' . now()->toDateString();

        if (Cache::add($key, 1, now()->addDay())) {
            Log::channel('integrations')->warning('third_party_api_deprecation', [
                'host'        => $host,
                'path'        => $path,
                'deprecation' => $dep,
                'sunset'      => $sun,
                'link'        => $event->response->header('Link'),
            ]);
        }
    });
}
Enter fullscreen mode Exit fullscreen mode

Two details matter. RFC 9745 defines Deprecation as a structured-field date, written as @ plus a Unix timestamp, while Sunset uses an HTTP date. And the Link header with rel="deprecation" or rel="sunset" often points to the migration guide, so keep it. Ship the log line to whatever alerting you already have and route it to the integration's owner, not a general channel.

In Node the same idea is an interceptor on your shared Axios or fetch wrapper. The important part is that it lives in one shared client, not in each call site.

Step 2: Pin versions explicitly

Never rely on the provider's account-level default version. Send the version on every request so behavior changes only when your code changes:

// config/services.php
'stripe' => [
    'secret'      => env('STRIPE_SECRET'),
    'api_version' => env('STRIPE_API_VERSION'), // a dated version string, reviewed on upgrade
],
'meta' => [
    'graph_version' => env('META_GRAPH_VERSION', 'v21.0'),
],
Enter fullscreen mode Exit fullscreen mode

Stripe lets you set the version per request with the Stripe-Version header or the SDK's stripe_version option. Meta's Graph API puts the version in the URL path, and each version has a published expiry. Put both values in config so an upgrade is a reviewed pull request with a test run, not a dashboard click.

Step 3: Keep a dependency register

I keep a short register in each repository I maintain. A YAML file is enough:

# docs/integrations.yml
- name: Stripe
  purpose: Card payments and refunds
  version: "pinned in STRIPE_API_VERSION"
  sdk: stripe/stripe-php ^16
  owner: payments@client.example
  notice_channel: Stripe developer changelog + dashboard email
  next_review: 2027-01-15
- name: Courier tracking API
  purpose: Shipment status webhooks and label creation
  version: v2
  eol: "2027-06-30 (example: date taken from the Sunset header)"
  owner: ops-dev@client.example
  notice_channel: Partner portal announcements
  next_review: 2026-12-01
Enter fullscreen mode Exit fullscreen mode

The register answers the three questions everyone asks during an incident: who owns this, which version are we on, and where do they announce changes. It also lets a quarterly health review take an hour instead of a week.

Rusty old electrical connectors next to new clean industrial connectors on a workbench, representing an old API version being replaced by a new one
AI-generated image.

Step 4: Put every provider behind an adapter

If calls to a provider are spread across 40 controllers, a version upgrade is a 40-file change. One interface per capability keeps it to one class:

interface ShipmentTracker
{
    /** @return TrackingEvent[] */
    public function events(string $trackingNumber): array;
}

final class CourierV2Tracker implements ShipmentTracker { /* current */ }
final class CourierV3Tracker implements ShipmentTracker { /* next version */ }

// AppServiceProvider
$this->app->bind(ShipmentTracker::class, fn () =>
    config('features.courier_v3')
        ? new CourierV3Tracker(/* ... */)
        : new CourierV2Tracker(/* ... */)
);
Enter fullscreen mode Exit fullscreen mode

The flag lets you move a slice of traffic to the new version and roll back without a deploy. I covered the same Strategy pattern for payment and courier code in Strategy & Factory patterns for payment and courier integrations, and the retry and replay side of these integrations in resilient ERP and CRM API integrations.

Step 5: Contract-test the next version in CI

Most providers offer a sandbox. Run a small suite nightly against it with the next version pinned. It should cover the five or six calls your business depends on, not the full API:

it('creates a shipment label on courier v3 sandbox', function () {
    $tracker = new CourierV3Tracker(config('services.courier.sandbox_key'));
    $events = $tracker->events('SANDBOX-TRACKING-001');
    expect($events)->not->toBeEmpty()
        ->and($events[0])->toBeInstanceOf(TrackingEvent::class);
})->group('contract');
Enter fullscreen mode Exit fullscreen mode

When it fails, you learn about a breaking change from CI, weeks before production.

Build-vs-buy: how much tooling do you need?

Approach Setup effort Ongoing cost Catches Misses
Header logging listener About an hour Near zero Providers that send Deprecation/Sunset Providers that only email
Dependency register + quarterly review Half a day A few hours a quarter Anything with a published end-of-life Surprise breaking changes
Nightly contract tests One to three days per provider Sandbox upkeep Breaking changes in the next version Undocumented production-only behavior
Commercial API monitoring Vendor onboarding Subscription Uptime, latency, schema drift Business-logic changes

For most of the small and mid-sized systems we maintain, the first three rows are enough. A commercial monitor earns its cost when you have dozens of providers or strict SLAs.

Failure modes I plan for

  • The notice went to a dead inbox. Fix: make the provider account contact a group alias owned by the team, and list it in the register.
  • The SDK upgrade changed the default version. Some SDKs pin a version per major release. Read the SDK changelog, not just the API changelog.
  • Sandbox and production disagree. Keep a canary: route a small share of real traffic through the new adapter before full cutover.
  • Webhooks change shape too. Version your webhook handlers and verify signatures on both shapes during the overlap.
  • OAuth scope retirements. These break login or token refresh, often for long-lived tokens first. Test a full fresh authorization, not just refresh.

Rollout checklist

  • [ ] Add a ResponseReceived listener that logs Deprecation, Sunset and Link headers
  • [ ] Pin every provider's API version in config
  • [ ] Create docs/integrations.yml with owner, version, notice channel and review date
  • [ ] Point provider account emails to a team alias
  • [ ] Wrap each provider in an adapter interface
  • [ ] Add a nightly contract test suite against sandboxes
  • [ ] Review the register quarterly as part of the maintenance retainer or your own ops calendar

Frequently asked questions

What is the difference between the Deprecation and Sunset headers?

Deprecation (RFC 9745) says a resource is or will be deprecated, meaning you should stop relying on it. Sunset (RFC 8594) gives the date it is expected to stop responding. Deprecation can arrive long before any sunset date.

Do major API providers send these headers?

Some do and many don't. Treat the headers as one signal among several. The dependency register and the provider's changelog are still necessary for the ones that only announce changes by email or blog post.

How often should we review third-party API versions?

Quarterly works for most business systems. Review sooner if a provider has a known short version lifecycle, or if the header listener logs a new deprecation.

Should we upgrade to every new API version?

No. Upgrade when the current version has an end-of-life date, when you need a new feature, or when a security fix requires it. Each upgrade should be a reviewed change with contract tests.

What should happen when the listener logs a deprecation?

Create a ticket for the integration owner with the host, path, dates and link. Plan the migration well before the sunset date.

Is this worth it for a small application?

Yes, because the cost is small. The listener and the register take an afternoon, and one avoided payment or courier outage pays for it.


Follow & Connect

Top comments (0)