{
  "openapi": "3.1.0",
  "info": {
    "title": "DIDHub API",
    "version": "0.2.0",
    "description": "Customer-facing REST API for DIDHub — phone numbers, SIP trunks, billing, account management.\n\n## Envelope format\n\nEvery JSON response uses an envelope: `{ ok: true, data: <T> }` on success, `{ ok: false, error: { code, message, fields? } }` on failure.\n\n## Authentication\n\nTwo schemes:\n\n1. **Session cookie (`didhub_sid`)** — set by `POST /v1/auth/login` and `POST /v1/auth/verify-email`. HMAC-signed. Used by the DIDHub dashboard SPA.\n2. **Bearer API key** (`Authorization: Bearer didhub_live_…`) — issued via `POST /v1/api-keys`. Programmatic API keys are in beta; contact `support@didhub.io` to enable for your account.\n\nMost endpoints accept either. `/v1/auth/*` (signup, login, password reset, etc.) are public.\n\n## Money\n\nAll monetary values are signed integer **cents** unless explicitly suffixed `_display`. Currency is USD for v1. Prices shown are the published customer rates; refer to your billing dashboard for any negotiated rates that apply to your account.\n\n## Rate limits & idempotency\n\nMutating endpoints accept an optional `Idempotency-Key` header. Re-sending the same key with the same body within 24 hours returns the original response. Rate limits are tier-based; exceeded limits return `429` with `Retry-After`.",
    "contact": {
      "name": "DIDHub support",
      "email": "support@didhub.io",
      "url": "https://didhub.io"
    },
    "license": { "name": "Proprietary" }
  },
  "servers": [
    { "url": "https://api.didhub.io/v1", "description": "Production" },
    { "url": "https://api.didhub.io", "description": "Production (no /v1 prefix — for /webhooks/* and /health)" }
  ],
  "security": [
    { "sessionCookieAuth": [] },
    { "bearerAuth": [] }
  ],
  "tags": [
    { "name": "Health", "description": "Liveness checks" },
    { "name": "Auth", "description": "Signup, login, password reset, email verification" },
    { "name": "MFA", "description": "TOTP enrollment + challenge" },
    { "name": "OAuth", "description": "Google + Microsoft sign-in flows" },
    { "name": "Sessions", "description": "Active web sessions + sign-in history" },
    { "name": "Accounts", "description": "Account profile + closure flow" },
    { "name": "API Keys", "description": "Programmatic credentials for /v1/* API calls" },
    { "name": "Catalog", "description": "Search DID availability across our carrier inventory" },
    { "name": "Cart", "description": "KV-backed shopping cart + atomic bulk checkout" },
    { "name": "Billing", "description": "Balance, transactions, Stripe top-up" },
    { "name": "Dashboard", "description": "Aggregated KPIs for the home tab — KV-cached, 5-minute TTL" },
    { "name": "Numbers", "description": "Order, list, route, release DIDs" },
    { "name": "SIP Trunks", "description": "SIP trunks + gateways" },
    { "name": "Routing Profiles", "description": "Named call-routing policies (failover, round-robin, weighted, simultaneous-ring) composed of one or more targets (SIP trunks, PSTN forwards, webhooks)" },
    { "name": "CDRs", "description": "Call Detail Records — read-only history of inbound and outbound calls with duration, hangup cause, and the customer charge for the call" },
    { "name": "Compliance", "description": "End users, identity documents, and compliance bundles for regulator-required address-of-record and KYC.\n\n**Data-controller responsibility:** these endpoints accept and return personal data your end-users entrust to you (names, dates of birth, document numbers, addresses). You are the data controller for this information under GDPR/CCPA — ensure your collection, storage, and downstream use is compliant, including obtaining lawful basis and honoring deletion requests." }
  ],
  "components": {
    "securitySchemes": {
      "sessionCookieAuth": {
        "type": "apiKey",
        "in": "cookie",
        "name": "didhub_sid",
        "description": "Session cookie. Set automatically by /v1/auth/login + /v1/auth/verify-email. HMAC-signed; `Domain=.didhub.io`."
      },
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "didhub_live_… (issued via POST /v1/api-keys)",
        "description": "Programmatic API key issued via POST /v1/api-keys. API keys are currently in beta — contact support@didhub.io to enable for your account."
      }
    },
    "parameters": {
      "AccountIdInHeader": {
        "name": "X-Account-Id",
        "in": "header",
        "description": "Optional account context for multi-account users. Defaults to the account on the session.",
        "required": false,
        "schema": { "type": "string", "example": "acct_example_abc123" }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": ["ok", "error"],
        "properties": {
          "ok": { "type": "boolean", "enum": [false] },
          "error": {
            "type": "object",
            "required": ["message"],
            "properties": {
              "code": { "type": "string", "description": "Machine-readable error code", "example": "insufficient_balance" },
              "message": { "type": "string", "description": "Human-readable message; safe to surface in UI", "example": "Balance $2.50 is below the order total of $5.00." },
              "fields": {
                "type": "object",
                "description": "Per-field validation errors (mirrors the request body shape)",
                "additionalProperties": { "type": "string" },
                "example": { "country": "Required, 2-letter ISO code" }
              }
            }
          }
        }
      },

      "Account": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "example": "acct_example_abc123" },
          "name": { "type": "string", "example": "DIDHub Test" },
          "tier": { "type": "string", "enum": ["self_serve", "manual_review", "verified", "wholesale"] },
          "country": { "type": "string", "description": "ISO-2", "example": "US" },
          "billing_mode": { "type": "string", "enum": ["prepaid", "postpaid"] },
          "status": { "type": "string", "enum": ["active", "suspended", "pending_closure", "closed"] },
          "balance": {
            "type": "object",
            "properties": {
              "amount": { "type": "number", "example": 5.0 },
              "amount_cents": { "type": "integer", "example": 500 },
              "currency": { "type": "string", "example": "USD" }
            }
          },
          "closure_grace_ends_at": { "type": "integer", "nullable": true, "description": "Unix seconds when account.status==pending_closure ends and account closes" },
          "created_at": { "type": "integer", "description": "Unix seconds" }
        }
      },

      "User": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "example": "usr_example_abc123" },
          "email": { "type": "string", "format": "email" },
          "name": { "type": "string" },
          "role": { "type": "string", "enum": ["owner", "admin", "billing", "member", "read_only"] },
          "email_verified": { "type": "boolean" },
          "phone_verified": { "type": "boolean" },
          "mfa_method": { "type": "string", "enum": ["totp", "email_otp"], "nullable": true }
        }
      },

      "Session": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "example": "ses_example_abc123" },
          "device_name": { "type": "string", "example": "Chrome on macOS" },
          "ip": { "type": "string", "nullable": true, "example": "129.213.69.69" },
          "ip_country": { "type": "string", "nullable": true, "example": "US" },
          "city": { "type": "string", "nullable": true },
          "is_trusted": { "type": "boolean", "description": "Set when user ticked \"Remember this device\" at login" },
          "is_current": { "type": "boolean", "description": "True when this row matches the cookie on the calling request" },
          "last_used_at": { "type": "integer" },
          "expires_at": { "type": "integer" },
          "created_at": { "type": "integer" }
        }
      },

      "LoginEvent": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "example": "evt_example_abc123" },
          "outcome": {
            "type": "string",
            "enum": ["success", "wrong_password", "no_such_user", "mfa_required", "mfa_failed", "suspicious", "locked_out", "fraud_screen_blocked", "fraud_screen_review", "unverified_email"]
          },
          "method": { "type": "string", "enum": ["password", "oauth_google", "oauth_microsoft", "magic_link"] },
          "ip": { "type": "string", "nullable": true },
          "ip_country": { "type": "string", "nullable": true },
          "is_new_device": { "type": "boolean" },
          "is_new_location": { "type": "boolean" },
          "user_agent": { "type": "string", "nullable": true },
          "fraud_decision": { "type": "string", "enum": ["allow", "review", "block"], "nullable": true },
          "ts": { "type": "integer" }
        }
      },

      "ApiKeySummary": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "example": "akey_example_abc123" },
          "name": { "type": "string", "example": "Production webhook signer" },
          "prefix_display": { "type": "string", "description": "Visible portion + ellipsis", "example": "didhub_live_UQcs…" },
          "scopes": { "type": "array", "items": { "type": "string" }, "example": ["*"] },
          "expires_at": { "type": "integer", "nullable": true },
          "last_used_at": { "type": "integer", "nullable": true },
          "last_used_ip": { "type": "string", "nullable": true },
          "revoked_at": { "type": "integer", "nullable": true },
          "created_at": { "type": "integer" },
          "status": { "type": "string", "enum": ["active", "revoked", "expired"] }
        }
      },

      "CreatedApiKey": {
        "type": "object",
        "description": "Returned ONCE on creation. The raw `key` is never recoverable — store it immediately.",
        "properties": {
          "id": { "type": "string", "example": "akey_example_abc123" },
          "name": { "type": "string" },
          "key": { "type": "string", "description": "Raw secret. Format: didhub_live_<32-char base64url>", "example": "didhub_live_UQcsmrT6I5FS18Z6xwgHYjVYq37X43g6WatYHVrL3k8" },
          "prefix_display": { "type": "string" },
          "scopes": { "type": "array", "items": { "type": "string" } },
          "expires_at": { "type": "integer", "nullable": true },
          "created_at": { "type": "integer" }
        }
      },

      "PaymentMethod": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "example": "pm_example_abc123" },
          "stripe_payment_method_id": { "type": "string", "example": "pm_1ExampleStripeMethodId" },
          "type": { "type": "string", "example": "card" },
          "brand": { "type": "string", "nullable": true, "example": "visa" },
          "last4": { "type": "string", "nullable": true, "example": "4242" },
          "exp_month": { "type": "integer", "nullable": true, "example": 12 },
          "exp_year": { "type": "integer", "nullable": true, "example": 2030 },
          "is_default": { "type": "boolean", "description": "Default = the one auto-recharge will target" },
          "created_at": { "type": "integer" }
        }
      },

      "AutoRechargeConfig": {
        "type": "object",
        "description": "When `enabled: true`, the worker fires an off-session PaymentIntent against the default saved card whenever a debit drops `account.balance_cents` below `threshold_cents`. KV-locked so parallel debits can't double-charge.",
        "properties": {
          "enabled": { "type": "boolean" },
          "threshold_cents": { "type": "integer", "nullable": true, "minimum": 100, "description": "Recharge when balance drops below this (≥ $1.00)" },
          "amount_cents": { "type": "integer", "nullable": true, "minimum": 500, "description": "Amount to recharge (≥ $5.00, ≤ $10,000)" }
        }
      },

      "BillingTransaction": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "example": "bt_example_abc123" },
          "kind": { "type": "string", "enum": ["topup", "charge", "refund", "trial_credit", "adjustment", "chargeback"] },
          "amount_cents": { "type": "integer", "description": "Signed; +credit, -debit" },
          "amount_display": { "type": "string", "example": "$5.00 USD" },
          "currency": { "type": "string", "example": "USD" },
          "description": { "type": "string", "example": "Welcome $5 trial credit" },
          "related_kind": { "type": "string", "nullable": true, "description": "Loose FK to related entity (number, invoice, etc.)" },
          "related_id": { "type": "string", "nullable": true },
          "stripe_payment_intent_id": { "type": "string", "nullable": true },
          "balance_after_cents": { "type": "integer" },
          "balance_after_display": { "type": "string" },
          "created_at": { "type": "integer" }
        }
      },

      "ClosureRequest": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "example": "cls_example_abc123" },
          "reason": { "type": "string", "nullable": true },
          "reason_category": { "type": "string", "enum": ["price", "feature", "consolidation", "other"], "nullable": true },
          "requested_at": { "type": "integer" },
          "grace_expires_at": { "type": "integer" },
          "requested_by_user_id": { "type": "string" },
          "days_remaining": { "type": "integer", "description": "Computed from now → grace_expires_at" }
        }
      },

      "CatalogResult": {
        "type": "object",
        "properties": {
          "e164": { "type": "string", "example": "+12125550100" },
          "country": { "type": "string", "example": "US" },
          "number_type": { "type": "string", "enum": ["geographic", "national", "mobile", "tollfree", "inum"] },
          "region": { "type": "string", "description": "Provider-supplied region label" },
          "city": { "type": "string", "nullable": true },
          "area_code": { "type": "string", "nullable": true, "example": "212" },
          "capabilities": { "type": "array", "items": { "type": "string" }, "example": ["voice", "sms", "fax", "t38"] },
          "monthly_price": { "type": "number", "description": "Customer-facing monthly recurring (USD)", "example": 2.99 },
          "setup_price": { "type": "number", "description": "One-time setup (USD)", "example": 0 },
          "currency": { "type": "string", "example": "USD" },
          "requires_activation": { "type": "boolean", "description": "True when supplier requires documents before number becomes usable" },
          "restrictions": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "type": { "type": "string", "enum": ["legal", "service", "purchase", "document"] },
                "message": { "type": "string" },
                "compliance_code": { "type": "string", "nullable": true }
              }
            }
          },
          "select_token": { "type": "string", "description": "Opaque token to pass into POST /v1/numbers to order this result. Only valid for the search session." },
          "from_catalog": { "type": "boolean", "description": "True for D1-availability rows (specific e164 picked at checkout); false for live-supplier results (specific e164 reserved)" }
        }
      },

      "CartItemSnapshot": {
        "type": "object",
        "description": "Price + display info captured at add-to-cart time. SPA can render the cart purely from this — no need to re-query catalog. Re-evaluated at checkout (supplier's current price wins).",
        "properties": {
          "e164": { "type": "string", "description": "Specific number for live results; placeholder like `+212…` for from_catalog rows" },
          "country": { "type": "string" },
          "number_type": { "type": "string" },
          "area_code": { "type": "string", "nullable": true },
          "region": { "type": "string", "nullable": true },
          "monthly_price_cents": { "type": "integer" },
          "setup_price_cents": { "type": "integer" },
          "currency": { "type": "string" },
          "capabilities": { "type": "array", "items": { "type": "string" } },
          "from_catalog": { "type": "boolean" }
        }
      },

      "CartItem": {
        "allOf": [
          { "$ref": "#/components/schemas/CartItemSnapshot" },
          {
            "type": "object",
            "properties": {
              "id": { "type": "string", "example": "ci_example_abc123" },
              "select_token": { "type": "string", "description": "The same token from /catalog/search; used at checkout" },
              "added_at": { "type": "integer", "description": "Unix seconds" }
            }
          }
        ]
      },

      "CartTotals": {
        "type": "object",
        "properties": {
          "item_count": { "type": "integer" },
          "monthly_cents": { "type": "integer", "description": "Sum of snapshot monthly_price_cents across items" },
          "setup_cents": { "type": "integer" },
          "total_due_cents": { "type": "integer", "description": "monthly_cents + setup_cents — what the customer will be charged at checkout" },
          "currency": { "type": "string" }
        }
      },

      "Cart": {
        "type": "object",
        "properties": {
          "items": { "type": "array", "items": { "$ref": "#/components/schemas/CartItem" } },
          "totals": { "$ref": "#/components/schemas/CartTotals" },
          "created_at": { "type": "integer", "nullable": true },
          "updated_at": { "type": "integer", "nullable": true },
          "expires_at_estimate": { "type": "integer", "nullable": true, "description": "Best-effort: updated_at + 4h. Refreshed on every cart write." }
        }
      },

      "CheckoutItemOutcome": {
        "type": "object",
        "properties": {
          "cart_item_id": { "type": "string" },
          "select_token": { "type": "string" },
          "snapshot": { "$ref": "#/components/schemas/CartItemSnapshot" },
          "order": { "oneOf": [{ "$ref": "#/components/schemas/NumberOrder" }, { "type": "null" }] },
          "number": { "oneOf": [{ "$ref": "#/components/schemas/Number" }, { "type": "null" }] },
          "status": { "type": "string", "enum": ["provisioned", "pending_documents", "failed"] },
          "error_code": { "type": "string", "nullable": true },
          "error_message": { "type": "string", "nullable": true }
        }
      },

      "CheckoutOutcome": {
        "type": "object",
        "properties": {
          "checkout_id": { "type": "string", "example": "co_example_abc123" },
          "status": { "type": "string", "enum": ["all_provisioned", "partial", "all_failed", "balance_too_low"] },
          "totals": { "$ref": "#/components/schemas/CartTotals" },
          "charged_cents": { "type": "integer", "description": "Sum of successful per-item charges (one billing_transaction per provisioned number)" },
          "items": { "type": "array", "items": { "$ref": "#/components/schemas/CheckoutItemOutcome" } },
          "created_at": { "type": "integer" }
        }
      },

      "Number": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "example": "num_example_abc123" },
          "e164": { "type": "string", "example": "+12125550100" },
          "country": { "type": "string" },
          "number_type": { "type": "string", "enum": ["geographic", "national", "mobile", "tollfree", "inum"] },
          "area_code": { "type": "string", "nullable": true },
          "region": { "type": "string", "nullable": true },
          "city": { "type": "string", "nullable": true },
          "capabilities": { "type": "array", "items": { "type": "string" } },
          "monthly_price_cents": { "type": "integer" },
          "setup_price_cents": { "type": "integer" },
          "currency": { "type": "string" },
          "status": { "type": "string", "enum": ["active", "pending_documents", "suspended", "released"], "description": "`active` = routable; `pending_documents` = waiting on compliance bundle; `suspended` = no longer routable (e.g. non-payment); `released` = let go. Note: \"past due\" is **not** a status — it's derived from `failed_billing_attempts > 0 && status == 'active'`." },
          "requires_activation": { "type": "boolean" },
          "assigned_at": { "type": "integer", "description": "Unix seconds when customer ordered" },
          "next_billing_at": { "type": "integer", "description": "Unix seconds when next monthly charge fires" },
          "released_at": { "type": "integer", "nullable": true },
          "release_scheduled_at": { "type": "integer", "nullable": true, "description": "Unix seconds when a scheduled release will finalise. Set when customer schedules release at end-of-cycle, or when the monthly-billing cron flips a number to `suspended` after 7 failed charges (release in 30 days). The hourly release-finalize cron actions this." },
          "trunk_id": { "type": "string", "nullable": true, "description": "Optional SIP trunk this number routes to" },
          "routing_profile_id": { "type": "string", "nullable": true, "description": "Optional routing profile applied to inbound calls (failover, ring-strategy, etc.). See /routing-profiles." },
          "compliance_bundle_id": { "type": "string", "nullable": true, "description": "Optional compliance bundle (address proof, ID docs) attached to satisfy local-presence requirements. See /compliance." },
          "failed_billing_attempts": { "type": "integer", "description": "Past-due counter. Zero on healthy numbers; > 0 means the monthly-billing cron tried to charge and the account couldn't cover it (e.g. balance below price, payment method failed). Cron retries daily; at attempt 7 the number flips to `status='suspended'` and `release_scheduled_at = now + 30d`. UI should surface a \"past due, top up to keep the number\" badge when `failed_billing_attempts > 0 && status == 'active'`." },
          "last_billing_attempt_at": { "type": "integer", "nullable": true, "description": "Unix seconds of the most recent monthly-billing cron attempt (success or failure). Null on freshly provisioned numbers that have not yet had a cycle." },
          "last_billing_failure_code": { "type": "string", "nullable": true, "description": "Most recent failure reason from the billing cron, e.g. `insufficient_balance`, `payment_method_declined`. Cleared (set null) on the next successful charge. Surfaced raw for forensics." }
        }
      },

      "DashboardSummary": {
        "type": "object",
        "description": "Aggregated home-tab metrics. Served from cache with a 5-minute TTL — typical warm read is ~150ms vs ~700ms cold. Invalidated immediately on any balance-affecting event (top-up, billing, number release) so freshness is sub-second in practice. Inspect `cached_at` to display \"as of HH:MM\" or to decide whether to force-bypass the cache.",
        "properties": {
          "active_numbers": { "type": "integer", "description": "Count of `number` rows where status='active' for this account." },
          "numbers_added_this_month": { "type": "integer", "description": "Numbers assigned with `assigned_at >= start_of_current_month`." },
          "calls_today": { "type": "integer", "description": "CDR count for today (UTC midnight cutoff)." },
          "calls_yesterday": { "type": "integer", "description": "CDR count for the previous UTC day." },
          "calls_change_pct": { "type": "number", "nullable": true, "description": "Percent change from yesterday → today. Null when yesterday=0 (division by zero); UI should render \"—\"." },
          "minutes_30d": { "type": "integer", "description": "Total billable minutes across the trailing 30 days (rounded)." },
          "seconds_30d": { "type": "integer", "description": "Total billable seconds across the trailing 30 days (un-rounded source for the minutes field)." },
          "avg_call_seconds": { "type": "integer", "description": "Mean billable duration over the trailing 30 days." },
          "completion_pct": { "type": "number", "description": "ASR proxy — share of CDRs in the last 30d with `hangup_cause='normal_clearing'`. 0-100." },
          "spend_this_month_cents": { "type": "integer", "description": "Customer-billed spend for the current calendar month (debit-positive integer cents)." },
          "projected_monthly_cents": { "type": "integer", "description": "Linear-projected end-of-month spend = spend_this_month × (days_in_month / day_of_month). Naïve forecast for the home-tab tile." },
          "call_volume_daily": {
            "type": "array",
            "description": "Per-day CDR counts over the trailing 30 days, oldest first. Drives the home-tab sparkline.",
            "items": {
              "type": "object",
              "properties": {
                "day_start": { "type": "integer", "description": "Unix seconds at UTC midnight." },
                "date": { "type": "string", "format": "date", "description": "ISO date (YYYY-MM-DD), matches day_start." },
                "count": { "type": "integer" }
              }
            }
          },
          "top_countries": {
            "type": "array",
            "description": "Top destination countries by call count over the trailing 30 days (max 5).",
            "items": {
              "type": "object",
              "properties": {
                "country": { "type": "string", "description": "ISO-2." },
                "count": { "type": "integer" }
              }
            }
          },
          "recent_cdrs": {
            "type": "array",
            "description": "Last 10 CDRs for the account, newest first. Same shape as /cdrs list entries, trimmed.",
            "items": {
              "type": "object",
              "properties": {
                "id": { "type": "string" },
                "direction": { "type": "string", "enum": ["inbound", "outbound"] },
                "caller": { "type": "string", "nullable": true, "description": "E.164 or SIP user — privacy-redacted upstream if applicable." },
                "callee": { "type": "string", "nullable": true },
                "started_at": { "type": "integer", "description": "Unix seconds." },
                "billable_duration_s": { "type": "integer" },
                "customer_total_cents": { "type": "integer" },
                "currency": { "type": "string" },
                "hangup_cause": { "type": "string", "nullable": true, "description": "FreeSWITCH-style cause, e.g. `normal_clearing`, `user_busy`, `no_answer`." },
                "sip_response_code": { "type": "integer", "nullable": true, "description": "Final SIP status code, e.g. 200, 486, 487." }
              }
            }
          },
          "balance_cents": { "type": "integer", "description": "Current account balance — included so the home tab doesn't need a second round-trip to /billing/balance. Updated immediately whenever a balance-mutating endpoint is hit (KV cache is invalidated)." },
          "account_status": { "type": "string", "enum": ["active", "suspended", "pending_closure", "closed"], "description": "Current account.status — included so the dashboard can render \"closure pending\" or \"suspended\" banners without a separate /accounts/me round-trip." },
          "cached_at": { "type": "integer", "description": "Unix seconds when this summary was computed. On a cold read this is `now`; on a warm read this is the timestamp of the original compute. Useful for clients that want to display \"as of HH:MM\" or decide whether to force-bypass cache." }
        }
      },

      "NumberOrder": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "example": "ord_example_abc123" },
          "status": { "type": "string", "enum": ["pending", "provisioned", "failed", "cancelled", "pending_documents"] },
          "request_e164": { "type": "string", "nullable": true, "description": "Specific number requested, or null for \"any in area\"" },
          "country": { "type": "string" },
          "number_type": { "type": "string" },
          "area_code": { "type": "string", "nullable": true },
          "monthly_price_cents": { "type": "integer" },
          "setup_price_cents": { "type": "integer" },
          "currency": { "type": "string" },
          "error_code": { "type": "string", "nullable": true },
          "error_message": { "type": "string", "nullable": true },
          "assigned_number_id": { "type": "string", "nullable": true },
          "created_at": { "type": "integer" },
          "completed_at": { "type": "integer", "nullable": true }
        }
      },

      "SipTrunk": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "example": "trnk_example_abc123" },
          "name": { "type": "string", "example": "Production PBX" },
          "status": { "type": "string", "enum": ["active", "disabled", "archived"] },
          "transport": { "type": "string", "enum": ["udp", "tcp", "tls", "wss"] },
          "codec_preference": { "type": "array", "items": { "type": "string" }, "example": ["opus", "g722", "g711a", "g711u"] },
          "dtmf_mode": { "type": "string", "enum": ["rfc4733", "inband", "sip_info"] },
          "registration_mode": { "type": "string", "enum": ["static_ip", "register"] },
          "caller_id_strategy": { "type": "string", "enum": ["inherit", "override"] },
          "caller_id_override": { "type": "string", "nullable": true },
          "created_at": { "type": "integer" },
          "updated_at": { "type": "integer" },
          "archived_at": { "type": "integer", "nullable": true }
        }
      },

      "SipGateway": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "example": "gw_example_abc123" },
          "name": { "type": "string" },
          "host": { "type": "string", "description": "FQDN or IP literal" },
          "port": { "type": "integer", "minimum": 1, "maximum": 65535, "example": 5060 },
          "transport": { "type": "string", "enum": ["udp", "tcp", "tls", "wss"] },
          "weight": { "type": "integer", "minimum": 0, "maximum": 10000, "description": "Distribution weight within priority band" },
          "priority": { "type": "integer", "minimum": 0, "maximum": 99, "description": "Lower fires first; 0 = highest priority" },
          "auth_username": { "type": "string", "nullable": true },
          "has_auth_password": { "type": "boolean", "description": "Password itself is never returned by the API; stored encrypted at rest" },
          "custom_headers": { "type": "object", "additionalProperties": { "type": "string" }, "nullable": true },
          "enabled": { "type": "boolean" },
          "created_at": { "type": "integer" },
          "updated_at": { "type": "integer" }
        }
      },

      "RoutingProfile": {
        "type": "object",
        "description": "A named call-routing policy. Combines one or more targets (SIP trunks, PSTN forwards, webhooks) with a delivery strategy.",
        "properties": {
          "id": { "type": "string", "example": "rp_example_abc123" },
          "name": { "type": "string", "example": "Production fallback" },
          "status": { "type": "string", "enum": ["active", "disabled", "archived"] },
          "strategy": { "type": "string", "enum": ["failover", "round_robin", "weighted_lb", "simultaneous_ring"], "description": "**failover**: try targets in `position` order, advance on failure.\n**round_robin**: per-call rotation through enabled targets.\n**weighted_lb**: distribute by `weight` (0–10000).\n**simultaneous_ring**: ring all enabled targets at once; first answer wins." },
          "ring_timeout_s": { "type": "integer", "minimum": 5, "maximum": 120, "example": 25, "description": "Per-target ring duration before advancing to the next target (failover/round_robin) or giving up (simultaneous_ring)." },
          "max_retries": { "type": "integer", "minimum": 0, "maximum": 10, "example": 2 },
          "target_count": { "type": "integer", "description": "Number of targets attached to this profile" },
          "number_count": { "type": "integer", "description": "Number of active DIDs currently routing through this profile" },
          "created_at": { "type": "integer" },
          "updated_at": { "type": "integer" },
          "archived_at": { "type": "integer", "nullable": true }
        }
      },

      "RoutingProfileTarget": {
        "type": "object",
        "description": "A single destination within a routing profile. Exactly one of `sip_trunk_id`, `pstn_forward_e164`, `webrtc_user_id`, or `webhook_url` is set based on `kind`.",
        "properties": {
          "id": { "type": "string", "example": "rpt_example_abc123" },
          "profile_id": { "type": "string", "example": "rp_example_abc123" },
          "kind": { "type": "string", "enum": ["sip_trunk", "pstn_forward", "webrtc_user", "webhook"] },
          "position": { "type": "integer", "minimum": 0, "maximum": 99, "description": "Order within the profile (lower runs first). Ignored for round_robin and simultaneous_ring strategies." },
          "weight": { "type": "integer", "minimum": 0, "maximum": 10000, "description": "Distribution weight for `weighted_lb` strategy. Ignored for others." },
          "sip_trunk_id": { "type": "string", "nullable": true, "description": "When kind=sip_trunk: ID of an active SIP trunk on this account" },
          "pstn_forward_e164": { "type": "string", "nullable": true, "description": "When kind=pstn_forward: E.164 number to forward to (carrier rates apply)" },
          "webrtc_user_id": { "type": "string", "nullable": true, "description": "When kind=webrtc_user: ID of a registered WebRTC user (beta)" },
          "webhook_url": { "type": "string", "nullable": true, "description": "When kind=webhook: HTTPS URL to receive call routing requests (beta)" },
          "conditions": { "type": "object", "nullable": true, "description": "Optional matching conditions (e.g., time-of-day, caller country)", "additionalProperties": true },
          "enabled": { "type": "boolean" },
          "created_at": { "type": "integer" },
          "updated_at": { "type": "integer" }
        }
      },

      "RoutingProfileDetail": {
        "type": "object",
        "properties": {
          "profile": { "$ref": "#/components/schemas/RoutingProfile" },
          "targets": { "type": "array", "items": { "$ref": "#/components/schemas/RoutingProfileTarget" } }
        }
      },

      "Cdr": {
        "type": "object",
        "description": "A single Call Detail Record. List endpoint returns a trimmed view; the detail endpoint adds `customer_per_second_cents` and `created_at`.",
        "properties": {
          "id": { "type": "string", "example": "cdr_example_abc123" },
          "direction": { "type": "string", "enum": ["inbound", "outbound"] },
          "caller": { "type": "string", "nullable": true, "description": "Calling party (E.164 for PSTN, SIP URI for trunked). May be redacted upstream by privacy settings." },
          "callee": { "type": "string", "nullable": true, "description": "Called party (E.164 or SIP URI)" },
          "e164": { "type": "string", "nullable": true, "description": "The DIDHub-owned number on this leg" },
          "number_id": { "type": "string", "nullable": true, "description": "Your `Number.id` for the DID on this call" },
          "routing_profile_id": { "type": "string", "nullable": true, "description": "Routing profile that handled this call (if any)" },
          "sip_trunk_id": { "type": "string", "nullable": true, "description": "Trunk that delivered the call (if any)" },
          "forward_to_e164": { "type": "string", "nullable": true, "description": "Destination E.164 if the call was forwarded to PSTN" },
          "webrtc_user_id": { "type": "string", "nullable": true },
          "started_at": { "type": "integer", "description": "Unix seconds when the call set up" },
          "answered_at": { "type": "integer", "nullable": true, "description": "Unix seconds when the called party answered (null on unanswered)" },
          "ended_at": { "type": "integer", "description": "Unix seconds when the call ended" },
          "duration_s": { "type": "integer", "description": "Total call duration in seconds (setup→teardown)" },
          "billable_duration_s": { "type": "integer", "description": "Billable seconds (typically answered_at→ended_at)" },
          "hangup_cause": { "type": "string", "nullable": true, "description": "FreeSWITCH-style cause, e.g. `normal_clearing`, `user_busy`, `no_answer`, `originator_cancel`" },
          "sip_response_code": { "type": "integer", "nullable": true, "description": "Final SIP response code (200, 486, 487, 503, …)" },
          "customer_total_cents": { "type": "integer", "description": "Total amount charged to your account for this call (integer cents)" },
          "currency": { "type": "string", "example": "USD" },
          "billing_transaction_id": { "type": "string", "nullable": true, "description": "Link to the BillingTransaction row this call generated" }
        }
      },

      "CdrDetail": {
        "allOf": [
          { "$ref": "#/components/schemas/Cdr" },
          {
            "type": "object",
            "properties": {
              "customer_per_second_cents": { "type": "number", "description": "Effective per-second rate applied to this call" },
              "created_at": { "type": "integer", "description": "Unix seconds when the CDR row was written" }
            }
          }
        ]
      },

      "EndUser": {
        "type": "object",
        "description": "A person or business that owns/uses a phone number for compliance purposes (carriers and regulators require this in most countries).\n\n**You are the data controller** for the personal data in this record — see the Compliance tag description.",
        "properties": {
          "id": { "type": "string", "example": "eu_example_abc123" },
          "kind": { "type": "string", "enum": ["person", "business"] },
          "first_name": { "type": "string", "nullable": true, "example": "Jane" },
          "last_name": { "type": "string", "nullable": true, "example": "Doe" },
          "date_of_birth": { "type": "string", "nullable": true, "format": "date", "example": "1990-01-01", "description": "ISO date. Required by some carriers for person-kind end users." },
          "business_name": { "type": "string", "nullable": true, "example": "Example Holdings Ltd." },
          "business_registration_number": { "type": "string", "nullable": true, "example": "12345678" },
          "vat_number": { "type": "string", "nullable": true, "example": "GB123456789" },
          "email": { "type": "string", "nullable": true, "format": "email", "example": "jane@example.com" },
          "phone_e164": { "type": "string", "nullable": true, "example": "+15550100200" },
          "address_line1": { "type": "string", "example": "1 Example Street" },
          "address_line2": { "type": "string", "nullable": true },
          "city": { "type": "string", "example": "London" },
          "state": { "type": "string", "nullable": true, "example": "Greater London" },
          "postal_code": { "type": "string", "example": "SW1A 1AA" },
          "country": { "type": "string", "description": "ISO-2", "example": "GB" },
          "id_doc_type": { "type": "string", "nullable": true, "enum": ["passport", "drivers_license", "national_id", "business_registration", "tax_certificate"] },
          "id_doc_number": { "type": "string", "nullable": true, "example": "AB1234567", "description": "Document reference number. Example is fabricated — never include real document numbers in tickets or shared files." },
          "id_doc_country": { "type": "string", "nullable": true, "description": "ISO-2 country of issue", "example": "GB" },
          "notes": { "type": "string", "nullable": true, "description": "Free-text notes for your records" },
          "display_name": { "type": "string", "description": "Computed: business_name OR `first_name last_name`" },
          "document_count": { "type": "integer", "description": "Number of identity/proof documents attached to this end user" },
          "created_at": { "type": "integer" },
          "updated_at": { "type": "integer" }
        }
      },

      "ComplianceDocument": {
        "type": "object",
        "description": "An uploaded identity or address-proof document. Binary content is stored encrypted at rest; access requires a valid session on the owning account.",
        "properties": {
          "id": { "type": "string", "example": "doc_example_abc123" },
          "end_user_id": { "type": "string", "nullable": true, "example": "eu_example_abc123" },
          "doc_type": { "type": "string", "enum": ["passport", "drivers_license", "national_id", "utility_bill", "bank_statement", "tax_certificate", "business_registration", "vat_certificate", "other"] },
          "filename": { "type": "string", "example": "passport.pdf" },
          "content_type": { "type": "string", "enum": ["application/pdf", "image/jpeg", "image/png"] },
          "size_bytes": { "type": "integer", "description": "File size (max 2 MB)" },
          "issued_at": { "type": "string", "nullable": true, "format": "date" },
          "expires_at": { "type": "string", "nullable": true, "format": "date" },
          "notes": { "type": "string", "nullable": true },
          "created_at": { "type": "integer" }
        }
      },

      "ComplianceBundle": {
        "type": "object",
        "description": "A submission package — one end user plus the supporting documents required for a given regulator. Attaches to numbers via PATCH /numbers/{e164}/compliance.",
        "properties": {
          "id": { "type": "string", "example": "bun_example_abc123" },
          "name": { "type": "string", "example": "UK geographic — Jane Doe" },
          "end_user_id": { "type": "string", "example": "eu_example_abc123" },
          "country": { "type": "string", "description": "ISO-2 of the regulator/jurisdiction", "example": "GB" },
          "number_type": { "type": "string", "nullable": true, "enum": ["geographic", "national", "mobile", "tollfree", "inum"] },
          "status": { "type": "string", "enum": ["draft", "submitted", "approved", "rejected"] },
          "document_ids": { "type": "array", "items": { "type": "string" }, "description": "Documents currently attached" },
          "review_notes": { "type": "string", "nullable": true, "description": "Reviewer feedback when status=rejected" },
          "submitted_at": { "type": "integer", "nullable": true },
          "approved_at": { "type": "integer", "nullable": true },
          "rejected_at": { "type": "integer", "nullable": true },
          "created_at": { "type": "integer" },
          "updated_at": { "type": "integer" }
        }
      }
    }
  },

  "paths": {
    "/health": {
      "get": {
        "summary": "Health check",
        "description": "Liveness probe with binding status (D1, KV, EMAIL, ASSETS). No auth required.\n\n**Note:** this endpoint is at the root, not under /v1. Use the second server entry to address it.",
        "tags": ["Health"],
        "security": [],
        "servers": [{ "url": "https://api.didhub.io" }],
        "responses": {
          "200": {
            "description": "Worker healthy",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "status": { "type": "string", "example": "ok" },
                    "service": { "type": "string", "example": "didhub-api" },
                    "version": { "type": "string" },
                    "environment": { "type": "string" },
                    "timestamp": { "type": "string", "format": "date-time" },
                    "bindings": {
                      "type": "object",
                      "properties": {
                        "d1_catalog": { "type": "boolean" },
                        "kv_sessions": { "type": "boolean" },
                        "send_email": { "type": "boolean" },
                        "assets": { "type": "boolean" }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },

    "/auth/signup": {
      "post": {
        "summary": "Create a new account",
        "description": "Creates an account + first user, sends a verification email, grants a $5 trial credit. Runs FraudScreen on the source IP — `decision: review` puts the account in `manual_review` tier (still usable for browsing, blocked on first order); `decision: block` returns 403.",
        "tags": ["Auth"],
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["email", "password", "name", "country"],
                "properties": {
                  "email": { "type": "string", "format": "email" },
                  "password": { "type": "string", "minLength": 8 },
                  "name": { "type": "string", "minLength": 2 },
                  "company": { "type": "string" },
                  "country": { "type": "string", "description": "ISO-2", "example": "US" },
                  "phone_e164": { "type": "string", "example": "+15551234567" },
                  "intended_use": { "type": "string", "enum": ["itsp", "dev", "internal", "other"] },
                  "captcha_token": { "type": "string", "description": "Cloudflare Turnstile token. Currently accepts a stub value." },
                  "marketing_opt_in": { "type": "boolean" }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Account created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean", "enum": [true] },
                    "data": {
                      "type": "object",
                      "properties": {
                        "account_id": { "type": "string" },
                        "user_id": { "type": "string" },
                        "email": { "type": "string" },
                        "tier": { "type": "string" },
                        "next_step": { "type": "string", "example": "verify_email" },
                        "verification_email_sent": { "type": "boolean" },
                        "trial_credit_usd": { "type": "number", "example": 5 }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": { "description": "Blocked by FraudScreen", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "409": { "description": "Email already in use", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "422": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },

    "/auth/email/resend": {
      "post": {
        "summary": "Resend the verification email",
        "description": "Always returns 204 — does not leak whether the email exists or is already verified.",
        "tags": ["Auth"],
        "security": [],
        "requestBody": {
          "content": { "application/json": { "schema": { "type": "object", "properties": { "email": { "type": "string", "format": "email" } } } } }
        },
        "responses": { "204": { "description": "Always" } }
      }
    },

    "/auth/verify-email": {
      "post": {
        "summary": "Consume an email-verification token",
        "description": "Marks the user's email as verified and **auto-logs them in** (returns Set-Cookie). Tokens expire 24h after issue.",
        "tags": ["Auth"],
        "security": [],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "type": "object", "required": ["token"], "properties": { "token": { "type": "string" } } } } }
        },
        "responses": {
          "200": {
            "description": "Verified + session created",
            "headers": { "Set-Cookie": { "schema": { "type": "string", "example": "didhub_sid=ses_…; HttpOnly; Secure; SameSite=Lax; Domain=.didhub.io" } } },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean", "enum": [true] },
                    "data": { "type": "object", "properties": { "verified": { "type": "boolean" }, "user_id": { "type": "string" }, "next_step": { "type": "string", "example": "verify_phone" } } }
                  }
                }
              }
            }
          },
          "410": { "description": "Token invalid, used, or expired", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },

    "/auth/login": {
      "post": {
        "summary": "Password sign-in",
        "description": "On success sets the `didhub_sid` cookie. When the user has MFA enabled, returns `challenge: \"mfa\"` and the session is in partial state — call `POST /v1/auth/mfa/challenge` to complete.",
        "tags": ["Auth"],
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["email", "password"],
                "properties": {
                  "email": { "type": "string", "format": "email" },
                  "password": { "type": "string" },
                  "trust_device": { "type": "boolean", "description": "\"Remember this device\" — extends device-trust window" }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Signed in (or partial if challenge present)",
            "headers": { "Set-Cookie": { "schema": { "type": "string" } } },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean", "enum": [true] },
                    "data": {
                      "type": "object",
                      "properties": {
                        "user_id": { "type": "string" },
                        "account_id": { "type": "string" },
                        "expires_at": { "type": "integer" },
                        "challenge": { "type": "string", "enum": ["mfa"], "nullable": true },
                        "mfa_method": { "type": "string", "enum": ["totp", "email_otp"], "nullable": true }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": { "description": "Wrong email or password", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "Email unverified or FraudScreen block", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },

    "/auth/logout": {
      "post": {
        "summary": "Revoke the current session",
        "tags": ["Auth"],
        "responses": {
          "200": {
            "description": "Session revoked, cookie cleared",
            "headers": { "Set-Cookie": { "schema": { "type": "string", "example": "didhub_sid=; Max-Age=0; …" } } },
            "content": { "application/json": { "schema": { "type": "object", "properties": { "ok": { "type": "boolean" } } } } }
          }
        }
      }
    },

    "/auth/forgot-password": {
      "post": {
        "summary": "Request a password-reset email",
        "description": "Always returns 204 — does not leak email existence.",
        "tags": ["Auth"],
        "security": [],
        "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { "email": { "type": "string", "format": "email" } } } } } },
        "responses": { "204": { "description": "Always" } }
      }
    },

    "/auth/reset-password": {
      "post": {
        "summary": "Set a new password",
        "description": "Consumes a reset token and **revokes all sessions** for the user. Sends a password-changed email.",
        "tags": ["Auth"],
        "security": [],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "type": "object", "required": ["token", "new_password"], "properties": { "token": { "type": "string" }, "new_password": { "type": "string", "minLength": 10 } } } } }
        },
        "responses": {
          "200": { "description": "Password reset, all sessions revoked" },
          "410": { "description": "Token invalid or expired", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "422": { "description": "Password too short", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },

    "/auth/magic-link": {
      "post": {
        "summary": "Request a magic sign-in link",
        "description": "Sends a one-click sign-in link to the email address. **Always returns 204** — does not leak whether the email belongs to an existing account. Token expires in **15 minutes** and is single-use. The act of clicking the link also marks the email verified (clicking proves email ownership).",
        "tags": ["Auth"],
        "security": [],
        "requestBody": {
          "content": { "application/json": { "schema": { "type": "object", "properties": { "email": { "type": "string", "format": "email" } } } } }
        },
        "responses": { "204": { "description": "Always — even for unknown emails" } }
      }
    },

    "/auth/magic-link/consume": {
      "post": {
        "summary": "Consume a magic-link token",
        "description": "Validates the token, creates a session, sets the `didhub_sid` cookie. Honors MFA — if the user has MFA enabled, returns `challenge: \"mfa\"` and the session is in partial state (call `/v1/auth/mfa/challenge` to complete). Also flips `email_verified` on the user if it wasn't already set.\n\nLogs a `login_event` with `method=magic_link`.",
        "tags": ["Auth"],
        "security": [],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "type": "object", "required": ["token"], "properties": { "token": { "type": "string" } } } } }
        },
        "responses": {
          "200": {
            "description": "Signed in (or partial if MFA challenge required)",
            "headers": { "Set-Cookie": { "schema": { "type": "string" } } },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean", "enum": [true] },
                    "data": {
                      "type": "object",
                      "properties": {
                        "user_id": { "type": "string" },
                        "account_id": { "type": "string" },
                        "expires_at": { "type": "integer" },
                        "challenge": { "type": "string", "enum": ["mfa"], "nullable": true },
                        "mfa_method": { "type": "string", "enum": ["totp", "email_otp"], "nullable": true }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": { "description": "Missing token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "410": { "description": "Token invalid, already used, or expired (returns same code for all — no information leak)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },

    "/auth/mfa/setup-start": {
      "post": {
        "summary": "Begin MFA enrollment",
        "description": "Generates a TOTP secret (not persisted yet) and an `otpauth://` URI for the SPA to render as a QR. The customer enters the first code from their authenticator into `POST /auth/mfa/setup-verify` to commit.",
        "tags": ["MFA"],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "type": "object", "required": ["method"], "properties": { "method": { "type": "string", "enum": ["totp"] } } } } }
        },
        "responses": {
          "200": {
            "description": "Secret + provisioning URI",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "data": {
                      "type": "object",
                      "properties": {
                        "method": { "type": "string", "enum": ["totp"] },
                        "secret": { "type": "string", "description": "Base32 TOTP secret" },
                        "otpauth_uri": { "type": "string" },
                        "issuer": { "type": "string", "example": "DIDHub" },
                        "account_label": { "type": "string" }
                      }
                    }
                  }
                }
              }
            }
          },
          "409": { "description": "MFA already enabled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },

    "/auth/mfa/setup-verify": {
      "post": {
        "summary": "Commit MFA enrollment",
        "description": "Verifies the first TOTP code and persists the secret (AES-GCM encrypted at rest).",
        "tags": ["MFA"],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "type": "object", "required": ["method", "secret", "code"], "properties": { "method": { "type": "string", "enum": ["totp"] }, "secret": { "type": "string" }, "code": { "type": "string", "pattern": "^[0-9]{6}$" } } } } }
        },
        "responses": {
          "200": { "description": "MFA enabled" },
          "400": { "description": "Code didn't match", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },

    "/auth/mfa/challenge": {
      "post": {
        "summary": "Complete MFA on a partial session",
        "description": "Promotes the calling session from partial (mfa_completed_at=NULL) to fully authed. The cookie itself doesn't change.",
        "tags": ["MFA"],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "type": "object", "required": ["code"], "properties": { "code": { "type": "string", "pattern": "^[0-9]{6}$" } } } } }
        },
        "responses": {
          "200": { "description": "Session is now fully authed" },
          "401": { "description": "Wrong code or no session", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },

    "/auth/mfa/disable": {
      "post": {
        "summary": "Turn off MFA",
        "description": "Requires the user's current password as confirmation.",
        "tags": ["MFA"],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "type": "object", "required": ["password"], "properties": { "password": { "type": "string" } } } } }
        },
        "responses": {
          "200": { "description": "MFA disabled" },
          "401": { "description": "Incorrect password", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "409": { "description": "MFA was not enabled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },

    "/auth/oauth/{provider}/start": {
      "get": {
        "summary": "Begin OAuth sign-in",
        "description": "Generates an HMAC-signed state nonce in a cookie and **redirects to the provider's consent screen**. The browser follows this redirect.",
        "tags": ["OAuth"],
        "security": [],
        "parameters": [
          { "name": "provider", "in": "path", "required": true, "schema": { "type": "string", "enum": ["google", "microsoft"] } },
          { "name": "return_to", "in": "query", "required": false, "schema": { "type": "string", "default": "/dashboard" } }
        ],
        "responses": {
          "302": { "description": "Redirect to provider authorization URL" },
          "404": { "description": "Unknown provider", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "503": { "description": "Provider not configured (client_id/secret unset)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },

    "/auth/sessions": {
      "get": {
        "summary": "List active sessions",
        "tags": ["Sessions"],
        "responses": {
          "200": {
            "description": "Active sessions",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "data": { "type": "object", "properties": { "sessions": { "type": "array", "items": { "$ref": "#/components/schemas/Session" } } } }
                  }
                }
              }
            }
          }
        }
      }
    },

    "/auth/sessions/{id}/revoke": {
      "post": {
        "summary": "Revoke a specific session",
        "description": "Revoking the calling session also clears the cookie — the next request 401s.",
        "tags": ["Sessions"],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": { "description": "Revoked" },
          "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },

    "/auth/sessions/revoke-others": {
      "post": {
        "summary": "Revoke all sessions except the caller's",
        "tags": ["Sessions"],
        "responses": {
          "200": {
            "description": "Done",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "data": { "type": "object", "properties": { "revoked_count": { "type": "integer" } } }
                  }
                }
              }
            }
          }
        }
      }
    },

    "/auth/login-history": {
      "get": {
        "summary": "Recent sign-in events",
        "description": "Last 50 login_event rows for the current user, 90-day window.",
        "tags": ["Sessions"],
        "responses": {
          "200": {
            "description": "Events",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "data": { "type": "object", "properties": { "events": { "type": "array", "items": { "$ref": "#/components/schemas/LoginEvent" } } } }
                  }
                }
              }
            }
          }
        }
      }
    },

    "/accounts/me": {
      "get": {
        "summary": "Current account + users",
        "tags": ["Accounts"],
        "responses": {
          "200": {
            "description": "Account + members",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "data": {
                      "type": "object",
                      "properties": {
                        "account": { "$ref": "#/components/schemas/Account" },
                        "users": { "type": "array", "items": { "$ref": "#/components/schemas/User" } }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "patch": {
        "summary": "Update account name / country / intended_use",
        "tags": ["Accounts"],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": { "type": "string" },
                  "country": { "type": "string", "description": "ISO-2" },
                  "intended_use": { "type": "string" }
                }
              }
            }
          }
        },
        "responses": { "200": { "description": "Updated" } }
      }
    },

    "/accounts/me/closure": {
      "get": {
        "summary": "Current closure status",
        "tags": ["Accounts"],
        "responses": {
          "200": {
            "description": "Status",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "data": {
                      "type": "object",
                      "properties": {
                        "status": { "type": "string", "enum": ["active", "pending_closure"] },
                        "request": { "oneOf": [{ "$ref": "#/components/schemas/ClosureRequest" }, { "type": "null" }] }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Request a 30-day grace closure",
        "tags": ["Accounts"],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason_category": { "type": "string", "enum": ["price", "feature", "consolidation", "other"] },
                  "reason": { "type": "string", "maxLength": 500 }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Closure scheduled",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": { "type": "string" },
                        "status": { "type": "string", "enum": ["pending_closure"] },
                        "grace_expires_at": { "type": "integer" },
                        "days_remaining": { "type": "integer", "example": 30 }
                      }
                    }
                  }
                }
              }
            }
          },
          "409": { "description": "Already pending or already closed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },

    "/accounts/me/closure/cancel": {
      "post": {
        "summary": "Cancel a pending closure",
        "description": "Restores account.status to 'active'. Refused once grace_expires_at has passed (cron has finalised by then).",
        "tags": ["Accounts"],
        "responses": {
          "200": { "description": "Cancelled, account is active again" },
          "409": { "description": "No pending closure, or grace expired", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },

    "/api-keys": {
      "get": {
        "summary": "List API keys",
        "tags": ["API Keys"],
        "responses": {
          "200": {
            "description": "Keys (without raw secrets)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "data": { "type": "object", "properties": { "keys": { "type": "array", "items": { "$ref": "#/components/schemas/ApiKeySummary" } } } }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Create an API key",
        "description": "Returns the raw secret **once**. Store it immediately — there is no recovery.",
        "tags": ["API Keys"],
        "x-codeSamples": [
          { "lang": "curl", "source": "curl -X POST https://api.didhub.io/v1/api-keys \\\n  -H \"Authorization: Bearer didhub_live_…\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"name\": \"Production server\", \"scopes\": [\"*\"]}'" },
          { "lang": "JavaScript", "source": "const res = await fetch(\"https://api.didhub.io/v1/api-keys\", {\n  method: \"POST\",\n  headers: {\n    Authorization: \"Bearer didhub_live_…\",\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({ name: \"Production server\", scopes: [\"*\"] }),\n});\nconst { data } = await res.json();\nconsole.log(data.key); // shown once — store it now" },
          { "lang": "Python", "source": "import requests\n\nres = requests.post(\n    \"https://api.didhub.io/v1/api-keys\",\n    headers={\"Authorization\": \"Bearer didhub_live_…\"},\n    json={\"name\": \"Production server\", \"scopes\": [\"*\"]},\n)\nprint(res.json()[\"data\"][\"key\"])  # shown once — store it now" }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["name"],
                "properties": {
                  "name": { "type": "string", "minLength": 2, "maxLength": 80 },
                  "scopes": { "type": "array", "items": { "type": "string" }, "description": "v1 accepts any short strings; full-access is `[\"*\"]`" },
                  "expires_at": { "type": "integer", "description": "Unix seconds in the future; omit for never-expires" }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "data": { "$ref": "#/components/schemas/CreatedApiKey" }
                  }
                }
              }
            }
          },
          "409": { "description": "20-active-key cap reached", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "422": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },

    "/api-keys/{id}": {
      "delete": {
        "summary": "Revoke an API key (idempotent)",
        "tags": ["API Keys"],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": { "description": "Revoked or already revoked" },
          "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },

    "/catalog/search": {
      "get": {
        "summary": "Search available DIDs",
        "description": "Live availability check across our carrier inventory. Results deduped by E.164 and capped at `limit`.\n\n**Fallback:** when no live inventory is returned, the response falls back to our availability catalog — those rows carry `from_catalog: true` and a placeholder e164 (the specific number is picked at order time).",
        "tags": ["Catalog"],
        "x-codeSamples": [
          { "lang": "curl", "source": "curl -G https://api.didhub.io/v1/catalog/search \\\n  -H \"Authorization: Bearer didhub_live_…\" \\\n  --data-urlencode \"country=US\" \\\n  --data-urlencode \"type=geographic\" \\\n  --data-urlencode \"area_code=212\" \\\n  --data-urlencode \"limit=25\"" },
          { "lang": "JavaScript", "source": "const params = new URLSearchParams({ country: \"US\", type: \"geographic\", area_code: \"212\", limit: \"25\" });\nconst res = await fetch(`https://api.didhub.io/v1/catalog/search?${params}`, {\n  headers: { Authorization: \"Bearer didhub_live_…\" },\n});\nconst { data } = await res.json();\nconsole.log(data.results);" },
          { "lang": "Python", "source": "import requests\n\nres = requests.get(\n    \"https://api.didhub.io/v1/catalog/search\",\n    headers={\"Authorization\": \"Bearer didhub_live_…\"},\n    params={\"country\": \"US\", \"type\": \"geographic\", \"area_code\": \"212\", \"limit\": 25},\n)\nprint(res.json()[\"data\"][\"results\"])" }
        ],
        "parameters": [
          { "name": "country", "in": "query", "required": true, "schema": { "type": "string", "minLength": 2, "maxLength": 2 }, "example": "US" },
          { "name": "type", "in": "query", "schema": { "type": "string", "enum": ["geographic", "national", "mobile", "tollfree", "inum"], "default": "geographic" } },
          { "name": "area_code", "in": "query", "schema": { "type": "string" }, "example": "212" },
          { "name": "state", "in": "query", "schema": { "type": "string" }, "description": "US 2-letter state filter", "example": "NY" },
          { "name": "zip", "in": "query", "schema": { "type": "string" }, "description": "US 5-digit ZIP filter" },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 25 } }
        ],
        "responses": {
          "200": {
            "description": "Search results + meta",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "data": {
                      "type": "object",
                      "properties": {
                        "results": { "type": "array", "items": { "$ref": "#/components/schemas/CatalogResult" } },
                        "meta": {
                          "type": "object",
                          "properties": {
                            "country": { "type": "string" },
                            "type": { "type": "string" },
                            "area_code": { "type": "string", "nullable": true },
                            "limit": { "type": "integer" },
                            "from_cache": { "type": "boolean" },
                            "providers_queried": { "type": "array", "items": { "type": "string" } },
                            "providers_with_results": { "type": "array", "items": { "type": "string" } },
                            "total_ms": { "type": "integer" }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "422": { "description": "Bad params", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },

    "/catalog/countries": {
      "get": {
        "summary": "Countries with stock",
        "description": "From the D1 catalog (no live supplier calls). Cheap, cacheable.",
        "tags": ["Catalog"],
        "responses": {
          "200": {
            "description": "Countries",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "data": {
                      "type": "object",
                      "properties": {
                        "countries": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "country": { "type": "string", "example": "US" },
                              "product_count": { "type": "integer" },
                              "total_stock": { "type": "integer" },
                              "min_monthly_price": { "type": "number", "nullable": true }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },

    "/catalog/area-codes": {
      "get": {
        "summary": "Known area codes for a country",
        "description": "From the D1 catalog. Useful as autocomplete fuel for the search form.",
        "tags": ["Catalog"],
        "parameters": [
          { "name": "country", "in": "query", "required": true, "schema": { "type": "string", "minLength": 2, "maxLength": 2 } },
          { "name": "type", "in": "query", "schema": { "type": "string" } }
        ],
        "responses": {
          "200": {
            "description": "Area codes",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "data": {
                      "type": "object",
                      "properties": {
                        "area_codes": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "area_code": { "type": "string" },
                              "display_name": { "type": "string", "nullable": true },
                              "number_type": { "type": "string" },
                              "provider_count": { "type": "integer" },
                              "total_stock": { "type": "integer" },
                              "min_monthly_price": { "type": "number", "nullable": true }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },

    "/billing/balance": {
      "get": {
        "summary": "Current balance + recent transactions",
        "tags": ["Billing"],
        "responses": {
          "200": {
            "description": "Balance",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "data": {
                      "type": "object",
                      "properties": {
                        "balance_cents": { "type": "integer" },
                        "balance_display": { "type": "string", "example": "$5.00 USD" },
                        "currency": { "type": "string" },
                        "recent_transactions": { "type": "array", "items": { "$ref": "#/components/schemas/BillingTransaction" } },
                        "auto_recharge": {
                          "type": "object",
                          "properties": {
                            "threshold_cents": { "type": "integer", "nullable": true },
                            "amount_cents": { "type": "integer", "nullable": true },
                            "enabled": { "type": "boolean" }
                          }
                        },
                        "stripe_configured": { "type": "boolean" }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },

    "/billing/transactions": {
      "get": {
        "summary": "Paginated transaction ledger",
        "tags": ["Billing"],
        "parameters": [
          { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 200, "default": 50 } },
          { "name": "before", "in": "query", "schema": { "type": "integer" }, "description": "Cursor: unix timestamp of the last row from the previous page" },
          { "name": "kind", "in": "query", "schema": { "type": "string", "enum": ["topup", "charge", "refund", "trial_credit", "adjustment", "chargeback"] } }
        ],
        "responses": {
          "200": {
            "description": "Transactions",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "data": {
                      "type": "object",
                      "properties": {
                        "transactions": { "type": "array", "items": { "$ref": "#/components/schemas/BillingTransaction" } },
                        "has_more": { "type": "boolean" },
                        "next_before": { "type": "integer", "nullable": true }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },

    "/billing/topup": {
      "post": {
        "summary": "Start a Stripe Payment Intent for a top-up",
        "description": "Returns a `client_secret` the SPA uses with Stripe Elements to confirm the payment. On confirmation, Stripe fires a `payment_intent.succeeded` webhook → we credit the balance.",
        "tags": ["Billing"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "amount_cents": { "type": "integer", "minimum": 500, "maximum": 1000000 },
                  "amount_usd": { "type": "number", "description": "Convenience alternative to amount_cents", "example": 25 }
                },
                "oneOf": [{ "required": ["amount_cents"] }, { "required": ["amount_usd"] }]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Payment Intent created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "data": {
                      "type": "object",
                      "properties": {
                        "client_secret": { "type": "string" },
                        "payment_intent_id": { "type": "string" },
                        "amount_cents": { "type": "integer" }
                      }
                    }
                  }
                }
              }
            }
          },
          "422": { "description": "Below min or above max", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "503": { "description": "Stripe not configured on this worker", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },

    "/billing/config": {
      "get": {
        "summary": "Stripe publishable key + suggested amounts",
        "description": "Public-ish config the SPA needs to mount Stripe Elements.",
        "tags": ["Billing"],
        "responses": {
          "200": {
            "description": "Config",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "data": {
                      "type": "object",
                      "properties": {
                        "stripe_configured": { "type": "boolean" },
                        "stripe_publishable_key": { "type": "string", "nullable": true },
                        "min_topup_cents": { "type": "integer", "example": 500 },
                        "max_topup_cents": { "type": "integer", "example": 1000000 },
                        "currency": { "type": "string", "example": "USD" },
                        "suggested_amounts_cents": { "type": "array", "items": { "type": "integer" }, "example": [1000, 2500, 5000, 10000, 25000, 50000] }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },

    "/cart": {
      "get": {
        "summary": "Get the current cart",
        "description": "KV-backed cart for the calling account. Empty for fresh accounts. Sliding 4-hour TTL refreshed on every write (add/remove). Snapshots on each item let you render totals without re-querying /catalog/search.",
        "tags": ["Cart"],
        "responses": {
          "200": {
            "description": "Cart",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "data": { "type": "object", "properties": { "cart": { "$ref": "#/components/schemas/Cart" } } }
                  }
                }
              }
            }
          }
        }
      },
      "delete": {
        "summary": "Empty the cart",
        "tags": ["Cart"],
        "responses": {
          "200": {
            "description": "Cart emptied",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "data": { "type": "object", "properties": { "cart": { "$ref": "#/components/schemas/Cart" } } }
                  }
                }
              }
            }
          }
        }
      }
    },

    "/cart/items": {
      "post": {
        "summary": "Add a number to the cart",
        "description": "Adds an item by `select_token` (from /catalog/search). The optional `snapshot` is stored verbatim and used to render the cart — pass the catalog row you just displayed so totals/labels stay consistent. Duplicates (same select_token) are refused with 409 `duplicate_item`.",
        "tags": ["Cart"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["select_token"],
                "properties": {
                  "select_token": { "type": "string", "description": "From /v1/catalog/search" },
                  "snapshot": {
                    "$ref": "#/components/schemas/CartItemSnapshot",
                    "description": "Optional but strongly recommended — without it, totals show $0 until checkout re-resolves."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Item added",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "data": { "type": "object", "properties": { "cart": { "$ref": "#/components/schemas/Cart" } } }
                  }
                }
              }
            }
          },
          "409": { "description": "Cart full (100 items) or duplicate item", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "422": { "description": "Invalid select_token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },

    "/cart/items/{id}": {
      "delete": {
        "summary": "Remove a cart item",
        "tags": ["Cart"],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "example": "ci_example_abc123" }],
        "responses": {
          "200": {
            "description": "Removed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "data": { "type": "object", "properties": { "cart": { "$ref": "#/components/schemas/Cart" } } }
                  }
                }
              }
            }
          },
          "404": { "description": "Item not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },

    "/cart/checkout": {
      "post": {
        "summary": "Submit the entire cart",
        "description": "Atomic-ish bulk order. Sequence:\n\n1. Refuse if account.status != active\n2. Refuse with 402 `insufficient_balance` if balance < cart total (whole-checkout gate, not per-item — simpler UX than partial fulfillment)\n3. Dispatch all items in parallel via the same code path as `POST /v1/numbers` (each item independent — one supplier failure does NOT block the others)\n4. Per-item outcomes returned. Successful items get a `number` row + a `billing_transaction` (one per number, NOT aggregated, so refunds stay clean). Failed items get a `number_order` row with status=failed and no charge.\n5. Cart cleanup: successful items removed; failed items stay so the customer can retry. If all failed, cart is wiped (tokens likely stale — easier to re-search).\n\n**Idempotency:** pass `client_checkout_id` to make retries safe. Same key returns the original outcome (TTL 24h).\n\n**Status interpretation:**\n- `all_provisioned` — every item succeeded (200)\n- `partial` — at least one succeeded, at least one failed (200) — inspect `items[]`\n- `all_failed` — every item failed (200) — inspect `items[]` for error codes\n- `balance_too_low` — refused before any dispatch (402) — top up and retry",
        "tags": ["Cart"],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "client_checkout_id": {
                    "type": "string",
                    "description": "Idempotency key. Same value within 24h returns the original outcome."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Outcome (success may be all/partial/all-failed; inspect items)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "data": { "type": "object", "properties": { "outcome": { "$ref": "#/components/schemas/CheckoutOutcome" } } }
                  }
                }
              }
            }
          },
          "402": {
            "description": "Insufficient balance — top up first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean", "enum": [false] },
                    "error": { "type": "object", "properties": { "code": { "type": "string", "enum": ["insufficient_balance"] }, "message": { "type": "string" } } },
                    "data": { "type": "object", "properties": { "outcome": { "$ref": "#/components/schemas/CheckoutOutcome" } } }
                  }
                }
              }
            }
          },
          "409": { "description": "Empty cart OR account is closed/pending_closure", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },

    "/billing/payment-methods": {
      "get": {
        "summary": "List saved payment methods",
        "description": "Reads from our local `payment_method` table (synced via the `setup_intent.succeeded` webhook). `is_default` is true for the card auto-recharge will target.",
        "tags": ["Billing"],
        "responses": {
          "200": {
            "description": "Saved cards",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "data": {
                      "type": "object",
                      "properties": {
                        "payment_methods": { "type": "array", "items": { "$ref": "#/components/schemas/PaymentMethod" } }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },

    "/billing/payment-methods/setup": {
      "post": {
        "summary": "Begin saving a payment method (Setup Intent)",
        "description": "Creates a Stripe Setup Intent and returns a `client_secret` the SPA passes to `<PaymentElement />` in setup mode. The customer enters their card; Stripe attaches it to their customer record without charging. On `setup_intent.succeeded` our webhook handler records the card in `payment_method` and (if it's their first card) sets it as default.",
        "tags": ["Billing"],
        "responses": {
          "200": {
            "description": "Setup Intent created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "data": {
                      "type": "object",
                      "properties": {
                        "client_secret": { "type": "string", "example": "seti_ExampleId_secret_ExampleSecret" },
                        "setup_intent_id": { "type": "string", "example": "seti_ExampleId" }
                      }
                    }
                  }
                }
              }
            }
          },
          "503": { "description": "Stripe not configured", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },

    "/billing/payment-methods/{id}": {
      "delete": {
        "summary": "Remove a saved payment method",
        "description": "Detaches from Stripe + soft-deletes our row. If it was the default and auto-recharge was on, auto-recharge is automatically disabled (no card to target).",
        "tags": ["Billing"],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Our `payment_method.id`, NOT the Stripe `pm_…` id" }],
        "responses": {
          "200": {
            "description": "Removed; returns updated list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "data": {
                      "type": "object",
                      "properties": {
                        "payment_methods": { "type": "array", "items": { "$ref": "#/components/schemas/PaymentMethod" } }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },

    "/billing/payment-methods/{id}/default": {
      "post": {
        "summary": "Set as the default payment method",
        "description": "Default is what auto-recharge targets. Pass our `payment_method.id`, not the Stripe `pm_…` id.",
        "tags": ["Billing"],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": { "description": "Updated; returns updated list" },
          "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },

    "/billing/auto-recharge": {
      "patch": {
        "summary": "Configure auto-recharge",
        "description": "Set thresholds for automatic top-ups. To enable: pass `enabled: true` + both `threshold_cents` (≥$1) and `amount_cents` (≥$5). To disable: pass `enabled: false` (other fields ignored). Requires a default payment method to be set first.\n\nWhen enabled, the worker fires an off-session PaymentIntent against the default card whenever a debit drops the balance below threshold. On Stripe decline: auto-recharge pauses 6 hours, an email goes out, the customer fixes their card. On 3DS step-up: customer gets an email with a link to confirm on-session.",
        "tags": ["Billing"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["enabled"],
                "properties": {
                  "enabled": { "type": "boolean" },
                  "threshold_cents": { "type": "integer", "minimum": 100, "description": "Required when enabled: recharge when balance drops below this" },
                  "amount_cents": { "type": "integer", "minimum": 500, "maximum": 1000000, "description": "Required when enabled: amount to recharge" }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "data": { "$ref": "#/components/schemas/AutoRechargeConfig" }
                  }
                }
              }
            }
          },
          "422": { "description": "Validation error (no default card, amounts out of range)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },

    "/dashboard/summary": {
      "get": {
        "summary": "Home-tab KPIs (KV-cached)",
        "description": "Returns the aggregate metrics shown on the dashboard home tab in a single round-trip — active numbers, call volume, spend, balance, recent CDRs.\n\n**Caching:** result is cached in KV under `dashboard:summary:<account_id>` with a 5-minute TTL. The cache is invalidated immediately on any balance move (top-ups, monthly billing cron, per-call charges) and on number releases, so callers normally see fresh data within milliseconds of a write. The 5-minute TTL is a safety net for missed invalidations, not the primary freshness mechanism.\n\nUnder the hood the cold-path compute issues ~10 parallel D1 queries (~700ms p50); a warm read is ~150ms. Inspect `data.cached_at` if you need to display \"as of HH:MM\" or decide whether to force-bypass.",
        "tags": ["Dashboard"],
        "responses": {
          "200": {
            "description": "Summary",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "data": { "$ref": "#/components/schemas/DashboardSummary" }
                  }
                }
              }
            }
          },
          "401": { "description": "Not signed in", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },

    "/numbers": {
      "get": {
        "summary": "List owned numbers",
        "tags": ["Numbers"],
        "parameters": [
          { "name": "limit", "in": "query", "schema": { "type": "integer", "default": 50, "maximum": 200 } },
          { "name": "status", "in": "query", "schema": { "type": "string", "enum": ["active", "pending_documents", "suspended", "released"] } }
        ],
        "responses": {
          "200": {
            "description": "Numbers",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "data": {
                      "type": "object",
                      "properties": {
                        "numbers": { "type": "array", "items": { "$ref": "#/components/schemas/Number" } },
                        "total": { "type": "integer" }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Order a DID",
        "description": "Submits an order via a `select_token` from `GET /v1/catalog/search`. Charges `monthly_price_cents + setup_price_cents` against the account balance. Idempotent on `client_order_id`.",
        "tags": ["Numbers"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["select_token"],
                "properties": {
                  "select_token": { "type": "string", "description": "From a /v1/catalog/search result" },
                  "client_order_id": { "type": "string", "description": "Caller-supplied idempotency key. Same value within the account returns the existing order." }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Order provisioned",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "data": {
                      "type": "object",
                      "properties": {
                        "order": { "$ref": "#/components/schemas/NumberOrder" },
                        "number": { "$ref": "#/components/schemas/Number" }
                      }
                    }
                  }
                }
              }
            }
          },
          "402": {
            "description": "Insufficient balance — top up first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean", "enum": [false] },
                    "error": { "type": "object", "properties": { "code": { "type": "string", "enum": ["insufficient_balance"] }, "message": { "type": "string" } } },
                    "data": { "type": "object", "properties": { "order": { "$ref": "#/components/schemas/NumberOrder" } } }
                  }
                }
              }
            }
          },
          "409": { "description": "Out of stock, or account closed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "422": { "description": "Bad select_token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "502": { "description": "Supplier error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "503": { "description": "Supplier not configured (provider API key missing)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },

    "/numbers/{e164}": {
      "get": {
        "summary": "Get number detail",
        "tags": ["Numbers"],
        "parameters": [
          { "name": "e164", "in": "path", "required": true, "schema": { "type": "string" }, "description": "URL-encoded E.164", "example": "%2B12125550100" }
        ],
        "responses": {
          "200": { "description": "OK" },
          "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      },
      "patch": {
        "summary": "Update number (currently only trunk_id)",
        "description": "Assign or unassign a SIP trunk. Pass `trunk_id: null` to clear.",
        "tags": ["Numbers"],
        "parameters": [{ "name": "e164", "in": "path", "required": true, "schema": { "type": "string" } }],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "trunk_id": { "type": "string", "nullable": true }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Updated" },
          "404": { "description": "Number not found" },
          "409": { "description": "Trunk not active", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "422": { "description": "Invalid trunk_id", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      },
      "delete": {
        "summary": "Release a number",
        "description": "Best-effort release at the supplier; always marks locally released so the customer isn't stuck.",
        "tags": ["Numbers"],
        "parameters": [{ "name": "e164", "in": "path", "required": true, "schema": { "type": "string" } }],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": { "type": "object", "properties": { "reason": { "type": "string" } } }
            }
          }
        },
        "responses": {
          "200": { "description": "Released" },
          "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },

    "/sip-trunks": {
      "get": {
        "summary": "List SIP trunks",
        "tags": ["SIP Trunks"],
        "parameters": [{ "name": "status", "in": "query", "schema": { "type": "string", "enum": ["active", "disabled", "archived"] } }],
        "responses": {
          "200": {
            "description": "Trunks",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "data": {
                      "type": "object",
                      "properties": {
                        "trunks": {
                          "type": "array",
                          "items": {
                            "allOf": [
                              { "$ref": "#/components/schemas/SipTrunk" },
                              { "type": "object", "properties": { "number_count": { "type": "integer" } } }
                            ]
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Create SIP trunk",
        "tags": ["SIP Trunks"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["name"],
                "properties": {
                  "name": { "type": "string", "minLength": 2, "maxLength": 80 },
                  "transport": { "type": "string", "enum": ["udp", "tcp", "tls", "wss"], "default": "udp" },
                  "codec_preference": { "type": "array", "items": { "type": "string" } },
                  "dtmf_mode": { "type": "string", "enum": ["rfc4733", "inband", "sip_info"], "default": "rfc4733" },
                  "registration_mode": { "type": "string", "enum": ["static_ip", "register"], "default": "static_ip" },
                  "caller_id_strategy": { "type": "string", "enum": ["inherit", "override"], "default": "inherit" },
                  "caller_id_override": { "type": "string", "nullable": true, "description": "Required when caller_id_strategy=override" }
                }
              }
            }
          }
        },
        "responses": {
          "201": { "description": "Created" },
          "422": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },

    "/sip-trunks/{id}": {
      "get": {
        "summary": "Trunk detail with gateways",
        "tags": ["SIP Trunks"],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": {
            "description": "Detail",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "data": {
                      "type": "object",
                      "properties": {
                        "trunk": { "$ref": "#/components/schemas/SipTrunk" },
                        "gateways": { "type": "array", "items": { "$ref": "#/components/schemas/SipGateway" } }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      },
      "patch": {
        "summary": "Update trunk fields",
        "tags": ["SIP Trunks"],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": { "type": "string" },
                  "status": { "type": "string", "enum": ["active", "disabled", "archived"] },
                  "transport": { "type": "string", "enum": ["udp", "tcp", "tls", "wss"] },
                  "codec_preference": { "type": "array", "items": { "type": "string" } },
                  "dtmf_mode": { "type": "string", "enum": ["rfc4733", "inband", "sip_info"] },
                  "registration_mode": { "type": "string", "enum": ["static_ip", "register"] },
                  "caller_id_strategy": { "type": "string", "enum": ["inherit", "override"] },
                  "caller_id_override": { "type": "string", "nullable": true }
                }
              }
            }
          }
        },
        "responses": { "200": { "description": "Updated" } }
      },
      "delete": {
        "summary": "Archive trunk",
        "description": "Soft-delete (sets status='archived'). Refused if numbers are still attached — unassign them first.",
        "tags": ["SIP Trunks"],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": { "description": "Archived" },
          "409": { "description": "Trunk in use (active numbers attached)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },

    "/sip-trunks/{id}/gateways": {
      "post": {
        "summary": "Add a gateway",
        "tags": ["SIP Trunks"],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["name", "host"],
                "properties": {
                  "name": { "type": "string", "minLength": 2 },
                  "host": { "type": "string", "description": "FQDN or IP literal" },
                  "port": { "type": "integer", "minimum": 1, "maximum": 65535, "default": 5060 },
                  "transport": { "type": "string", "enum": ["udp", "tcp", "tls", "wss"], "default": "udp" },
                  "weight": { "type": "integer", "minimum": 0, "maximum": 10000, "default": 100 },
                  "priority": { "type": "integer", "minimum": 0, "maximum": 99, "default": 0 },
                  "auth_username": { "type": "string" },
                  "auth_password": { "type": "string", "description": "Plain-text on the wire; AES-GCM encrypted at rest. Never returned." },
                  "custom_headers": { "type": "object", "additionalProperties": { "type": "string" } },
                  "enabled": { "type": "boolean", "default": true }
                }
              }
            }
          }
        },
        "responses": {
          "201": { "description": "Returns full trunk detail including the new gateway" },
          "404": { "description": "Trunk not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "422": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },

    "/sip-trunks/{id}/gateways/{gwId}": {
      "patch": {
        "summary": "Update a gateway",
        "tags": ["SIP Trunks"],
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } },
          { "name": "gwId", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": { "type": "string" },
                  "host": { "type": "string" },
                  "port": { "type": "integer" },
                  "transport": { "type": "string" },
                  "weight": { "type": "integer" },
                  "priority": { "type": "integer" },
                  "auth_username": { "type": "string" },
                  "auth_password": { "type": "string" },
                  "custom_headers": { "type": "object", "additionalProperties": { "type": "string" } },
                  "enabled": { "type": "boolean" }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Returns full trunk detail" },
          "404": { "description": "Gateway not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      },
      "delete": {
        "summary": "Delete a gateway",
        "tags": ["SIP Trunks"],
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } },
          { "name": "gwId", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": { "description": "Returns full trunk detail" },
          "404": { "description": "Gateway not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },

    "/routing-profiles": {
      "get": {
        "summary": "List routing profiles",
        "description": "Returns all routing profiles on the account, with `target_count` and `number_count` per profile so the UI can show which profiles are in use.",
        "tags": ["Routing Profiles"],
        "parameters": [
          { "name": "status", "in": "query", "required": false, "schema": { "type": "string", "enum": ["active", "disabled", "archived"] } }
        ],
        "responses": {
          "200": {
            "description": "Profiles list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "data": {
                      "type": "object",
                      "properties": {
                        "profiles": { "type": "array", "items": { "$ref": "#/components/schemas/RoutingProfile" } }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Create a routing profile",
        "tags": ["Routing Profiles"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["name"],
                "properties": {
                  "name": { "type": "string", "minLength": 2, "maxLength": 80 },
                  "strategy": { "type": "string", "enum": ["failover", "round_robin", "weighted_lb", "simultaneous_ring"], "default": "failover" },
                  "ring_timeout_s": { "type": "integer", "minimum": 5, "maximum": 120, "default": 25 },
                  "max_retries": { "type": "integer", "minimum": 0, "maximum": 10, "default": 2 }
                }
              }
            }
          }
        },
        "responses": {
          "201": { "description": "Created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RoutingProfileDetail" } } } },
          "422": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },

    "/routing-profiles/{id}": {
      "get": {
        "summary": "Get a routing profile (with its targets)",
        "tags": ["Routing Profiles"],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": { "description": "Profile detail", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RoutingProfileDetail" } } } },
          "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      },
      "patch": {
        "summary": "Update a routing profile",
        "description": "Partial update. Send only the fields you want changed.",
        "tags": ["Routing Profiles"],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": { "type": "string" },
                  "status": { "type": "string", "enum": ["active", "disabled", "archived"] },
                  "strategy": { "type": "string", "enum": ["failover", "round_robin", "weighted_lb", "simultaneous_ring"] },
                  "ring_timeout_s": { "type": "integer", "minimum": 5, "maximum": 120 },
                  "max_retries": { "type": "integer", "minimum": 0, "maximum": 10 }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Updated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RoutingProfileDetail" } } } },
          "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      },
      "delete": {
        "summary": "Archive a routing profile",
        "description": "Soft-deletes (status=archived). Refused with 409 if any non-released numbers are still attached — detach them first by PATCHing each number with `routing_profile_id: null`.",
        "tags": ["Routing Profiles"],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": { "description": "Archived" },
          "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "409": { "description": "Profile in use — detach attached numbers first", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },

    "/routing-profiles/{id}/targets": {
      "post": {
        "summary": "Add a target to a routing profile",
        "tags": ["Routing Profiles"],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["kind"],
                "properties": {
                  "kind": { "type": "string", "enum": ["sip_trunk", "pstn_forward", "webrtc_user", "webhook"] },
                  "position": { "type": "integer", "minimum": 0, "maximum": 99 },
                  "weight": { "type": "integer", "minimum": 0, "maximum": 10000 },
                  "sip_trunk_id": { "type": "string", "description": "Required when kind=sip_trunk" },
                  "pstn_forward_e164": { "type": "string", "description": "Required when kind=pstn_forward; E.164 format" },
                  "webrtc_user_id": { "type": "string", "description": "Required when kind=webrtc_user (beta)" },
                  "webhook_url": { "type": "string", "description": "Required when kind=webhook; HTTPS only (beta)" },
                  "enabled": { "type": "boolean", "default": true },
                  "conditions": { "type": "object", "additionalProperties": true }
                }
              }
            }
          }
        },
        "responses": {
          "201": { "description": "Target added — returns updated profile detail", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RoutingProfileDetail" } } } },
          "422": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },

    "/routing-profiles/{id}/targets/{targetId}": {
      "patch": {
        "summary": "Update a target",
        "description": "Partial update of any target field except `kind` (which is immutable — delete + re-create if you need to change it).",
        "tags": ["Routing Profiles"],
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } },
          { "name": "targetId", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "position": { "type": "integer", "minimum": 0, "maximum": 99 },
                  "weight": { "type": "integer", "minimum": 0, "maximum": 10000 },
                  "sip_trunk_id": { "type": "string", "nullable": true },
                  "pstn_forward_e164": { "type": "string", "nullable": true },
                  "webrtc_user_id": { "type": "string", "nullable": true },
                  "webhook_url": { "type": "string", "nullable": true },
                  "conditions": { "type": "object", "nullable": true, "additionalProperties": true },
                  "enabled": { "type": "boolean" }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Updated — returns updated profile detail", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RoutingProfileDetail" } } } },
          "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "409": { "description": "Attempted to change kind", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      },
      "delete": {
        "summary": "Remove a target from a routing profile",
        "tags": ["Routing Profiles"],
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } },
          { "name": "targetId", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": { "description": "Removed — returns updated profile detail", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RoutingProfileDetail" } } } },
          "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },

    "/cdrs": {
      "get": {
        "summary": "List call detail records",
        "description": "Paginated list of CDRs for the account, newest first. Use `before` (Unix seconds) to page back through history. Returned alongside `total`, `total_charged_cents`, and `total_billable_s` over the same filter — convenient for billing reconciliation pages.",
        "tags": ["CDRs"],
        "parameters": [
          { "name": "limit", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 1, "maximum": 200, "default": 50 } },
          { "name": "before", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Unix seconds — return CDRs with `started_at < before`" },
          { "name": "direction", "in": "query", "required": false, "schema": { "type": "string", "enum": ["inbound", "outbound"] } },
          { "name": "e164", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Filter by DIDHub-owned number on this leg" },
          { "name": "from", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Unix seconds — CDRs started at or after this time" },
          { "name": "to", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Unix seconds — CDRs started at or before this time" }
        ],
        "responses": {
          "200": {
            "description": "Page of CDRs",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "data": {
                      "type": "object",
                      "properties": {
                        "cdrs": { "type": "array", "items": { "$ref": "#/components/schemas/Cdr" } },
                        "has_more": { "type": "boolean" },
                        "next_before": { "type": "integer", "nullable": true, "description": "Pass as `before` on the next call to continue paging" },
                        "total": { "type": "integer" },
                        "total_charged_cents": { "type": "integer" },
                        "total_billable_s": { "type": "integer" }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },

    "/cdrs/{id}": {
      "get": {
        "summary": "Get a single CDR",
        "tags": ["CDRs"],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": {
            "description": "CDR detail",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "data": {
                      "type": "object",
                      "properties": { "cdr": { "$ref": "#/components/schemas/CdrDetail" } }
                    }
                  }
                }
              }
            }
          },
          "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },

    "/compliance/end-users": {
      "get": {
        "summary": "List end users",
        "description": "Lists every end user (person or business) on the account, with a `document_count` for each.\n\n**PII reminder:** these records contain personal data your end-users entrust to you. You are the data controller — see the Compliance tag description.",
        "tags": ["Compliance"],
        "responses": {
          "200": {
            "description": "End user list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "data": {
                      "type": "object",
                      "properties": { "end_users": { "type": "array", "items": { "$ref": "#/components/schemas/EndUser" } } }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Create an end user",
        "tags": ["Compliance"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["kind", "address_line1", "city", "postal_code", "country"],
                "properties": {
                  "kind": { "type": "string", "enum": ["person", "business"] },
                  "first_name": { "type": "string", "description": "Required when kind=person" },
                  "last_name": { "type": "string", "description": "Required when kind=person" },
                  "date_of_birth": { "type": "string", "format": "date" },
                  "business_name": { "type": "string", "description": "Required when kind=business" },
                  "business_registration_number": { "type": "string" },
                  "vat_number": { "type": "string" },
                  "email": { "type": "string", "format": "email" },
                  "phone_e164": { "type": "string" },
                  "address_line1": { "type": "string" },
                  "address_line2": { "type": "string" },
                  "city": { "type": "string" },
                  "state": { "type": "string" },
                  "postal_code": { "type": "string" },
                  "country": { "type": "string", "description": "ISO-2" },
                  "id_doc_type": { "type": "string", "enum": ["passport", "drivers_license", "national_id", "business_registration", "tax_certificate"] },
                  "id_doc_number": { "type": "string" },
                  "id_doc_country": { "type": "string", "description": "ISO-2 country of issue" },
                  "notes": { "type": "string" }
                }
              }
            }
          }
        },
        "responses": {
          "201": { "description": "Created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/EndUser" } } } },
          "422": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },

    "/compliance/end-users/{id}": {
      "get": {
        "summary": "Get an end user",
        "tags": ["Compliance"],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": { "description": "End user", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/EndUser" } } } },
          "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      },
      "patch": {
        "summary": "Update an end user",
        "description": "Partial update. `kind` is immutable — delete and re-create if you need to switch person↔business.",
        "tags": ["Compliance"],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "requestBody": { "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } },
        "responses": {
          "200": { "description": "Updated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/EndUser" } } } },
          "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "409": { "description": "Attempted to change kind", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      },
      "delete": {
        "summary": "Delete an end user",
        "description": "Permanently removes the end user and any documents attached to it that aren't referenced by another bundle. Use for GDPR erasure requests.",
        "tags": ["Compliance"],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": { "description": "Deleted" },
          "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "409": { "description": "End user is referenced by a bundle attached to an active number — detach first", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },

    "/compliance/documents": {
      "get": {
        "summary": "List uploaded documents",
        "tags": ["Compliance"],
        "responses": {
          "200": {
            "description": "Documents list (metadata only — no binary content)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "data": {
                      "type": "object",
                      "properties": { "documents": { "type": "array", "items": { "$ref": "#/components/schemas/ComplianceDocument" } } }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Upload a document",
        "description": "`multipart/form-data` upload. Allowed content types: `application/pdf`, `image/jpeg`, `image/png`. Max size 2 MB. The binary is encrypted at rest.",
        "tags": ["Compliance"],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": ["file", "doc_type"],
                "properties": {
                  "file": { "type": "string", "format": "binary" },
                  "doc_type": { "type": "string", "enum": ["passport", "drivers_license", "national_id", "utility_bill", "bank_statement", "tax_certificate", "business_registration", "vat_certificate", "other"] },
                  "end_user_id": { "type": "string", "description": "Optional — associate with an end user" },
                  "issued_at": { "type": "string", "format": "date" },
                  "expires_at": { "type": "string", "format": "date" },
                  "notes": { "type": "string" }
                }
              }
            }
          }
        },
        "responses": {
          "201": { "description": "Uploaded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ComplianceDocument" } } } },
          "413": { "description": "File too large (>2 MB)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "415": { "description": "Unsupported content type", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },

    "/compliance/documents/{id}": {
      "get": {
        "summary": "Get document metadata",
        "tags": ["Compliance"],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": { "description": "Document metadata (use /download for binary)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ComplianceDocument" } } } },
          "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      },
      "delete": {
        "summary": "Delete a document",
        "description": "Removes the document and its binary content. Refused if currently attached to a non-rejected bundle.",
        "tags": ["Compliance"],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": { "description": "Deleted" },
          "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "409": { "description": "Attached to an active bundle", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },

    "/compliance/documents/{id}/download": {
      "get": {
        "summary": "Download a document's binary content",
        "description": "Returns the original file with the stored `content_type`. Requires session auth on the owning account — the URL is **not** shareable.",
        "tags": ["Compliance"],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": {
            "description": "File content",
            "content": {
              "application/pdf": { "schema": { "type": "string", "format": "binary" } },
              "image/jpeg": { "schema": { "type": "string", "format": "binary" } },
              "image/png": { "schema": { "type": "string", "format": "binary" } }
            }
          },
          "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },

    "/compliance/bundles": {
      "get": {
        "summary": "List compliance bundles",
        "tags": ["Compliance"],
        "responses": {
          "200": {
            "description": "Bundles list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "data": {
                      "type": "object",
                      "properties": { "bundles": { "type": "array", "items": { "$ref": "#/components/schemas/ComplianceBundle" } } }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Create a bundle (draft)",
        "description": "Bundles always start in `draft` status. Add documents via POST /bundles/{id}/documents, then submit by PATCHing status to `submitted`.",
        "tags": ["Compliance"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["name", "end_user_id", "country"],
                "properties": {
                  "name": { "type": "string" },
                  "end_user_id": { "type": "string" },
                  "country": { "type": "string", "description": "ISO-2" },
                  "number_type": { "type": "string", "enum": ["geographic", "national", "mobile", "tollfree", "inum"] }
                }
              }
            }
          }
        },
        "responses": {
          "201": { "description": "Created (draft)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ComplianceBundle" } } } },
          "422": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },

    "/compliance/bundles/{id}": {
      "get": {
        "summary": "Get a bundle",
        "tags": ["Compliance"],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": { "description": "Bundle detail", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ComplianceBundle" } } } },
          "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      },
      "patch": {
        "summary": "Update a bundle",
        "description": "Send `status: \"submitted\"` to submit a draft for review. Once `submitted` or `approved`, only `name` is editable.",
        "tags": ["Compliance"],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": { "type": "string" },
                  "status": { "type": "string", "enum": ["draft", "submitted"], "description": "Only forward transitions are allowed via API; `approved`/`rejected` are set by the review process." }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Updated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ComplianceBundle" } } } },
          "409": { "description": "Invalid status transition", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      },
      "delete": {
        "summary": "Delete a bundle",
        "description": "Allowed only when status is `draft` or `rejected`. To revoke a submitted/approved bundle, contact support.",
        "tags": ["Compliance"],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": { "description": "Deleted" },
          "409": { "description": "Bundle is in use", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },

    "/compliance/bundles/{id}/documents": {
      "post": {
        "summary": "Attach a document to a bundle",
        "tags": ["Compliance"],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["document_id"],
                "properties": { "document_id": { "type": "string" } }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Attached", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ComplianceBundle" } } } },
          "404": { "description": "Bundle or document not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },

    "/compliance/bundles/{id}/documents/{docId}": {
      "delete": {
        "summary": "Detach a document from a bundle",
        "tags": ["Compliance"],
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } },
          { "name": "docId", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": { "description": "Detached", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ComplianceBundle" } } } },
          "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },

    "/numbers/{e164}/compliance": {
      "patch": {
        "summary": "Attach a compliance bundle to a number",
        "description": "Associates an `approved` bundle with the number. Required for some countries before the number becomes routable (Number.status moves from `pending_documents` → `active`).",
        "tags": ["Compliance"],
        "parameters": [{ "name": "e164", "in": "path", "required": true, "schema": { "type": "string" }, "example": "+15550100200" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["bundle_id"],
                "properties": { "bundle_id": { "type": "string", "nullable": true, "description": "Bundle ID to attach, or null to detach" } }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Attached — returns updated Number", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Number" } } } },
          "404": { "description": "Number or bundle not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "409": { "description": "Bundle not in `approved` state, or country/type mismatch", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    }
  }
}
