Dial x402

API Stability

Our deprecation promise, the rolling sunset window, and the live stability scorecard.

API Stability

We keep aliases around. We don't pull paths out from under you.

If we rename an endpoint, the old path keeps working. There is no fixed calendar date for removal — the only signal you'll ever see is a rolling sunset in the response headers that tells you the earliest we could remove it. As long as your traffic keeps arriving, that floor rolls forward.

This page documents the policy, the headers, the live deprecations endpoint, and the internal scorecard we grade ourselves against release-by-release.

Our deprecation promise

  1. Aliases are cheap. When we rename a path, we add the old name to a rewrite table and leave it there. Removal is a deliberate human decision, never a calendar trigger.
  2. No fixed sunset dates. The Sunset: header publishes a rolling floor — the earliest we could remove this path. It never points backward and rolls forward as your traffic continues.
  3. One global policy. Every alias follows the same window (/api/v1/deprecations shows the current values). Special cases — a path we genuinely need to remove on a tighter timeline for security reasons — are filed as separate policy decisions, not per-entry knobs.
  4. Human-in-the-loop removal. Even when our internal cron flags an alias as removal-eligible, the default action is to leave it alone.

How the rolling window works

Three policy constants live in apps/web/lib/api-stability/policy.ts:

ConstantDefaultMeaning
minimumGraceDays180Floor — no entry is ever removal-eligible before addedAt + 180d.
inactivityDays60Hits must be zero for this many consecutive days.
noticeWindowDays60Per-caller: ≥ this many days since their last deprecation notice.

The Sunset: header on a deprecated response is computed as:

floor       = addedAt    + minimumGraceDays
activityEnd = lastHitAt  + (inactivityDays + noticeWindowDays)
sunset      = max(floor, activityEnd, now + noticeWindowDays)

So:

  • Path just deprecated, still seeing trafficSunset = max(floor, lastHit + 120d). Honest about "earliest possible" and rolls forward every time you hit it.
  • Path quiet for many weeks → tightens, but never closer than 60 days from now.

Response headers on deprecated paths

Every response from an aliased path carries:

Deprecation: true                                                  RFC 9745
Link: </api/v1/numbers/buy>; rel="successor-version",              RFC 8288
      <https://x402.dial.wtf/docs/api-reference/stability>; rel="deprecation",
      <https://x402.dial.wtf/docs/api-reference/stability>; rel="deprecation-policy"
Sunset: Sun, 16 Nov 2026 00:00:00 GMT                              RFC 8594 (rolling floor)
Warning: 299 - "deprecated; rolling sunset — see Link rel=deprecation-policy"

GET / HEAD requests get 308 Permanent Redirect with these headers. Body-bearing methods (POST / PUT / PATCH / DELETE) are rewritten transparently on the same request — the body reaches the successor handler intact, and the headers are stamped on the response.

Programmatic check — GET /api/v1/deprecations

curl https://x402.dial.wtf/api/v1/deprecations
{
  "policy": {
    "minimumGraceDays": 180,
    "inactivityDays": 60,
    "noticeWindowDays": 60,
    "docs": "https://x402.dial.wtf/docs/api-reference/stability"
  },
  "entries": [
    {
      "from": "/api/v1/lines/buy",
      "successor": "/api/v1/numbers/buy",
      "reason": "renamed numbers/buy",
      "addedAt": "2026-05-20",
      "minimumSunset": "2026-11-16",
      "estimatedSunset": "2026-11-16",
      "lastHitAt": null,
      "uniqueCallersLast30d": null
    }
  ]
}

Cached 5 minutes. No auth.

In @dial/api:

import { DialClient } from "@dial/api";
const client = new DialClient({ baseUrl: "https://x402.dial.wtf" });
const { policy, entries } = await client.apiStability.deprecations();

The SDK also emits a one-time console.warn per deprecated (path → successor) pair seen in your process — so a renamed path shows up the first time you hit it without you having to wire anything up.

How notifications reach you

Today, in-band: the headers above + the SDK warning + the /api/v1/deprecations endpoint. A daily cron (legacy-rewrite-review) aggregates the structured api.legacy_path_hit events that every alias emits, and is wired to email + dashboard banner pipelines as they land (tracked at #119). Org-level Slack / Discord delivery lands with #46.


Stability scorecard

Every row is a question we want to answer "yes" to before we tell a paying customer "the API is stable." We grade ourselves honestly so the gap turns into a roadmap.

MarkMeaning
Shipped, enforced in CI / middleware / code.
🟡Partially done — works in some places, not enforced.
Not started.

Versioning & naming

#StandardStatus
1Major version in the path (/v1/, /v2/) — never in headers only
2Minor/patch additions are additive-only within a major🟡
3Path renames add a rewrite alias and keep it indefinitely
4Endpoint shape changes ship as a new path; old path stays🟡 policy doc, this page
5Pre-1.0 surfaces labelled experimental / preview in docs and OpenAPI

Deprecation signalling (HTTP)

#StandardStatus
6Deprecation: true on every response from a deprecated path (RFC 9745)
7Sunset: published as a rolling floor (RFC 8594)
8Link: ...; rel="successor-version" (RFC 8288)
9Warning: 299 informational on the deprecated response
10OpenAPI "deprecated": true auto-derived from the rewrite table
11Per-method deprecation supported by the table shape🟡 table extensible

Rolling sunset window

#StandardStatus
12Minimum grace floor in months (default 180d)
13Activity-gated removal: zero-hit ≥60d and per-caller notice ≥60d✅ headers; ❌ persistence pending #98
14Per-caller notification pipeline triggered by observed traffic❌ pending #98 + #119
15Removal is a human-in-the-loop confirmation, never automated✅ by design
16Single global policy, no per-entry overrides

Telemetry

#StandardStatus
17Hit counter, tagged by path / caller / version / UA✅ stderr events; ❌ KV counter pending #98
18Admin dashboard: which deprecated paths still see traffic, by caller
19Alert: a new caller (UA never seen) hits a deprecated path
20Top callers per deprecated path — auto-drafts a notification❌ pending #119

Contract testing & breaking-change detection

#StandardStatus
21OpenAPI spec is the source of truth, generated from Zod + catalog
22CI diffs OpenAPI between main and the PR, fails on breaking changes❌ #117
23Generated client (@dial/api) regenerated + version-bumped in same PR🟡 drift check exists, semver bumping not enforced
24Backwards-compat snapshot suite per route per version
25Mock server published from the OpenAPI spec

Idempotency, retries, and write safety

#StandardStatus
26All write endpoints accept Idempotency-Key🟡 x402 settlement hashes only — #96
27Idempotency keys persist ≥24h🟡 same
28Idempotency conflicts (same key, different body) → 409
29Documented retry-safe vs. non-retry-safe error codes🟡

Errors

#StandardStatus
30Error codes are stable strings, never repurposed
31Central error-code registry / docs page
32Errors include a request_id echoing X-Request-Id
33Errors use a stable { type, code, message } triplet🟡

Rate limiting & quotas

#StandardStatus
34RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset headers❌ #98
35429 responses include Retry-After
36Quota state queryable at /account / /usage🟡

Webhooks & async surfaces

#StandardStatus
37Webhook payload format is versioned independently from REST🟡 #114
38Non-2xx webhook deliveries are replayed with exponential backoff❌ #114
39Webhook signing-key rotation is zero-downtime + documented❌ #114

Documentation & change communication

#StandardStatus
40Machine-readable changelog (CHANGELOG.md or /api/changelog JSON)❌ #119
41Human changelog page with "Migration from X to Y" guides❌ #119
42Email / in-dashboard notice triggered by observed traffic on a deprecated path❌ pending #119
43Status page (uptime + incidents)❌ #106
44Public roadmap🟡 GitHub Issues

Authentication & key lifecycle

#StandardStatus
45Long-lived programmatic API keys with scopes❌ #61
46Key rotation: create new → swap → revoke old, zero-downtime❌ #61
47Test-mode vs. live-mode keys, distinguishable by prefix❌ #61
48Keys can be restricted to source IPs / scopes❌ #61

SDK lifecycle

#StandardStatus
49Official SDK auto-regenerated from OpenAPI on every release🟡 #117
50SDK pins a minimum supported API version
51SDK surfaces deprecation warnings✅ warn-once per (path → successor)
52SDK has its own semver + changelog independent of the API🟡 #117 / #119

On this page