Hermes Wiki
Developer/AppIntegration/ThirdPartyAPIs/Fundamentals/depending-on-a-contract-you-dont-control

Depending on a Contract You Don't Control

Concept

Calling a third-party API is a fundamentally different risk than calling your own internal service, even though the HTTP mechanics look identical. An internal service's contract, deploy schedule, and failure modes are within your organization's control — you can coordinate a breaking change, read the on-call runbook, or page the team that owns it. A third-party API's contract belongs to someone else entirely: they can deprecate a field, tighten a rate limit, rotate their own infrastructure, or have an outage on their own timeline, and your only visibility into any of it is whatever changelog, status page, or support channel they choose to publish. Resilience mechanics like timeouts, retries with backoff, and circuit breakers (see Retry Strategies and Circuit Breaker Pattern) protect you from a third party being slow or down, but they don't protect you from a third party changing what it does — that's a distinct category of risk this note focuses on.

Three concerns are specific to a contract you don't own: rate limit compliance, where the provider tells you your quota via response headers or documentation and you're expected to police yourself before they do it for you with a hard block; contract drift, where a field gets deprecated, a response shape changes, or an endpoint is sunset on the vendor's schedule, not yours; and credential lifecycle, where the API key or OAuth token you're calling with was issued by someone else's identity system and can be revoked, expired, or rotated outside your deploy pipeline.

Tradeoffs

Integration posture Benefit Cost
Call the vendor API directly wherever needed, no abstraction layer Fastest to ship, least code Every call site has to independently handle rate limits, auth refresh, and error mapping; a vendor contract change means hunting down every call site
A thin internal wrapper/adapter around the vendor SDK One place to update on a contract change, one place to enforce rate-limit and retry policy consistently Extra layer to build and maintain even when the vendor never changes anything
Fully abstracted "provider interface" designed for swapping vendors (e.g. payments, SMS) Vendor lock-in risk is minimized; a bad vendor can be replaced without touching call sites Real cost to design a lowest-common-denominator interface across vendors with genuinely different capabilities — often abstracts away exactly the vendor-specific feature you picked that vendor for
Ignore the vendor's stated rate limit and let retries/backoff absorb 429s No proactive work needed Wastes request budget on calls guaranteed to fail, and a vendor that treats repeated 429s as abuse may hard-block or throttle the account rather than just rejecting individual calls

The wrapper-layer middle ground is the right default for most integrations: enough indirection to have one place to enforce rate-limit and auth-refresh policy and to absorb a contract change, without over-investing in a multi-vendor abstraction for an integration that will realistically only ever have one provider.

When to use / when not to

  • Build a thin wrapper the moment more than one call site talks to the same third-party API — the first contract change or auth-refresh bug will otherwise need to be fixed in N places instead of one.
  • Read and respect the vendor's documented rate limit and Retry-After/X-RateLimit-* response headers proactively, rather than discovering the limit by hitting 429s in production — most providers (Stripe, GitHub, Twilio) publish exact per-endpoint quotas.
  • Subscribe to the vendor's changelog or status page and treat "breaking change coming" notices as a real deadline, the same way you'd treat an internal deprecation notice — the difference is nobody inside your org will chase you about it.
  • Invest in a full multi-vendor abstraction only when a second vendor is a genuine near-term possibility (e.g. a payments processor where regulatory or cost pressure could force a switch) — building one "just in case" for an integration that will only ever have one real provider is speculative cost for no realized benefit.
  • Don't treat a third-party webhook as a trusted internal event without verifying its signature — see Stripe's thin-events webhook design for how a well-designed vendor minimizes your exposure to their own payload changes.

Common pitfall

Hardcoding a vendor's current response shape and error codes directly into business logic scattered across the codebase, so that when the vendor adds a new field, renames one, or introduces a new error code, the failure shows up as a confusing downstream bug (a null pointer, a silently-mishandled error branch) rather than a clear, single-point contract mismatch. The fix is the same wrapper-layer discipline as above: parse and validate the vendor's response shape in exactly one place, fail loudly and specifically there if it doesn't match what's expected, and let everything downstream depend on your own stable internal type rather than the vendor's raw response. This also makes credential rotation (see Secrets Management and Rotation) tractable — a vendor forcing a key rotation on their own schedule only touches the one wrapper that holds the credential, not every call site.

Engineering Lens

The judgment call that separates a well-integrated third-party dependency from a fragile one isn't which resilience library gets used — it's whether the team ever explicitly asked "what happens to us the day this vendor changes something without telling us in advance, or has an outage lasting an hour, or revokes our key by mistake." Teams that treat a vendor integration as just another internal API call, with no wrapper boundary and no monitoring of the vendor's own health independent of their own service's health, discover the answer during an incident instead of during design. The stronger answer in a review is naming the specific blast radius — which call sites break, whether there's a graceful degradation path (queue for later, show a cached value, disable the feature) — rather than assuming a generic circuit breaker alone makes the dependency safe.

Sources

Hermes Wiki