{"openapi":"3.1.0","info":{"title":"codus-nullus-pbx-billing/portal-api","version":"0.0.0","description":"Billing + onboarding API for the pbx product. Customer-facing surface for `billing.pbx.lt` and admin surface for `admin.pbx.lt`. Provisions tenants on `https://my.pbx.lt/api/v1` after Revolut payment-method verification succeeds.  All errors are RFC 7807 problem-details. Stable error codes live in `src/errors/registry.ts`; the SPA looks up localized messages by code. Money in minor units + ISO 4217 currency. Pagination is cursor-based. Mutating money / upstream-pbx operations require `Idempotency-Key`.  Voice rule: every human-visible string uses \"we / our team / Codus Nullus\"."},"servers":[{"url":"https://billing-api.pbx.lt/v1","description":"Production"},{"url":"https://staging-billing-api.pbx.lt/v1","description":"Staging"},{"url":"http://localhost:8787/v1","description":"Local (wrangler dev)"}],"tags":[{"name":"auth (public)","description":"Signup + sign-in + password reset (no auth)."},{"name":"auth (customer)","description":"Authenticated session + profile."},{"name":"account","description":"Company details, addresses, EU VAT."},{"name":"subscription","description":"Plan tier, lifecycle, upgrade / downgrade / cancel."},{"name":"payment-methods","description":"Saved Revolut methods; raw card data never touches this API."},{"name":"payments","description":"One-shot Revolut hosted-checkout flows: prepaid call-balance topups."},{"name":"invoices","description":"Invoice list + PDF download."},{"name":"tenants","description":"Pbx tenants owned by this account; subset mirror of upstream pbx-core."},{"name":"support","description":"Light helpdesk."},{"name":"admin","description":"Internal admin surface; admin role required."},{"name":"webhooks","description":"Inbound from Revolut. Signature verified before any side effect."},{"name":"me-notifications","description":"In-app notification list + mark-read (stub)."},{"name":"me-banners","description":"Account-level banners (dunning warnings, system messages)."},{"name":"vat","description":"EU VAT verification status + proof upload (stub)."},{"name":"compliance","description":"KYB compliance intake list + document upload (stub)."}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Portal session JWT. Admin endpoints require the `admin` role claim."}},"schemas":{"SignupStatus":{"type":"string","enum":["pending_payment","payment_verified","provisioning","active","failed"]},"SignupResponse":{"type":"object","properties":{"signup_id":{"type":"string","example":"sig_01HX9Q2K8B3R4T5V6W7X8Y9Z0A"},"order_token":{"type":"string","description":"Revolut order token for the 0-amount save order (plan 0075). The SPA mounts the Revolut Pay widget with this token + the public key from VITE_REVOLUT_PUBLIC_KEY; the customer authorises (card or Revolut account) and the page shows EUR 0.00. No charge.","example":"order_token_abc123"},"status":{"$ref":"#/components/schemas/SignupStatus"},"amount_minor":{"type":"integer","description":"Monthly price for the chosen plan in minor units (cents). NOT charged at signup - shown for reference; the first charge happens at trial end.","example":499},"currency":{"type":"string","minLength":3,"maxLength":3,"example":"EUR"},"created_at":{"type":"string","format":"date-time","example":"2026-05-14T09:30:00Z"}},"required":["signup_id","order_token","status","amount_minor","currency","created_at"]},"Problem":{"type":"object","properties":{"type":{"type":"string","format":"uri","example":"urn:codus-nullus:pbx-billing:problem:not-found"},"title":{"type":"string","example":"Resource not found"},"status":{"type":"integer","example":404},"detail":{"type":"string"},"instance":{"type":"string"},"code":{"type":"string","description":"Stable error code; SPA looks up localized message by this key.","example":"not_found"},"invalid_params":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"reason":{"type":"string"}},"required":["name","reason"]}},"retry_after":{"type":"integer"}},"required":["type","title","status","code"]},"PlanTier":{"type":"string","enum":["promo","starter","business","enterprise"],"description":"pbx-billing plan tier. promo = EUR 0.99/mo, starter = EUR 4.99/mo, business = EUR 19/mo, enterprise = EUR 59/mo."},"Locale":{"type":"string","enum":["en","lt"],"example":"en"},"SignupRequestCompany":{"type":"object","properties":{"customer_type":{"type":"string","enum":["company"],"description":"Legal entity. Requires `company_name`; rejects `full_name`.","example":"company"},"company_name":{"type":"string","minLength":1,"example":"Main Office Ltd"},"email":{"type":"string","format":"email","example":"owner@mainoffice.lt"},"plan_tier":{"$ref":"#/components/schemas/PlanTier"},"country":{"type":"string","minLength":2,"maxLength":2,"description":"ISO 3166-1 alpha-2.","example":"LT"},"password":{"type":"string","minLength":12,"description":"argon2id hashed server-side; never persisted in plaintext."},"admin_first_name":{"type":"string","minLength":1,"example":"Jonas"},"admin_last_name":{"type":"string","minLength":1,"example":"Jonaitis"},"language":{"$ref":"#/components/schemas/Locale"}},"required":["customer_type","company_name","email","plan_tier","country"],"additionalProperties":false},"SignupRequestIndividual":{"type":"object","properties":{"customer_type":{"type":"string","enum":["individual"],"description":"Sole / household customer. Requires `full_name`; rejects `company_name`.","example":"individual"},"full_name":{"type":"string","minLength":1,"description":"Used when customer_type='individual'. Single field by design - first/last split is collected at account-completion time only if the customer asks to be addressed formally.","example":"Jane Doe"},"email":{"type":"string","format":"email","example":"owner@mainoffice.lt"},"plan_tier":{"$ref":"#/components/schemas/PlanTier"},"country":{"type":"string","minLength":2,"maxLength":2,"description":"ISO 3166-1 alpha-2.","example":"LT"},"password":{"type":"string","minLength":12,"description":"argon2id hashed server-side; never persisted in plaintext."},"admin_first_name":{"type":"string","minLength":1,"example":"Jonas"},"admin_last_name":{"type":"string","minLength":1,"example":"Jonaitis"},"language":{"$ref":"#/components/schemas/Locale"}},"required":["customer_type","full_name","email","plan_tier","country"],"additionalProperties":false},"SignupRequest":{"oneOf":[{"$ref":"#/components/schemas/SignupRequestCompany"},{"$ref":"#/components/schemas/SignupRequestIndividual"}],"discriminator":{"propertyName":"customer_type","mapping":{"company":"#/components/schemas/SignupRequestCompany","individual":"#/components/schemas/SignupRequestIndividual"}}},"SignupStatusResponse":{"type":"object","properties":{"signup_id":{"type":"string"},"status":{"$ref":"#/components/schemas/SignupStatus"},"account_id":{"type":"string","nullable":true,"description":"Set once provisioning succeeds."},"failure_reason":{"type":"string","nullable":true},"created_at":{"type":"string","format":"date-time","example":"2026-05-14T09:30:00Z"},"updated_at":{"type":"string","format":"date-time","example":"2026-05-14T09:30:00Z"}},"required":["signup_id","status","account_id","failure_reason","created_at","updated_at"]},"UserProfile":{"type":"object","properties":{"id":{"type":"string","example":"usr_01HX9Q2K8B3R4T5V6W7X8Y9Z0A"},"email":{"type":"string","format":"email"},"role":{"type":"string","enum":["owner","admin","support"]},"account_id":{"type":"string"},"language":{"$ref":"#/components/schemas/Locale"}},"required":["id","email","role","account_id","language"]},"SignInResponse":{"type":"object","properties":{"access_token":{"type":"string","description":"Short-lived JWT bearer (1h)."},"expires_at":{"type":"integer","description":"Access-token expiry as unix epoch seconds.","example":1747300000},"refresh_token":{"type":"string","description":"Opaque 30-day refresh token; rotates on use."},"token_type":{"type":"string","enum":["Bearer"]},"user":{"$ref":"#/components/schemas/UserProfile"}},"required":["access_token","expires_at","refresh_token","token_type","user"]},"SignupSessionRequest":{"type":"object","properties":{"grant":{"type":"string","minLength":1}},"required":["grant"]},"SignInRequest":{"type":"object","properties":{"email":{"type":"string","format":"email"},"password":{"type":"string","minLength":1}},"required":["email","password"]},"RefreshRequest":{"type":"object","properties":{"refresh_token":{"type":"string"}},"required":["refresh_token"]},"SignOutRequest":{"type":"object","properties":{"refresh_token":{"type":"string"}},"required":["refresh_token"]},"ForgotPasswordRequest":{"type":"object","properties":{"email":{"type":"string","format":"email"}},"required":["email"]},"ResetPasswordRequest":{"type":"object","properties":{"token":{"type":"string"},"new_password":{"type":"string","minLength":8}},"required":["token","new_password"]},"FirstLoginRequest":{"type":"object","properties":{"token":{"type":"string"}},"required":["token"]},"MagicLinkRequest":{"type":"object","properties":{"email":{"type":"string","format":"email"}},"required":["email"]},"MagicLinkVerifyRequest":{"type":"object","properties":{"token":{"type":"string"}},"required":["token"]},"VerificationRequest":{"type":"object","properties":{"email":{"type":"string","format":"email"},"purpose":{"type":"string","enum":["email_verify","login"],"example":"email_verify"}},"required":["email","purpose"]},"VerificationVerifiedResponse":{"type":"object","properties":{"verified":{"type":"boolean","enum":[true]},"grant":{"type":"string","description":"Plan 0080 (Feature A): short-lived signed proof that this browser verified the signup email. Pass to POST /signup/{signup_id}/session after provisioning completes to auto-mint a session. Only present for purpose=email_verify."}},"required":["verified"]},"VerificationVerifyRequest":{"type":"object","properties":{"purpose":{"type":"string","enum":["email_verify","login"],"example":"email_verify"},"email":{"type":"string","format":"email"},"code":{"type":"string","minLength":4,"maxLength":10},"token":{"type":"string"}},"required":["purpose"]},"MeResponse":{"type":"object","properties":{"user_id":{"type":"string"},"account_id":{"type":"string"},"email":{"type":"string","format":"email"},"first_name":{"type":"string"},"last_name":{"type":"string"},"role":{"type":"string","enum":["owner","admin","support"]},"language":{"$ref":"#/components/schemas/Locale"},"notifications_enabled":{"type":"boolean"},"created_at":{"type":"string","format":"date-time","example":"2026-05-14T09:30:00Z"}},"required":["user_id","account_id","email","first_name","last_name","role","language","notifications_enabled","created_at"]},"UpdateMeRequest":{"type":"object","properties":{"first_name":{"type":"string","minLength":1},"last_name":{"type":"string","minLength":1},"language":{"$ref":"#/components/schemas/Locale"},"notifications_enabled":{"type":"boolean"}}},"ChangePasswordRequest":{"type":"object","properties":{"current_password":{"type":"string","minLength":1,"description":"Required when the user already has a password on file. Optional only for first-login users who came in via magic link and have not set a password yet."},"new_password":{"type":"string","minLength":8,"description":"New password; at least 8 characters."}},"required":["new_password"]},"MeAccountResponse":{"type":"object","properties":{"account_id":{"type":"string"},"email":{"type":"string","format":"email"},"company_name":{"type":"string"},"contact_phone":{"type":"string","nullable":true},"reg_number":{"type":"string","nullable":true},"vat_id":{"type":"string","nullable":true},"vat_status":{"type":"string","enum":["not_provided","pending","valid","invalid"]},"language":{"$ref":"#/components/schemas/Locale"},"pbx_tenant_id":{"type":"string","nullable":true},"status":{"type":"string","enum":["pending_payment","provisioning","active","suspended","cancelled"]},"created_at":{"type":"string","format":"date-time","example":"2026-05-14T09:30:00Z"},"updated_at":{"type":"string","format":"date-time","example":"2026-05-14T09:30:00Z"}},"required":["account_id","email","company_name","contact_phone","reg_number","vat_id","vat_status","language","pbx_tenant_id","status","created_at","updated_at"]},"UpdateMeAccountRequest":{"type":"object","properties":{"company_name":{"type":"string","minLength":1,"maxLength":200,"example":"Main Office Ltd"},"contact_phone":{"type":"string","minLength":1,"maxLength":50,"example":"+37060000000"},"reg_number":{"type":"string","minLength":1,"maxLength":50,"example":"300012345"},"vat_id":{"type":"string","minLength":1,"maxLength":50,"description":"EU VAT id. VIES re-validation lives on `PATCH /v1/account` and is intentionally NOT triggered by this lighter endpoint.","example":"LT100017026817"}}},"Address":{"type":"object","nullable":true,"properties":{"line1":{"type":"string","minLength":1},"line2":{"type":"string"},"city":{"type":"string","minLength":1},"postal_code":{"type":"string","minLength":1},"country":{"type":"string","minLength":2,"maxLength":2,"example":"LT"}},"required":["line1","city","postal_code","country"],"description":"Registered/legal company address. Null until the customer fills it in via `POST /v1/account/addresses` (the signup flow does not collect addresses)."},"VatStatus":{"type":"string","enum":["not_provided","pending","valid","invalid"]},"AccountResponse":{"type":"object","properties":{"account_id":{"type":"string"},"company_name":{"type":"string"},"legal_address":{"$ref":"#/components/schemas/Address"},"invoice_address":{"allOf":[{"$ref":"#/components/schemas/Address"},{"description":"Falls back to legal_address when null."}]},"reg_number":{"type":"string","nullable":true},"vat_id":{"type":"string","nullable":true,"example":"LT100017026817"},"vat_status":{"$ref":"#/components/schemas/VatStatus"},"vat_checked_at":{"type":"string","nullable":true,"format":"date-time","description":"Last VIES check time. Null if never validated.","example":"2026-05-14T09:30:00Z"},"language":{"$ref":"#/components/schemas/Locale"},"contact_email":{"type":"string","format":"email"},"contact_phone":{"type":"string","nullable":true},"pbx_tenant_id":{"type":"string","nullable":true,"description":"my.pbx.lt tenant id, set after provisioning."},"status":{"type":"string","enum":["pending_payment","provisioning","active","suspended","cancelled"]},"created_at":{"type":"string","format":"date-time","example":"2026-05-14T09:30:00Z"},"updated_at":{"type":"string","format":"date-time","example":"2026-05-14T09:30:00Z"}},"required":["account_id","company_name","legal_address","invoice_address","reg_number","vat_id","vat_status","vat_checked_at","language","contact_email","contact_phone","pbx_tenant_id","status","created_at","updated_at"]},"UpdateAccountRequest":{"type":"object","properties":{"company_name":{"type":"string","minLength":1},"legal_address":{"$ref":"#/components/schemas/Address"},"invoice_address":{"$ref":"#/components/schemas/Address"},"reg_number":{"type":"string"},"vat_id":{"type":"string","description":"EU VAT id; will be re-validated against VIES on save."},"contact_email":{"type":"string","format":"email"},"contact_phone":{"type":"string"},"language":{"$ref":"#/components/schemas/Locale"}}},"AddressKind":{"type":"string","enum":["legal","invoice"]},"UpsertAddressRequest":{"type":"object","properties":{"kind":{"$ref":"#/components/schemas/AddressKind"},"line1":{"type":"string","minLength":1,"maxLength":200},"line2":{"type":"string","maxLength":200},"city":{"type":"string","minLength":1,"maxLength":100},"postal_code":{"type":"string","minLength":1,"maxLength":20},"country":{"type":"string","minLength":2,"maxLength":2,"description":"ISO 3166-1 alpha-2.","example":"LT"}},"required":["kind","line1","city","postal_code","country"]},"SubscriptionStatus":{"type":"string","enum":["trialing","active","past_due","unpaid","cancel_at_period_end","cancelled","suspended"]},"Money":{"type":"object","properties":{"amount_minor":{"type":"integer","description":"Integer minor units (cents).","example":499},"currency":{"type":"string","minLength":3,"maxLength":3,"example":"EUR"}},"required":["amount_minor","currency"]},"Subscription":{"type":"object","properties":{"subscription_id":{"type":"string"},"plan_tier":{"allOf":[{"$ref":"#/components/schemas/PlanTier"},{"nullable":true}]},"status":{"$ref":"#/components/schemas/SubscriptionStatus"},"price":{"$ref":"#/components/schemas/Money"},"current_period_start":{"type":"string","format":"date-time","example":"2026-05-14T09:30:00Z"},"current_period_end":{"type":"string","format":"date-time","example":"2026-05-14T09:30:00Z"},"cancel_at_period_end":{"type":"boolean"},"default_payment_method_id":{"type":"string","nullable":true},"next_invoice_at":{"type":"string","nullable":true,"format":"date-time","example":"2026-05-14T09:30:00Z"},"created_at":{"type":"string","format":"date-time","example":"2026-05-14T09:30:00Z"},"plan_tier_pending":{"allOf":[{"$ref":"#/components/schemas/PlanTier"},{"nullable":true}]},"plan_tier_pending_effective_at":{"type":"string","nullable":true,"format":"date-time","example":"2026-05-14T09:30:00Z"},"trial_started_at":{"type":"string","nullable":true,"format":"date-time","example":"2026-05-14T09:30:00Z"},"trial_ends_at":{"type":"string","nullable":true,"format":"date-time","example":"2026-05-14T09:30:00Z"},"trial_converted_at":{"type":"string","nullable":true,"format":"date-time","example":"2026-05-14T09:30:00Z"}},"required":["subscription_id","plan_tier","status","price","current_period_start","current_period_end","cancel_at_period_end","default_payment_method_id","next_invoice_at","created_at"]},"ChangePlanResponse":{"type":"object","properties":{"subscription":{"$ref":"#/components/schemas/Subscription"},"message":{"type":"string","description":"Localizable confirmation string. Currently English-only; the SPA may override via a localized template keyed off the response code.","example":"Your new plan starts on 2026-06-14. We'll charge the new amount on that date."}},"required":["subscription","message"]},"ChangePlanRequest":{"type":"object","properties":{"new_tier":{"$ref":"#/components/schemas/PlanTier"}},"required":["new_tier"]},"ChangePlanNowProration":{"type":"object","properties":{"delta_amount_minor":{"type":"integer"},"period_days":{"type":"integer"}},"required":["delta_amount_minor","period_days"]},"ChangePlanNowResponse":{"type":"object","properties":{"subscription":{"$ref":"#/components/schemas/Subscription"},"proration":{"$ref":"#/components/schemas/ChangePlanNowProration"}},"required":["subscription","proration"]},"ChangePlanNowRequest":{"type":"object","properties":{"new_tier":{"$ref":"#/components/schemas/PlanTier"}},"required":["new_tier"]},"CancelSubscriptionRequest":{"type":"object","properties":{"reason":{"type":"string","enum":["too_expensive","missing_features","switching","temporary_pause","other"]},"note":{"type":"string","maxLength":500}}},"PaymentMethod":{"type":"object","properties":{"payment_method_id":{"type":"string"},"brand":{"type":"string","enum":["visa","mastercard","amex","discover","unknown"]},"last4":{"type":"string","minLength":4,"maxLength":4},"exp_month":{"type":"integer","minimum":1,"maximum":12},"exp_year":{"type":"integer","minimum":2026,"maximum":2099},"is_default":{"type":"boolean"},"created_at":{"type":"string","format":"date-time","example":"2026-05-14T09:30:00Z"}},"required":["payment_method_id","brand","last4","exp_month","exp_year","is_default","created_at"]},"AddPaymentMethodResponse":{"type":"object","properties":{"checkout_url":{"type":"string","format":"uri","description":"Revolut hosted-checkout URL for tokenisation. Raw card data never touches this API."},"return_url":{"type":"string","format":"uri","description":"Where the customer is sent after Revolut completes / cancels the flow."}},"required":["checkout_url","return_url"]},"VerifyPaymentMethodResponse":{"type":"object","properties":{"verification_id":{"type":"string","example":"pmv_8a1d..."},"checkout_url":{"type":"string","format":"uri","description":"Revolut hosted checkout URL. Redirect the customer here to enter card details."},"mode":{"type":"string","enum":["zero_auth","micro_auth"]},"amount_minor":{"type":"integer","description":"Effective auth amount used. May differ from request when zero_auth falls back to micro_auth."},"currency":{"type":"string","enum":["EUR"]},"status":{"type":"string","enum":["pending_verification"]},"created_at":{"type":"string","format":"date-time","example":"2026-05-14T09:30:00Z"}},"required":["verification_id","checkout_url","mode","amount_minor","currency","status","created_at"]},"VerifyPaymentMethodRequest":{"type":"object","properties":{"email":{"type":"string","format":"email","description":"Customer email. Used to mint / link the Revolut Customer that will own the saved card.","example":"owner@example.com"},"mode":{"type":"string","enum":["zero_auth","micro_auth"],"default":"micro_auth","description":"zero_auth: ask Revolut for a EUR 0 auth (rejected on /api/orders today, falls back to micro_auth). micro_auth: tiny manual-capture auth that we cancel as soon as the card is saved."},"amount_minor":{"type":"integer","minimum":1,"maximum":100,"default":1,"description":"Auth amount in EUR cents for micro_auth mode (1-100, default 1). Ignored for zero_auth.","example":1}},"required":["email"]},"TopupResponse":{"type":"object","properties":{"topup_id":{"type":"string","description":"Local topup id."},"checkout_url":{"type":"string","format":"uri"},"amount_minor":{"type":"integer"},"currency":{"type":"string","enum":["EUR"]},"status":{"type":"string","enum":["pending"]}},"required":["topup_id","checkout_url","amount_minor","currency","status"]},"TopupRequest":{"type":"object","properties":{"amount_minor":{"type":"integer","minimum":100,"description":"Amount in minor units (cents). Minimum 100 (1.00 EUR).","example":100},"currency":{"type":"string","enum":["EUR"],"example":"EUR"}},"required":["amount_minor","currency"]},"InvoiceStatus":{"type":"string","enum":["draft","open","paid","void","uncollectible","refunded","partially_refunded"]},"InvoiceLineItem":{"type":"object","properties":{"description":{"type":"string"},"quantity":{"type":"integer","minimum":1},"unit_price":{"$ref":"#/components/schemas/Money"},"line_total":{"$ref":"#/components/schemas/Money"},"period_start":{"type":"string","nullable":true,"format":"date-time","example":"2026-05-14T09:30:00Z"},"period_end":{"type":"string","nullable":true,"format":"date-time","example":"2026-05-14T09:30:00Z"}},"required":["description","quantity","unit_price","line_total","period_start","period_end"]},"Invoice":{"type":"object","properties":{"invoice_id":{"type":"string"},"number":{"type":"string","example":"PBX-2026-000037"},"status":{"$ref":"#/components/schemas/InvoiceStatus"},"issued_at":{"type":"string","format":"date-time","example":"2026-05-14T09:30:00Z"},"due_at":{"type":"string","format":"date-time","example":"2026-05-14T09:30:00Z"},"paid_at":{"type":"string","nullable":true,"format":"date-time","example":"2026-05-14T09:30:00Z"},"subtotal":{"$ref":"#/components/schemas/Money"},"vat_amount":{"$ref":"#/components/schemas/Money"},"vat_rate_basis_points":{"type":"integer","description":"21% = 2100 basis points.","example":2100},"total":{"$ref":"#/components/schemas/Money"},"line_items":{"type":"array","items":{"$ref":"#/components/schemas/InvoiceLineItem"}},"pdf_available":{"type":"boolean"}},"required":["invoice_id","number","status","issued_at","due_at","paid_at","subtotal","vat_amount","vat_rate_basis_points","total","line_items","pdf_available"]},"PageInfo":{"type":"object","properties":{"next_cursor":{"type":"string","nullable":true,"description":"Cursor for the next page; null when exhausted."}},"required":["next_cursor"]},"InvoiceListResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Invoice"}},"page_info":{"$ref":"#/components/schemas/PageInfo"}},"required":["data","page_info"]},"CreditNote":{"type":"object","properties":{"id":{"type":"string","example":"crn_8a1f..."},"number":{"type":"string","nullable":true,"description":"Sequential per-year credit-note identifier (plan 0044). Null only for legacy rows pre-migration-0024.","example":"CN-2026-00001"},"invoice_id":{"type":"string","example":"inv_3b7c..."},"invoice_number":{"type":"string","example":"PBX-2026-000037"},"amount":{"$ref":"#/components/schemas/Money"},"reason_code":{"type":"string","example":"goodwill"},"notes":{"type":"string","nullable":true},"issued_at":{"type":"integer","description":"Unix ms.","example":1747590000123},"kind":{"type":"string","enum":["refund","credit"],"description":"refund = paired with an actual Revolut card refund; credit = internal credit-only adjustment.","example":"credit"}},"required":["id","number","invoice_id","invoice_number","amount","reason_code","notes","issued_at","kind"]},"CreditNoteListResponse":{"type":"object","properties":{"credit_notes":{"type":"array","items":{"$ref":"#/components/schemas/CreditNote"}}},"required":["credit_notes"]},"TenantInfoResponse":{"type":"object","properties":{"tenant_id":{"type":"string","nullable":true,"description":"Upstream pbx tenant id, or null when the account has not been provisioned yet.","example":"tnt_01HZ..."},"my_pbx_url":{"type":"string","format":"uri","description":"Deep link to the tenant on https://my.pbx.lt. Stubbed when tenant_id is null.","example":"https://my.pbx.lt/tnt_01HZ..."},"sip_domain":{"type":"string","description":"SIP domain the tenant registers on. Optional; absent until the upstream pbx core surfaces it.","example":"my.pbx.lt"}},"required":["tenant_id","my_pbx_url"]},"Tenant":{"type":"object","properties":{"tenant_id":{"type":"string","example":"550e8400-e29b-41d4-a716-446655440000"},"code":{"type":"string","description":"Mirrored from pbx-core; immutable.","example":"0001"},"name":{"type":"string"},"slug":{"type":"string"},"status":{"type":"string","enum":["active","suspended","deleted"]},"country":{"type":"string","minLength":2,"maxLength":2},"timezone":{"type":"string","example":"Europe/Vilnius"},"contact_email":{"type":"string","format":"email"},"account_id":{"type":"string"},"my_pbx_url":{"type":"string","format":"uri","description":"Deep-link target for the \"Open phone system\" button.","example":"https://my.pbx.lt/"},"created_at":{"type":"string","format":"date-time","example":"2026-05-14T09:30:00Z"}},"required":["tenant_id","code","name","slug","status","country","timezone","contact_email","account_id","my_pbx_url","created_at"]},"TenantListResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Tenant"}}},"required":["data"]},"SupportTicketCategory":{"type":"string","enum":["billing","technical","account","other"]},"SupportTicketStatus":{"type":"string","enum":["open","awaiting_customer","awaiting_us","resolved","closed","duplicate"]},"SupportTicket":{"type":"object","properties":{"ticket_id":{"type":"string"},"reference":{"type":"string"},"subject":{"type":"string"},"category":{"$ref":"#/components/schemas/SupportTicketCategory"},"status":{"$ref":"#/components/schemas/SupportTicketStatus"},"last_activity_at":{"type":"string","format":"date-time","example":"2026-05-14T09:30:00Z"},"created_at":{"type":"string","format":"date-time","example":"2026-05-14T09:30:00Z"}},"required":["ticket_id","subject","status","last_activity_at","created_at"]},"SupportTicketListResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/SupportTicket"}},"page_info":{"$ref":"#/components/schemas/PageInfo"}},"required":["data","page_info"]},"CreateSupportTicketRequest":{"type":"object","properties":{"subject":{"type":"string","minLength":1,"maxLength":200},"message":{"type":"string","minLength":1,"maxLength":10000},"category":{"type":"string","enum":["billing","technical","account","other"],"default":"other"}},"required":["subject","message"]},"SupportMessage":{"type":"object","properties":{"id":{"type":"string"},"author":{"type":"string","enum":["customer","admin"]},"body":{"type":"string"},"created_at":{"type":"string","format":"date-time","example":"2026-05-14T09:30:00Z"}},"required":["id","author","body","created_at"]},"SupportTicketDetail":{"allOf":[{"$ref":"#/components/schemas/SupportTicket"},{"type":"object","properties":{"messages":{"type":"array","items":{"$ref":"#/components/schemas/SupportMessage"}}},"required":["messages"]}]},"AdminTenantRow":{"type":"object","properties":{"tenant_id":{"type":"string"},"account_id":{"type":"string"},"company_name":{"type":"string"},"contact_email":{"type":"string","format":"email"},"pbx_status":{"type":"string","enum":["active","suspended","deleted","unknown"]},"subscription_status":{"$ref":"#/components/schemas/SubscriptionStatus"},"plan_tier":{"type":"string"},"mrr":{"$ref":"#/components/schemas/Money"},"created_at":{"type":"string","format":"date-time","example":"2026-05-14T09:30:00Z"}},"required":["tenant_id","account_id","company_name","contact_email","pbx_status","subscription_status","plan_tier","mrr","created_at"]},"AdminTenantListResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AdminTenantRow"}},"page_info":{"$ref":"#/components/schemas/PageInfo"}},"required":["data","page_info"]},"AdminProvisionTenantRequest":{"type":"object","properties":{"account_id":{"type":"string"},"override_reason":{"type":"string","minLength":1,"description":"Audit-trail reason for manual provisioning override (e.g. \"automated provisioning failed, retried by hand\")."}},"required":["account_id","override_reason"]},"AdminTenantStateTransitionRequest":{"type":"object","properties":{"action":{"type":"string","enum":["suspend","restore"]},"reason":{"type":"string","minLength":1}},"required":["action","reason"]},"AdminCreditNoteRequest":{"type":"object","properties":{"invoice_id":{"type":"string"},"amount":{"allOf":[{"$ref":"#/components/schemas/Money"},{"description":"Credit-note amount; <= invoice total."}]},"reason":{"type":"string","minLength":1},"revolut_refund":{"type":"boolean","description":"When true, refund the captured Revolut order tied to this invoice and send the refund-issued email instead of the credit-note email. Default false (internal credit only)."}},"required":["invoice_id","amount","reason"]},"AdminSubscriptionTransitionRequest":{"type":"object","properties":{"to_status":{"type":"string","enum":["active","cancelled","suspended"]}},"required":["to_status"]},"AdminChangePlanRequest":{"type":"object","properties":{"new_tier":{"$ref":"#/components/schemas/PlanTier"},"effective":{"type":"string","enum":["next_cycle","immediate"]},"reason":{"type":"string","minLength":1,"description":"Audit-trail reason for the operator override."}},"required":["new_tier","effective","reason"]},"AdminBillingCycleRow":{"type":"object","properties":{"id":{"type":"string"},"subscription_id":{"type":"string"},"account_id":{"type":"string"},"period_start":{"type":"integer"},"period_end":{"type":"integer"},"amount_minor":{"type":"integer"},"currency":{"type":"string","minLength":3,"maxLength":3},"state":{"type":"string","enum":["planned","charging","charged","failed","skipped","abandoned"]},"attempt_count":{"type":"integer"},"next_attempt_at":{"type":"integer","nullable":true},"revolut_order_id":{"type":"string","nullable":true},"revolut_payment_method_id":{"type":"string"},"invoice_id":{"type":"string","nullable":true},"last_failure_code":{"type":"string","nullable":true},"last_failure_message":{"type":"string","nullable":true},"created_at":{"type":"integer"},"updated_at":{"type":"integer"}},"required":["id","subscription_id","account_id","period_start","period_end","amount_minor","currency","state","attempt_count","next_attempt_at","revolut_order_id","revolut_payment_method_id","invoice_id","last_failure_code","last_failure_message","created_at","updated_at"]},"AdminBillingCycleListResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AdminBillingCycleRow"}}},"required":["data"]},"SchedulerHealthCounts":{"type":"object","properties":{"planned":{"type":"integer"},"charged":{"type":"integer"},"failed":{"type":"integer"},"abandoned":{"type":"integer"}},"required":["planned","charged","failed","abandoned"]},"SchedulerDunningQueueDepth":{"type":"object","properties":{"current":{"type":"integer"},"grace":{"type":"integer"},"retry_1":{"type":"integer"},"retry_2":{"type":"integer"},"final_notice":{"type":"integer"},"suspended":{"type":"integer"},"cancelled":{"type":"integer"}},"required":["current","grace","retry_1","retry_2","final_notice","suspended","cancelled"]},"SchedulerHealthResponse":{"type":"object","properties":{"window_seconds":{"type":"integer"},"generated_at":{"type":"integer"},"counts":{"$ref":"#/components/schemas/SchedulerHealthCounts"},"oldest_stuck_in_charging_age_seconds":{"type":"integer","nullable":true},"dunning_queue_depth_by_state":{"$ref":"#/components/schemas/SchedulerDunningQueueDepth"}},"required":["window_seconds","generated_at","counts","oldest_stuck_in_charging_age_seconds","dunning_queue_depth_by_state"]},"SchedulerAlert":{"type":"object","properties":{"id":{"type":"string"},"severity":{"type":"string","enum":["warning","critical"]},"message":{"type":"string"},"sample_subscription_ids":{"type":"array","items":{"type":"string"}},"metric":{"type":"number"}},"required":["id","severity","message"]},"SchedulerAlertsResponse":{"type":"object","properties":{"generated_at":{"type":"integer"},"alerts":{"type":"array","items":{"$ref":"#/components/schemas/SchedulerAlert"}}},"required":["generated_at","alerts"]},"AdminMetricsTotals":{"type":"object","properties":{"signups":{"type":"integer"},"active_subscriptions":{"type":"integer"},"mrr_minor":{"type":"integer"},"currency":{"type":"string","minLength":3,"maxLength":3},"dunning_count":{"type":"integer"},"churn_rate":{"type":"number"}},"required":["signups","active_subscriptions","mrr_minor","currency","dunning_count","churn_rate"]},"AdminMetricsPerTier":{"type":"object","properties":{"signups":{"type":"integer"},"active":{"type":"integer"},"mrr_minor":{"type":"integer"}},"required":["signups","active","mrr_minor"]},"AdminMetricsByPlanTier":{"type":"object","properties":{"starter":{"$ref":"#/components/schemas/AdminMetricsPerTier"},"business":{"$ref":"#/components/schemas/AdminMetricsPerTier"},"enterprise":{"$ref":"#/components/schemas/AdminMetricsPerTier"}},"required":["starter","business","enterprise"]},"AdminMetricsWindow":{"type":"object","properties":{"label":{"type":"string","enum":["mtd","last_30_days","last_90_days"]},"from":{"type":"string","format":"date-time","example":"2026-05-14T09:30:00Z"},"to":{"type":"string","format":"date-time","example":"2026-05-14T09:30:00Z"},"totals":{"$ref":"#/components/schemas/AdminMetricsTotals"},"by_plan_tier":{"$ref":"#/components/schemas/AdminMetricsByPlanTier"}},"required":["label","from","to","totals","by_plan_tier"]},"AdminMetricsResponse":{"type":"object","properties":{"as_of":{"type":"string","format":"date-time","example":"2026-05-14T09:30:00Z"},"windows":{"type":"array","items":{"$ref":"#/components/schemas/AdminMetricsWindow"}}},"required":["as_of","windows"]},"RevolutWebhookAck":{"type":"object","properties":{"ok":{"type":"boolean"}},"required":["ok"]},"NotificationSeverity":{"type":"string","enum":["info","warn","critical"]},"Notification":{"type":"object","properties":{"id":{"type":"string","example":"ntf_abc123"},"kind":{"type":"string","example":"invoice_issued"},"severity":{"$ref":"#/components/schemas/NotificationSeverity"},"message_key":{"type":"string","example":"notifications.invoice_issued"},"message_args":{"type":"object","nullable":true,"additionalProperties":{"nullable":true},"description":"Template arguments for the message_key.","example":{"invoice_number":"INV-2026-PBXBILL-000001","amount":"9.99 EUR"}},"cta_label_key":{"type":"string","nullable":true,"example":"notifications.cta.view_invoice"},"cta_url":{"type":"string","nullable":true,"example":"/invoices/inv_abc"},"read":{"type":"boolean","example":false},"read_at":{"type":"string","nullable":true,"format":"date-time","example":"2026-05-14T09:30:00Z"},"created_at":{"type":"string","format":"date-time","example":"2026-05-14T09:30:00Z"}},"required":["id","kind","severity","message_key","message_args","cta_label_key","cta_url","read","read_at","created_at"]},"PaginatedNotificationsResponse":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Notification"}},"cursor_next":{"type":"string","nullable":true,"description":"Cursor for the next page; absent or null when exhausted."},"unread_count":{"type":"integer","minimum":0,"description":"Total unread notifications for the signed-in account."}},"required":["items","unread_count"]},"BannerSeverity":{"type":"string","enum":["info","warn","critical"]},"AccountBanner":{"type":"object","properties":{"id":{"type":"string","example":"bnr_01HZ..."},"severity":{"$ref":"#/components/schemas/BannerSeverity"},"kind":{"type":"string","description":"Banner family. `dunning_grace`, `dunning_retry_1`, `dunning_retry_2`, `dunning_suspended`, `dunning_cancelled` for the dunning machine; admin-set values are free-form (e.g. `system`).","example":"dunning_retry_1"},"message_key":{"type":"string","description":"Locale-dictionary key resolved client-side.","example":"banners.dunning.retry_1"},"message_args":{"type":"object","nullable":true,"additionalProperties":{"nullable":true},"description":"Template variables for the message_key (date, amount, count, ...). Null when the template needs no args."},"cta_label_key":{"type":"string","nullable":true,"description":"Optional locale key for a call-to-action button."},"cta_url":{"type":"string","nullable":true,"description":"Destination URL for the CTA. Relative paths are interpreted by the SPA."},"created_at":{"type":"integer","description":"Unix epoch seconds."},"expires_at":{"type":"integer","nullable":true,"description":"Unix epoch seconds when the banner auto-clears; null = never."}},"required":["id","severity","kind","message_key","message_args","cta_label_key","cta_url","created_at","expires_at"]},"AccountBannerListResponse":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/AccountBanner"}}},"required":["items"]},"AdminAccountBanner":{"allOf":[{"$ref":"#/components/schemas/AccountBanner"},{"type":"object","properties":{"active":{"type":"boolean","description":"Cleared banners are visible to admins for audit."},"source":{"type":"string","description":"`scheduler` | `admin` | `manual`.","example":"scheduler"},"source_ref":{"type":"string","nullable":true,"description":"Originating reference: dunning state name for scheduler-set banners, operator id for admin-set."},"updated_at":{"type":"integer"}},"required":["active","source","source_ref","updated_at"]}]},"AdminAccountBannerListResponse":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/AdminAccountBanner"}}},"required":["items"]},"AdminCreateBannerRequest":{"type":"object","properties":{"severity":{"$ref":"#/components/schemas/BannerSeverity"},"kind":{"type":"string","minLength":1,"maxLength":80},"message_key":{"type":"string","minLength":1,"maxLength":160},"message_args":{"type":"object","nullable":true,"additionalProperties":{"nullable":true}},"cta_label_key":{"type":"string","nullable":true,"minLength":1,"maxLength":160},"cta_url":{"type":"string","nullable":true,"minLength":1,"maxLength":2048},"expires_at":{"type":"integer","nullable":true,"minimum":0,"exclusiveMinimum":true}},"required":["severity","kind","message_key"]},"AdminVatProof":{"type":"object","properties":{"id":{"type":"string"},"account_id":{"type":"string"},"vat_id":{"type":"string","nullable":true},"r2_key":{"type":"string"},"content_type":{"type":"string"},"size_bytes":{"type":"integer"},"customer_note":{"type":"string","nullable":true},"status":{"type":"string","enum":["pending_review","approved","rejected"]},"reviewed_at":{"type":"integer","nullable":true},"reviewed_by":{"type":"string","nullable":true},"reviewer_note":{"type":"string","nullable":true},"created_at":{"type":"integer"},"updated_at":{"type":"integer"}},"required":["id","account_id","vat_id","r2_key","content_type","size_bytes","customer_note","status","reviewed_at","reviewed_by","reviewer_note","created_at","updated_at"]},"AdminVatProofListResponse":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/AdminVatProof"}},"cursor_next":{"type":"string","nullable":true}},"required":["items","cursor_next"]},"AdminVatProofDecisionRequest":{"type":"object","properties":{"reviewer_note":{"type":"string","maxLength":1000}}},"ComplianceIntakeStatus":{"type":"string","enum":["open","submitted","approved","rejected"]},"ComplianceIntake":{"type":"object","properties":{"id":{"type":"string","example":"kyb_01HZ..."},"account_id":{"type":"string","example":"acc_01HZ..."},"status":{"$ref":"#/components/schemas/ComplianceIntakeStatus"},"required_docs":{"type":"array","items":{"type":"string"},"description":"Symbolic document keys the SPA renders into a per-locale label set.","example":["articles_of_incorporation","beneficial_ownership_declaration","proof_of_address"]},"uploaded_docs":{"type":"array","items":{"type":"string"},"description":"Subset of `required_docs` keys for which at least one file has landed.","example":["articles_of_incorporation"]},"submitted_at":{"type":"integer","nullable":true},"reviewed_at":{"type":"integer","nullable":true},"reviewed_by":{"type":"string","nullable":true},"reviewer_note":{"type":"string","nullable":true},"created_at":{"type":"integer"},"updated_at":{"type":"integer"}},"required":["id","account_id","status","required_docs","uploaded_docs","submitted_at","reviewed_at","reviewed_by","reviewer_note","created_at","updated_at"]},"AdminKybIntakeListResponse":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/ComplianceIntake"}},"cursor_next":{"type":"string","nullable":true}},"required":["items","cursor_next"]},"AdminKybIntakeDecisionRequest":{"type":"object","properties":{"reviewer_note":{"type":"string","maxLength":2000}}},"AuditActor":{"type":"string","nullable":true,"enum":["customer","admin","system","webhook"]},"AuditLogRow":{"type":"object","properties":{"id":{"type":"string","description":"Row id (uuid)."},"ts":{"type":"integer","description":"Event timestamp in unix milliseconds."},"action":{"type":"string","example":"auth.sign_in"},"account_id":{"type":"string","nullable":true},"user_id":{"type":"string","nullable":true},"actor":{"$ref":"#/components/schemas/AuditActor"},"ip":{"type":"string","nullable":true},"user_agent":{"type":"string","nullable":true},"outcome":{"type":"string","enum":["success","failure"]},"reason":{"type":"string","nullable":true,"description":"Short code from the error / outcome vocabulary; null on success rows that have no reason."},"meta":{"type":"string","nullable":true,"description":"JSON blob of non-sensitive identifiers; opaque to the API. Never contains secrets."}},"required":["id","ts","action","account_id","user_id","actor","ip","user_agent","outcome","reason","meta"]},"AuditLogListResponse":{"type":"object","properties":{"rows":{"type":"array","items":{"$ref":"#/components/schemas/AuditLogRow"}},"next_cursor":{"type":"string","description":"Opaque cursor for the next page; omitted when the page is the last."}},"required":["rows"]},"AdminCustomerCustomerType":{"type":"string","enum":["company","individual"]},"AdminCustomerSubscriptionStatus":{"type":"string","enum":["trialing","active","past_due","unpaid","cancel_at_period_end","cancelled","suspended"]},"AdminCustomerVatStatus":{"type":"string","enum":["none","pending_review","valid","invalid"]},"AdminCustomerDunningState":{"type":"string","enum":["current","grace","retry_1","retry_2","final_notice","suspended","cancelled"]},"AdminCustomerListRow":{"type":"object","properties":{"account_id":{"type":"string"},"display_name":{"type":"string"},"email":{"type":"string"},"customer_type":{"$ref":"#/components/schemas/AdminCustomerCustomerType"},"plan_tier":{"type":"string"},"subscription_status":{"$ref":"#/components/schemas/AdminCustomerSubscriptionStatus"},"vat_status":{"$ref":"#/components/schemas/AdminCustomerVatStatus"},"dunning_state":{"$ref":"#/components/schemas/AdminCustomerDunningState"},"created_at":{"type":"integer","description":"Unix seconds."}},"required":["account_id","display_name","email","customer_type","plan_tier","subscription_status","vat_status","dunning_state","created_at"]},"AdminCustomerListResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AdminCustomerListRow"}},"next_cursor":{"type":"string","nullable":true}},"required":["data","next_cursor"]},"AdminCustomerAccount":{"type":"object","properties":{"account_id":{"type":"string"},"display_name":{"type":"string"},"email":{"type":"string"},"customer_type":{"$ref":"#/components/schemas/AdminCustomerCustomerType"},"company_name":{"type":"string","nullable":true},"full_name":{"type":"string","nullable":true},"vat_id":{"type":"string","nullable":true},"vat_status":{"$ref":"#/components/schemas/AdminCustomerVatStatus"},"created_at":{"type":"integer"}},"required":["account_id","display_name","email","customer_type","company_name","full_name","vat_id","vat_status","created_at"]},"AdminCustomerSubscriptionView":{"type":"object","nullable":true,"properties":{"subscription_id":{"type":"string"},"plan_tier":{"type":"string"},"status":{"$ref":"#/components/schemas/AdminCustomerSubscriptionStatus"},"current_period_start":{"type":"string","format":"date-time","example":"2026-05-14T09:30:00Z"},"current_period_end":{"type":"string","format":"date-time","example":"2026-05-14T09:30:00Z"},"cancel_at_period_end":{"type":"boolean"},"dunning_state":{"$ref":"#/components/schemas/AdminCustomerDunningState"},"failed_attempts":{"type":"integer"},"default_payment_method_id":{"type":"string","nullable":true}},"required":["subscription_id","plan_tier","status","current_period_start","current_period_end","cancel_at_period_end","dunning_state","failed_attempts","default_payment_method_id"]},"AdminCustomerPaymentMethod":{"type":"object","properties":{"id":{"type":"string"},"brand":{"type":"string"},"last4":{"type":"string"},"exp_month":{"type":"integer"},"exp_year":{"type":"integer"},"is_default":{"type":"boolean"}},"required":["id","brand","last4","exp_month","exp_year","is_default"]},"AdminCustomerInvoice":{"type":"object","properties":{"invoice_id":{"type":"string"},"number":{"type":"string"},"status":{"type":"string","enum":["draft","open","paid","void","uncollectible","refunded","partially_refunded"]},"issued_at":{"type":"string","format":"date-time","example":"2026-05-14T09:30:00Z"},"total":{"$ref":"#/components/schemas/Money"},"open_balance_minor":{"type":"integer"}},"required":["invoice_id","number","status","issued_at","total","open_balance_minor"]},"AdminCustomerDetailResponse":{"type":"object","properties":{"account":{"$ref":"#/components/schemas/AdminCustomerAccount"},"subscription":{"$ref":"#/components/schemas/AdminCustomerSubscriptionView"},"payment_methods":{"type":"array","items":{"$ref":"#/components/schemas/AdminCustomerPaymentMethod"}},"invoices":{"type":"array","items":{"$ref":"#/components/schemas/AdminCustomerInvoice"}}},"required":["account","subscription","payment_methods","invoices"]},"AdminCustomerPaymentMethodsResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AdminCustomerPaymentMethod"}}},"required":["data"]},"AdminAddPaymentMethodResponse":{"type":"object","properties":{"redirect_url":{"type":"string","format":"uri","description":"Revolut hosted-checkout URL the operator should hand to the customer."},"payment_method_id":{"type":"string","description":"Pre-confirmation payment_method row id (verification still pending)."}},"required":["redirect_url","payment_method_id"]},"AdminUpdateCustomerAccountRequest":{"type":"object","properties":{"vat_id":{"type":"string","nullable":true,"description":"VAT id to set on the account; null clears it. Triggers VIES re-validation when changed."},"company_name":{"type":"string"},"contact_phone":{"type":"string"},"reg_number":{"type":"string"}}},"AdminSupportTicketRow":{"type":"object","properties":{"ticket_id":{"type":"string"},"reference":{"type":"string"},"account_id":{"type":"string"},"subject":{"type":"string"},"category":{"$ref":"#/components/schemas/SupportTicketCategory"},"status":{"$ref":"#/components/schemas/SupportTicketStatus"},"last_activity_at":{"type":"string","format":"date-time","example":"2026-05-14T09:30:00Z"},"created_at":{"type":"string","format":"date-time","example":"2026-05-14T09:30:00Z"},"duplicate_of_ticket_id":{"type":"string","nullable":true},"assignee_admin_id":{"type":"string","nullable":true}},"required":["ticket_id","reference","account_id","subject","category","status","last_activity_at","created_at"]},"AdminSupportTicketListResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AdminSupportTicketRow"}},"page_info":{"$ref":"#/components/schemas/PageInfo"}},"required":["data","page_info"]},"AdminSupportTicketAuditEntry":{"type":"object","properties":{"id":{"type":"string"},"ts":{"type":"string","format":"date-time","example":"2026-05-14T09:30:00Z"},"action":{"type":"string"},"actor":{"type":"string","nullable":true},"user_id":{"type":"string","nullable":true},"outcome":{"type":"string"},"meta":{"type":"object","nullable":true,"additionalProperties":{"nullable":true}}},"required":["id","ts","action","actor","user_id","outcome","meta"]},"AdminSupportTicketDetail":{"allOf":[{"$ref":"#/components/schemas/AdminSupportTicketRow"},{"type":"object","properties":{"customer_email":{"type":"string"},"messages":{"type":"array","items":{"$ref":"#/components/schemas/SupportMessage"}},"audit":{"type":"array","items":{"$ref":"#/components/schemas/AdminSupportTicketAuditEntry"},"default":[]}},"required":["customer_email","messages"]}]},"AdminSupportTicketReplyRequest":{"type":"object","properties":{"body":{"type":"string","minLength":1,"maxLength":10000}},"required":["body"]},"AdminSupportReplyTemplate":{"type":"object","properties":{"id":{"type":"string"},"label":{"type":"string"},"body":{"type":"string"}},"required":["id","label","body"]},"AdminSupportReplyTemplateList":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AdminSupportReplyTemplate"}}},"required":["data"]},"AdminInvoiceListStatus":{"type":"string","enum":["open","paid","void","overdue"]},"AdminInvoiceListRow":{"type":"object","properties":{"id":{"type":"string"},"number":{"type":"string"},"account":{"type":"object","properties":{"account_id":{"type":"string"},"company_name":{"type":"string"},"email":{"type":"string"}},"required":["account_id","company_name","email"]},"amount_minor":{"type":"integer"},"currency":{"type":"string","minLength":3,"maxLength":3},"status":{"$ref":"#/components/schemas/AdminInvoiceListStatus"},"issued_at":{"type":"integer","description":"Unix seconds."},"paid_at":{"type":"integer","nullable":true}},"required":["id","number","account","amount_minor","currency","status","issued_at","paid_at"]},"AdminInvoiceListResponse":{"type":"object","properties":{"invoices":{"type":"array","items":{"$ref":"#/components/schemas/AdminInvoiceListRow"}},"next_cursor":{"type":"string","nullable":true}},"required":["invoices","next_cursor"]},"AdminPaymentKind":{"type":"string","enum":["topup","subscription","signup"],"description":"Payment source. v1 returns rows for `topup` (prepaid balance) and `subscription` (billing-cycle charges). `signup` is accepted as a filter for forward compatibility but currently returns no rows because signup setup_orders carry no amount in the local schema (the first billing_cycle carries the money)."},"AdminPaymentStatus":{"type":"string","enum":["pending","completed","failed","refunded"]},"AdminPaymentListRow":{"type":"object","properties":{"id":{"type":"string"},"kind":{"$ref":"#/components/schemas/AdminPaymentKind"},"customer":{"type":"object","properties":{"account_id":{"type":"string","nullable":true},"company_name":{"type":"string","nullable":true},"email":{"type":"string","nullable":true}},"required":["account_id","company_name","email"],"description":"Owning account when present; null fields appear for topups whose account has been deleted (account_id was SET NULL)."},"amount_minor":{"type":"integer"},"currency":{"type":"string","minLength":3,"maxLength":3},"status":{"$ref":"#/components/schemas/AdminPaymentStatus"},"occurred_at":{"type":"integer","description":"Unix seconds."},"revolut_order_id":{"type":"string","nullable":true}},"required":["id","kind","customer","amount_minor","currency","status","occurred_at","revolut_order_id"]},"AdminPaymentListResponse":{"type":"object","properties":{"payments":{"type":"array","items":{"$ref":"#/components/schemas/AdminPaymentListRow"}},"next_cursor":{"type":"string","nullable":true}},"required":["payments","next_cursor"]},"OperationalTsBucket":{"type":"object","properties":{"ts":{"type":"integer","description":"Bucket start (unix ms)."},"count":{"type":"integer"}},"required":["ts","count"]},"OperationalRequests":{"type":"object","properties":{"total":{"type":"integer"},"ts_buckets":{"type":"array","items":{"$ref":"#/components/schemas/OperationalTsBucket"}},"by_status":{"type":"array","items":{"type":"object","properties":{"status":{"type":"string"},"count":{"type":"integer"}},"required":["status","count"]}}},"required":["total","ts_buckets","by_status"]},"OperationalErrors":{"type":"object","properties":{"total":{"type":"integer"},"ts_buckets":{"type":"array","items":{"$ref":"#/components/schemas/OperationalTsBucket"}},"by_route":{"type":"array","items":{"type":"object","properties":{"route":{"type":"string"},"count":{"type":"integer"}},"required":["route","count"]}},"by_account":{"type":"array","items":{"type":"object","properties":{"account_id":{"type":"string"},"count":{"type":"integer"}},"required":["account_id","count"]}}},"required":["total","ts_buckets","by_route","by_account"]},"OperationalLatency":{"type":"object","nullable":true,"properties":{"p50":{"type":"number"},"p95":{"type":"number"},"p99":{"type":"number"}},"required":["p50","p95","p99"],"description":"Per-request latency percentiles over the range. Null in v1: CF Workers do not emit latency to AE by default; will be populated when the worker explicitly samples request-end into APP_ERRORS or a sibling dataset."},"OperationalAuditVolume":{"type":"object","properties":{"total":{"type":"integer"},"by_action":{"type":"array","items":{"type":"object","properties":{"action":{"type":"string"},"count":{"type":"integer"}},"required":["action","count"]}}},"required":["total","by_action"]},"OperationalDashboardResponse":{"type":"object","properties":{"requests":{"$ref":"#/components/schemas/OperationalRequests"},"errors":{"$ref":"#/components/schemas/OperationalErrors"},"latency":{"$ref":"#/components/schemas/OperationalLatency"},"audit_volume":{"$ref":"#/components/schemas/OperationalAuditVolume"},"range":{"type":"object","properties":{"since":{"type":"integer"},"until":{"type":"integer"},"granularity":{"type":"string","enum":["hour","day"]}},"required":["since","until","granularity"]}},"required":["requests","errors","latency","audit_volume","range"]},"SecurityDashboardAdmin2fa":{"type":"object","properties":{"total_admins":{"type":"integer"},"enrolled":{"type":"integer"},"not_enrolled":{"type":"array","items":{"type":"object","properties":{"user_id":{"type":"string"},"email":{"type":"string"},"days_since_admin_role":{"type":"integer"}},"required":["user_id","email","days_since_admin_role"]}},"recently_used":{"type":"array","items":{"type":"object","properties":{"user_id":{"type":"string"},"email":{"type":"string"},"last_used_at":{"type":"integer"}},"required":["user_id","email","last_used_at"]}}},"required":["total_admins","enrolled","not_enrolled","recently_used"]},"SecurityDashboardFailedSignins":{"type":"object","properties":{"last_24h":{"type":"integer"},"last_7d":{"type":"integer"},"hot_accounts":{"type":"array","items":{"type":"object","properties":{"account_id":{"type":"string"},"failure_count":{"type":"integer"}},"required":["account_id","failure_count"]}}},"required":["last_24h","last_7d","hot_accounts"]},"SecurityDashboardMagicLinks":{"type":"object","properties":{"issued_7d":{"type":"integer"},"consumed_7d":{"type":"integer"},"expired_7d":{"type":"integer"},"by_purpose":{"type":"array","items":{"type":"object","properties":{"purpose":{"type":"string"},"count":{"type":"integer"}},"required":["purpose","count"]}}},"required":["issued_7d","consumed_7d","expired_7d","by_purpose"]},"SecurityDashboardJwtRotation":{"type":"object","properties":{"previous_secret_set":{"type":"boolean"},"rotation_active_since":{"type":"integer","nullable":true,"description":"v1 limitation: always null. Rotation onset is not persisted; this field exists for forward compatibility once an operator-set marker is wired (see plan 0019 A3.4)."}},"required":["previous_secret_set","rotation_active_since"]},"SecurityDashboardResponse":{"type":"object","properties":{"admin_2fa":{"$ref":"#/components/schemas/SecurityDashboardAdmin2fa"},"failed_signins":{"$ref":"#/components/schemas/SecurityDashboardFailedSignins"},"magic_links":{"$ref":"#/components/schemas/SecurityDashboardMagicLinks"},"jwt_rotation":{"$ref":"#/components/schemas/SecurityDashboardJwtRotation"}},"required":["admin_2fa","failed_signins","magic_links","jwt_rotation"]},"BillingDashboardSubscriptions":{"type":"object","properties":{"total_active":{"type":"integer"},"by_tier":{"type":"array","items":{"type":"object","properties":{"tier":{"type":"string"},"count":{"type":"integer"}},"required":["tier","count"]}},"cancelled_in_range":{"type":"integer"},"pending_payment":{"type":"integer"},"suspended":{"type":"integer"}},"required":["total_active","by_tier","cancelled_in_range","pending_payment","suspended"]},"BillingDashboardSchedulerHealth":{"type":"object","properties":{"last_tick_at":{"type":"integer","nullable":true},"ticks_in_range":{"type":"integer"},"success_rate":{"type":"number","description":"Fraction in [0,1]. 1.0 when ticks_in_range is 0."},"recent_failures":{"type":"array","items":{"type":"object","properties":{"ts":{"type":"integer"},"job":{"type":"string"},"error":{"type":"string"}},"required":["ts","job","error"]}},"in_progress_count":{"type":"integer"}},"required":["last_tick_at","ticks_in_range","success_rate","recent_failures","in_progress_count"]},"BillingDashboardRevenue":{"type":"object","properties":{"ts_buckets":{"type":"array","items":{"type":"object","properties":{"ts":{"type":"integer"},"amount_cents":{"type":"integer"},"currency":{"type":"string"}},"required":["ts","amount_cents","currency"]}},"total_amount_cents":{"type":"integer"},"currency":{"type":"string","description":"Single currency when all paid invoices in range agree; \"MIXED\" when buckets span multiple currencies."}},"required":["ts_buckets","total_amount_cents","currency"]},"BillingDashboardResponse":{"type":"object","properties":{"subscriptions":{"$ref":"#/components/schemas/BillingDashboardSubscriptions"},"scheduler_health":{"$ref":"#/components/schemas/BillingDashboardSchedulerHealth"},"revenue":{"$ref":"#/components/schemas/BillingDashboardRevenue"},"top_accounts":{"type":"array","items":{"type":"object","properties":{"account_id":{"type":"string"},"recent_invoice_amount_cents":{"type":"integer"},"recent_invoice_count":{"type":"integer"}},"required":["account_id","recent_invoice_amount_cents","recent_invoice_count"]}},"dunning":{"type":"array","items":{"type":"object","properties":{"state":{"type":"string"},"count":{"type":"integer"}},"required":["state","count"]}},"range":{"type":"object","properties":{"since_ms":{"type":"integer"},"until_ms":{"type":"integer"},"range":{"type":"string"}},"required":["since_ms","until_ms","range"]}},"required":["subscriptions","scheduler_health","revenue","top_accounts","dunning","range"]},"ComplianceDashboardDeletions":{"type":"object","properties":{"pending":{"type":"integer"},"pending_table":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"account_id":{"type":"string"},"requested_at":{"type":"integer"},"days_pending":{"type":"integer"}},"required":["id","account_id","requested_at","days_pending"]}},"completed_30d":{"type":"integer"},"cancelled_30d":{"type":"integer"}},"required":["pending","pending_table","completed_30d","cancelled_30d"]},"ComplianceDashboardKyb":{"type":"object","properties":{"pending":{"type":"integer"},"pending_table":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"account_id":{"type":"string"},"submitted_at":{"type":"integer"},"days_pending":{"type":"integer"}},"required":["id","account_id","submitted_at","days_pending"]}},"oldest_pending_days":{"type":"integer"}},"required":["pending","pending_table","oldest_pending_days"]},"ComplianceDashboardVat":{"type":"object","properties":{"pending":{"type":"integer"},"vies_failures_7d":{"type":"integer"}},"required":["pending","vies_failures_7d"]},"ComplianceDashboardResponse":{"type":"object","properties":{"deletions":{"$ref":"#/components/schemas/ComplianceDashboardDeletions"},"kyb":{"$ref":"#/components/schemas/ComplianceDashboardKyb"},"vat":{"$ref":"#/components/schemas/ComplianceDashboardVat"}},"required":["deletions","kyb","vat"]},"Admin2faEnrollResponse":{"type":"object","properties":{"secret":{"type":"string"},"totp_uri":{"type":"string"},"qr_url":{"type":"string","description":"data: URL the SPA can render as an <img src> tag. Encoded as text/plain payload of the TOTP URI; the SPA uses a client-side QR library to draw the actual matrix."},"recovery_codes":{"type":"array","items":{"type":"string"},"description":"Plaintext recovery codes shown ONCE. The DB stores SHA-256 hashes; the customer must save these out-of-band."}},"required":["secret","totp_uri","qr_url","recovery_codes"]},"Admin2faEnrollBody":{"type":"object","properties":{}},"Admin2faSessionResponse":{"type":"object","properties":{"access_token":{"type":"string"},"expires_at":{"type":"integer"},"refresh_token":{"type":"string"},"token_type":{"type":"string","enum":["Bearer"]},"re_enrol_required":{"type":"boolean","description":"True iff this consume drained the last recovery code; the SPA must redirect the operator to re-enrol."}},"required":["access_token","expires_at","refresh_token","token_type"]},"Admin2faVerifyBody":{"type":"object","properties":{"tmp_token":{"type":"string","minLength":1},"code":{"type":"string","minLength":1}},"required":["tmp_token","code"]},"Admin2faRecoveryBody":{"type":"object","properties":{"tmp_token":{"type":"string","minLength":1},"code":{"type":"string","minLength":1}},"required":["tmp_token","code"]},"Admin2faDeleteResponse":{"type":"object","properties":{"status":{"type":"string","enum":["deleted"]}},"required":["status"]},"Me2faStatusResponse":{"type":"object","properties":{"enrolled":{"type":"boolean"}},"required":["enrolled"]},"Me2faEnrollStartResponse":{"type":"object","properties":{"enroll_token":{"type":"string","description":"Short-lived (5min) signed token carrying the candidate TOTP secret. POST back to /v1/me/2fa/enroll/verify with the first 6-digit code to commit the enrolment."},"enroll_token_expires_at":{"type":"integer"},"secret":{"type":"string"},"totp_uri":{"type":"string"},"qr_url":{"type":"string","description":"data: URL the SPA can render as <img src>. Encoded as text/plain payload of the TOTP URI; the SPA uses a client-side QR library to draw the matrix."}},"required":["enroll_token","enroll_token_expires_at","secret","totp_uri","qr_url"]},"Me2faEnrollStartBody":{"type":"object","properties":{}},"Me2faEnrollVerifyResponse":{"type":"object","properties":{"recovery_codes":{"type":"array","items":{"type":"string"},"description":"Plaintext recovery codes shown ONCE. The DB stores SHA-256 hashes; the customer must save these out-of-band."}},"required":["recovery_codes"]},"Me2faEnrollVerifyBody":{"type":"object","properties":{"enroll_token":{"type":"string","minLength":1},"code":{"type":"string","minLength":1}},"required":["enroll_token","code"]},"Me2faDisableResponse":{"type":"object","properties":{"status":{"type":"string","enum":["disabled"]}},"required":["status"]},"Me2faDisableBody":{"type":"object","properties":{"code":{"type":"string","minLength":1}},"required":["code"]},"Me2faSignInSessionResponse":{"type":"object","properties":{"access_token":{"type":"string"},"expires_at":{"type":"integer"},"refresh_token":{"type":"string"},"token_type":{"type":"string","enum":["Bearer"]},"re_enrol_required":{"type":"boolean","description":"True iff this consume drained the last recovery code; the SPA must redirect the user to re-enrol."}},"required":["access_token","expires_at","refresh_token","token_type"]},"Me2faSignInVerifyBody":{"type":"object","properties":{"tmp_token":{"type":"string","minLength":1},"code":{"type":"string","minLength":1}},"required":["tmp_token","code"]},"Me2faSignInRecoveryBody":{"type":"object","properties":{"tmp_token":{"type":"string","minLength":1},"code":{"type":"string","minLength":1}},"required":["tmp_token","code"]},"MeExportBundle":{"type":"object","properties":{"generated_at":{"type":"integer"},"account":{"type":"object","nullable":true,"additionalProperties":{"nullable":true}},"users":{"type":"array","items":{"type":"object","additionalProperties":{"nullable":true}}},"addresses":{"type":"array","items":{"type":"object","additionalProperties":{"nullable":true}}},"subscriptions":{"type":"array","items":{"type":"object","additionalProperties":{"nullable":true}}},"invoices":{"type":"array","items":{"type":"object","additionalProperties":{"nullable":true}}},"payment_methods":{"type":"array","items":{"type":"object","additionalProperties":{"nullable":true}}},"topups":{"type":"array","items":{"type":"object","additionalProperties":{"nullable":true}}},"kyb_intakes":{"type":"array","items":{"type":"object","additionalProperties":{"nullable":true}}},"vat_verification":{"type":"object","nullable":true,"additionalProperties":{"nullable":true}},"vat_proofs":{"type":"array","items":{"type":"object","additionalProperties":{"nullable":true}}},"audit_log":{"type":"array","items":{"type":"object","additionalProperties":{"nullable":true}}},"magic_links":{"type":"array","items":{"type":"object","additionalProperties":{"nullable":true}}},"refresh_tokens":{"type":"array","items":{"type":"object","additionalProperties":{"nullable":true}}},"banners":{"type":"array","items":{"type":"object","additionalProperties":{"nullable":true}}},"notifications":{"type":"array","items":{"type":"object","additionalProperties":{"nullable":true}}}},"required":["generated_at","account","users","addresses","subscriptions","invoices","payment_methods","topups","kyb_intakes","vat_verification","vat_proofs","audit_log","magic_links","refresh_tokens","banners","notifications"]},"MeDeleteRequestResponse":{"type":"object","properties":{"deletion_request_id":{"type":"string"},"expires_at":{"type":"integer"}},"required":["deletion_request_id","expires_at"]},"MeDeleteRequestBody":{"type":"object","properties":{"confirm_email":{"type":"string","format":"email","description":"Must equal the authenticated user’s email; mismatch yields 400."}},"required":["confirm_email"]},"MeDeleteConfirmResponse":{"type":"object","properties":{"status":{"type":"string","enum":["deleted"]}},"required":["status"]},"MeDeleteConfirmBody":{"type":"object","properties":{"token":{"type":"string","minLength":1}},"required":["token"]},"MeDeleteCancelResponse":{"type":"object","properties":{"status":{"type":"string","enum":["cancelled"]}},"required":["status"]},"MeDeleteCancelBody":{"type":"object","properties":{}},"VatStatusResponse":{"type":"object","properties":{"status":{"allOf":[{"$ref":"#/components/schemas/VatStatus"},{"enum":["none","pending","approved","rejected"]}]},"vat_id":{"type":"string","example":"LT100017026817"},"country":{"type":"string","description":"ISO 3166-1 alpha-2 country code derived from the VAT id prefix.","example":"LT"},"name":{"type":"string","description":"Registered name as returned by VIES (when valid + the member state discloses it).","example":"UAB CODUS NULLUS"},"address":{"type":"string","description":"Registered address as returned by VIES (when valid + the member state discloses it).","example":"VILNIAUS G. 1, VILNIUS"},"vat_checked_at":{"type":"integer","description":"Unix epoch seconds of the most recent successful VIES verification (or admin manual approval)."}},"required":["status"]},"VatProofResponse":{"type":"object","properties":{"accepted":{"type":"boolean","example":true},"id":{"type":"string","example":"vpf_01HXYZ..."},"status":{"type":"string","enum":["pending_review"],"example":"pending_review"},"message":{"type":"string","example":"Document received. We will notify you when verification completes."}},"required":["accepted","id","status","message"]},"ComplianceIntakeList":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/ComplianceIntake"}}},"required":["items"]},"ComplianceIntakeDocument":{"type":"object","properties":{"id":{"type":"string","example":"kybd_01HZ..."},"intake_id":{"type":"string"},"doc_kind":{"type":"string","example":"articles_of_incorporation"},"content_type":{"type":"string","example":"application/pdf"},"size_bytes":{"type":"integer"},"uploaded_at":{"type":"integer"}},"required":["id","intake_id","doc_kind","content_type","size_bytes","uploaded_at"]},"ComplianceDocumentUploadResponse":{"type":"object","properties":{"document":{"$ref":"#/components/schemas/ComplianceIntakeDocument"},"intake":{"$ref":"#/components/schemas/ComplianceIntake"}},"required":["document","intake"]}},"parameters":{}},"paths":{"/v1/_errors":{"post":{"tags":["internal"],"summary":"SPA error ingest (write-only)","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","maxLength":2000},"stack":{"type":"string","maxLength":8000},"url":{"type":"string","maxLength":1000},"ua":{"type":"string","maxLength":500},"app":{"type":"string","enum":["portal"]},"app_version":{"type":"string","maxLength":100},"session_id":{"type":"string","maxLength":200}},"required":["message","url","ua","app"]}}}},"responses":{"204":{"description":"Accepted (or silently rate-limited)."}}}},"/v1/signup":{"post":{"tags":["auth (public)"],"summary":"Start a new pbx-billing signup (trial-first via Revolut Pay 0-amount save)","description":"Plan 0075 (trial-first via Revolut Pay 0-amount save; supersedes 0074). Collects customer details + plan selection. Mints a Revolut Customer, then creates a 0-amount order with `save_payment_method_for: \"merchant\"` and returns its `order_token`. The SPA mounts the Revolut Pay widget with that token + VITE_REVOLUT_PUBLIC_KEY; the customer authorises (card OR Revolut account) and the page shows EUR 0.00 - NOTHING is charged at signup. Our webhook retrieves the saved `payment_method.id` on order completion, materialises the account, creates a local `trialing` subscription (trial length from TRIAL_PERIOD_DURATION, default P7D), provisions the my.pbx.lt tenant, and sends the welcome email. Our own scheduler charges the saved method at trial end and on each monthly cycle.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignupRequest"}}}},"responses":{"201":{"description":"Signup created; SPA mounts the Revolut Pay widget with `order_token`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignupResponse"}}}},"403":{"description":"Individual signups are disabled in this environment.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Email already registered.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"422":{"description":"Invalid plan tier or validation failure.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Configuration or upstream failure.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"502":{"description":"Revolut Merchant API failure.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/signup/{signup_id}":{"get":{"tags":["auth (public)"],"summary":"Poll signup progress","description":"Used by the `/signup/success` page to advance the wizard once the webhook has finished provisioning.","parameters":[{"schema":{"type":"string"},"required":true,"name":"signup_id","in":"path"}],"responses":{"200":{"description":"Current state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignupStatusResponse"}}}},"404":{"description":"Unknown signup id.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/signup/{signup_id}/session":{"post":{"tags":["auth (public)"],"summary":"Exchange a signup grant for a session (auto-login after signup)","description":"Plan 0080 (Feature A). Called by /signup/success once the signup row is active. The `grant` (minted at email-verify) proves this browser verified the signup email; the route checks the grant, the email binding, and requires status=active before minting a session. Returns the standard SignInResponse, or a `{status:\"2fa_required\", tmp_token, ...}` wrapper if the user has customer 2FA. While the row is still provisioning it returns 409 signup.not_ready so the SPA keeps polling.","parameters":[{"schema":{"type":"string"},"required":true,"name":"signup_id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignupSessionRequest"}}}},"responses":{"200":{"description":"Session minted, or 2FA pending (discriminated by `status`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignInResponse"}}}},"401":{"description":"Grant invalid / expired, email mismatch, or no user.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Unknown signup id.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Signup is still provisioning; retry shortly.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/auth/sign-in":{"post":{"tags":["auth (public)"],"summary":"Exchange email + password for a JWT pair (or a 2FA pending token for admins)","description":"Customers: returns the standard SignInResponse on success. Admins enrolled in TOTP 2FA: returns `{status:\"2fa_required\", tmp_token, tmp_token_expires_at}` instead; the SPA must POST that tmp_token + the 6-digit TOTP code to /v1/admin/2fa/verify (or the recovery code to /v1/admin/2fa/recovery-code) to exchange it for the real session. Admins NOT yet enrolled in TOTP 2FA still receive the standard SignInResponse, but the server records a warning audit entry (`admin.signin.no_2fa`).","parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID v4. Required on mutating operations that touch money or upstream pbx provisioning. Server replays the original response on conflict.","example":"b0e2f5b1-1f5a-4d9d-8b8a-1f9c1f9c1f9c"},"required":true,"name":"idempotency-key","in":"header"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignInRequest"}}}},"responses":{"200":{"description":"Authenticated, or 2FA pending for admins (discriminated by `status` field).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignInResponse"}}}},"400":{"description":"Idempotency-Key missing or malformed.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Invalid credentials.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Account is not active.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Idempotency-Key reused with a different request body.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/auth/refresh":{"post":{"tags":["auth (public)"],"summary":"Exchange a refresh token for a new access token","parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID v4. Required on mutating operations that touch money or upstream pbx provisioning. Server replays the original response on conflict.","example":"b0e2f5b1-1f5a-4d9d-8b8a-1f9c1f9c1f9c"},"required":true,"name":"idempotency-key","in":"header"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefreshRequest"}}}},"responses":{"200":{"description":"New access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignInResponse"}}}},"400":{"description":"Idempotency-Key missing or malformed.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Refresh token invalid or expired.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Idempotency-Key reused with a different request body.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/auth/sign-out":{"post":{"tags":["auth (customer)"],"summary":"Revoke the current session","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID v4. Required on mutating operations that touch money or upstream pbx provisioning. Server replays the original response on conflict.","example":"b0e2f5b1-1f5a-4d9d-8b8a-1f9c1f9c1f9c"},"required":true,"name":"idempotency-key","in":"header"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignOutRequest"}}}},"responses":{"204":{"description":"Signed out."},"400":{"description":"Idempotency-Key missing or malformed.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Idempotency-Key reused with a different request body.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/auth/forgot-password":{"post":{"tags":["auth (public)"],"summary":"Request a password-reset email","description":"Always returns 202 regardless of whether the email is registered (anti-enumeration). When the address is known, mints a short-lived magic link with `purpose=reset` and emails it to the customer.","parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID v4. Required on mutating operations that touch money or upstream pbx provisioning. Server replays the original response on conflict.","example":"b0e2f5b1-1f5a-4d9d-8b8a-1f9c1f9c1f9c"},"required":true,"name":"idempotency-key","in":"header"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ForgotPasswordRequest"}}}},"responses":{"202":{"description":"Email queued (returns 202 even if the address is unknown to avoid enumeration)."},"400":{"description":"Idempotency-Key missing or malformed.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Idempotency-Key reused with a different request body.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/auth/reset-password":{"post":{"tags":["auth (public)"],"summary":"Complete password reset","description":"Consumes the reset magic link, hashes the new password, revokes all existing refresh tokens for the account, and returns a fresh access + refresh pair.","parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID v4. Required on mutating operations that touch money or upstream pbx provisioning. Server replays the original response on conflict.","example":"b0e2f5b1-1f5a-4d9d-8b8a-1f9c1f9c1f9c"},"required":true,"name":"idempotency-key","in":"header"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResetPasswordRequest"}}}},"responses":{"200":{"description":"Password reset; session minted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignInResponse"}}}},"400":{"description":"Idempotency-Key missing or malformed.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Reset token invalid, expired, consumed, or wrong purpose.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Idempotency-Key reused with a different request body.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/auth/first-login":{"post":{"tags":["auth (public)"],"summary":"Sign in via the welcome magic link emailed after signup","description":"Consumes the first-login magic link emailed at signup completion and returns a fresh session. The user can later set a password via /me/password (out of scope).","parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID v4. Required on mutating operations that touch money or upstream pbx provisioning. Server replays the original response on conflict.","example":"b0e2f5b1-1f5a-4d9d-8b8a-1f9c1f9c1f9c"},"required":true,"name":"idempotency-key","in":"header"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FirstLoginRequest"}}}},"responses":{"200":{"description":"Session minted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignInResponse"}}}},"400":{"description":"Idempotency-Key missing or malformed.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"First-login token invalid, expired, consumed, or wrong purpose.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Idempotency-Key reused with a different request body.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/auth/magic-link/request":{"post":{"tags":["auth (public)"],"summary":"Request a magic-link sign-in email for a returning customer","description":"Always returns 202 regardless of whether the email is registered (anti-enumeration). When the address is known, mints a short-lived magic link with `purpose=first_login` (reused; see route comments) and emails it to the customer. The link target is `https://billing.pbx.lt/auth/verify?token=<plaintext>` (production) or the staging equivalent.","parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID v4. Required on mutating operations that touch money or upstream pbx provisioning. Server replays the original response on conflict.","example":"b0e2f5b1-1f5a-4d9d-8b8a-1f9c1f9c1f9c"},"required":true,"name":"idempotency-key","in":"header"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MagicLinkRequest"}}}},"responses":{"202":{"description":"Email queued (returns 202 even if the address is unknown to avoid enumeration)."},"400":{"description":"Idempotency-Key missing or malformed.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Idempotency-Key reused with a different request body.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/auth/magic-link/verify":{"post":{"tags":["auth (public)"],"summary":"Consume a sign-in magic link and mint a session","description":"Consumes the magic link emailed by `/v1/auth/magic-link/request` (or the welcome email after signup completion - the verify path accepts both since they share `purpose=first_login`) and returns a fresh access + refresh pair plus the embedded user profile. `purpose=reset` links are not accepted here; those go through `/v1/auth/reset-password`.","parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID v4. Required on mutating operations that touch money or upstream pbx provisioning. Server replays the original response on conflict.","example":"b0e2f5b1-1f5a-4d9d-8b8a-1f9c1f9c1f9c"},"required":true,"name":"idempotency-key","in":"header"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MagicLinkVerifyRequest"}}}},"responses":{"200":{"description":"Session minted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignInResponse"}}}},"400":{"description":"Idempotency-Key missing or malformed.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Magic link invalid, expired, consumed, or wrong purpose.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Idempotency-Key reused with a different request body.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/auth/verification/request":{"post":{"tags":["auth (public)"],"summary":"Request a verification code + link email (signup email-verify or passwordless login)","description":"Plan 0076. Mints a 6-digit code and a magic link for the given `purpose` and emails both. Always returns 202. For `purpose=login` the email must map to a known account (else the request is silently ignored - anti-enumeration). For `purpose=email_verify` a row is always minted keyed on the email so signup can later gate on the verified address.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerificationRequest"}}}},"responses":{"202":{"description":"Email queued (returns 202 even if the address is unknown to avoid enumeration)."},"422":{"description":"Validation failure.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/auth/verification/verify":{"post":{"tags":["auth (public)"],"summary":"Verify a code or link (signup email-verify or passwordless login)","description":"Plan 0076. Accepts a typed `code` (with the `email` it was sent to) OR a link `token`. For `purpose=email_verify` returns `{ verified: true }`. For `purpose=login` mints and returns a session (the standard SignInResponse, or a 2fa-pending wrapper if the user has customer 2FA). Every failure - wrong code, expired, consumed, attempt cap - returns the same opaque `auth.verification_invalid` so a code's state cannot be probed.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerificationVerifyRequest"}}}},"responses":{"200":{"description":"Login session minted (purpose=login).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignInResponse"}}}},"202":{"description":"Email verified (purpose=email_verify).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerificationVerifiedResponse"}}}},"401":{"description":"Code or link invalid, expired, consumed, or attempt cap reached.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"422":{"description":"Validation failure.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/me":{"get":{"tags":["auth (customer)"],"summary":"Current user profile","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Profile.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeResponse"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"User row not found for this token.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}},"patch":{"tags":["auth (customer)"],"summary":"Update profile / language / notification preferences","description":"Partial update. `first_name`, `last_name`, and `notifications_enabled` mutate the user row. `language` updates the parent account row (single source of truth for locale per the i18n policy). Every field is optional - an empty body returns the current profile unchanged.","security":[{"bearerAuth":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateMeRequest"}}}},"responses":{"200":{"description":"Updated profile.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeResponse"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"User row not found for this token.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/me/password":{"post":{"tags":["auth (customer)"],"summary":"Set or change the current user password","description":"Sets a new password for the authenticated user and revokes every existing refresh token for the account, forcing all other sessions to re-authenticate. `current_password` is required when the user already has a password on file. First-login users (no `pwd_hash` yet) may omit `current_password`. Use `Idempotency-Key` to make the call safe to retry.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID v4. Required on mutating operations that touch money or upstream pbx provisioning. Server replays the original response on conflict.","example":"b0e2f5b1-1f5a-4d9d-8b8a-1f9c1f9c1f9c"},"required":true,"name":"idempotency-key","in":"header"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangePasswordRequest"}}}},"responses":{"204":{"description":"Password set; refresh tokens revoked."},"400":{"description":"Idempotency-Key missing or `current_password` required but not provided.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Unauthenticated, or `current_password` is incorrect.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Idempotency-Key reused with a different request body.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/me/account":{"post":{"tags":["auth (customer)"],"summary":"Backfill the signed-in account with company details captured after signup","description":"Partial update of the `accounts` row tied to the bearer token. Every field is optional so the SPA can drip-feed completion. Allowed: `company_name`, `contact_phone`, `reg_number`, `vat_id`. `email`, `language`, `status`, `pbx_tenant_id`, and the lifecycle timestamps are not mutable through this endpoint. For structured legal / invoice addresses and VIES re-validation use `PATCH /v1/account` instead. Use `Idempotency-Key` to make the call safe to retry.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID v4. Required on mutating operations that touch money or upstream pbx provisioning. Server replays the original response on conflict.","example":"b0e2f5b1-1f5a-4d9d-8b8a-1f9c1f9c1f9c"},"required":true,"name":"idempotency-key","in":"header"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateMeAccountRequest"}}}},"responses":{"200":{"description":"Updated account row.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeAccountResponse"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Account row not found for this token.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Idempotency-Key reused with a different request body.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}},"get":{"tags":["auth (customer)"],"summary":"Read the signed-in account row in the lighter /me projection","description":"Returns the same shape as `POST /v1/me/account`: `account_id`, `email`, `company_name`, `contact_phone`, `reg_number`, `vat_id`, `vat_status`, `language`, `pbx_tenant_id`, `status`, `created_at`, `updated_at`. For the heavier projection that includes structured legal / invoice address rows, use `GET /v1/account` instead.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Account row.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeAccountResponse"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Account row not found for this token.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/account":{"get":{"tags":["account"],"summary":"Get the current account (full projection with structured addresses)","description":"Returns the account row plus its `legal` and `invoice` addresses (each may be null until the customer fills them in). For the lighter projection without addresses use `GET /v1/me/account` instead.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountResponse"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Account row not found for this token.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}},"patch":{"tags":["account"],"summary":"Update company details and VAT id","description":"Plan 0029 F2 (#4). Updates the writable account fields (company_name, contact_phone, reg_number, vat_id). On a vat_id change the handler consults the 30-day VIES cache; on cache miss it calls the EU VIES SOAP endpoint and persists the result. VIES outage is fail-OPEN per the plan: vat_status flips to 'pending', the PATCH succeeds, and a follow-up GET /v1/vat will re-verify lazily. Address fields are ignored by this endpoint - the SPA uses POST /v1/account/addresses for those.","security":[{"bearerAuth":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAccountRequest"}}}},"responses":{"200":{"description":"Updated account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountResponse"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Account row not found for this token.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"422":{"description":"VAT id failed VIES validation (registry says invalid).","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/account/addresses":{"post":{"tags":["account"],"summary":"Create or replace one structured address for the signed-in account","description":"Upserts the address keyed by `(account_id, kind)` where kind is `legal` or `invoice`. The legal address is the company registered/postal address; the invoice address is an optional separate billing address that overrides legal on invoice headers. Returns the full account projection so the SPA can re-render the Company page from one round trip.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID v4. Required on mutating operations that touch money or upstream pbx provisioning. Server replays the original response on conflict.","example":"b0e2f5b1-1f5a-4d9d-8b8a-1f9c1f9c1f9c"},"required":true,"name":"idempotency-key","in":"header"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpsertAddressRequest"}}}},"responses":{"200":{"description":"Updated account row with addresses.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountResponse"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Account row not found for this token.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Idempotency-Key reused with a different request body.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/account/addresses/{kind}":{"delete":{"tags":["account"],"summary":"Remove one structured address (legal or invoice)","description":"Idempotent: returns 204 whether the row was actually present or already absent. The SPA can call this safely on every Save action.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"$ref":"#/components/schemas/AddressKind"},"required":true,"name":"kind","in":"path"}],"responses":{"204":{"description":"Removed (or already absent)."},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/subscription":{"get":{"tags":["subscription"],"summary":"Get the current subscription","description":"Plan 0029 F2 (#1). Hydrates the active subscription row for the signed-in account. The response shape mirrors POST /subscription/change-plan: same field set, same epoch-second to ISO conversions. Dunning state is NOT exposed via this endpoint by design - the SubscriptionResponseSchema does not include dunning columns and the SPA reads them via GET /v1/me/data (banners path) instead. Keeping the surface narrow avoids a schema expansion that would ripple into the admin SPA + change-plan response.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Subscription.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Subscription"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"No subscription on this account.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/subscription/change-plan":{"post":{"tags":["subscription"],"summary":"Schedule a plan change for the next billing cycle","description":"Phase D of plan 0014: writes `plan_tier_pending` + `plan_tier_pending_effective_at` on the subscription. The cn-scheduler planning pass picks up the pending tier on the next cron tick, snapshots the new amount, and the charge pass promotes `plan_tier` to the new value once the cycle charges successfully. There is no Revolut hosted-checkout step - the new tier just takes effect at the next cycle boundary. Legacy `revolut_native` subscriptions are rejected with 409 `subscription.legacy_engine_change_unsupported`; they must be migrated to the cn-scheduler engine first.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID v4. Required on mutating operations that touch money or upstream pbx provisioning. Server replays the original response on conflict.","example":"b0e2f5b1-1f5a-4d9d-8b8a-1f9c1f9c1f9c"},"required":true,"name":"idempotency-key","in":"header"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangePlanRequest"}}}},"responses":{"200":{"description":"Plan change scheduled.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangePlanResponse"}}}},"400":{"description":"Idempotency-Key missing.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"No active subscription.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Same tier as current, no payment method, legacy engine, or Idempotency-Key reused with a different request body.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/subscription/change-plan-now":{"post":{"tags":["subscription"],"summary":"Change plan immediately with prorated delta","description":"Plan 0025 phase B2v2: customer changes tier mid-cycle. Server computes the prorated delta via @codus-nullus/billing-primitives' computePlanChange, charges the saved card on upgrade (delta > 0), refunds the most-recently-charged cycle on downgrade (delta < 0), or no-ops (delta == 0). Always rewrites subscriptions.plan_tier + price_amount_minor; on upgrade also resets the cycle anchor to now (next renewal lands 30 days from the change). Idempotency-Key wrapped so a client retry never double-charges.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID v4. Required on mutating operations that touch money or upstream pbx provisioning. Server replays the original response on conflict.","example":"b0e2f5b1-1f5a-4d9d-8b8a-1f9c1f9c1f9c"},"required":true,"name":"idempotency-key","in":"header"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangePlanNowRequest"}}}},"responses":{"200":{"description":"Plan change applied; delta charged or refunded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangePlanNowResponse"}}}},"400":{"description":"Idempotency-Key missing.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"No active subscription.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Same tier as current, no payment method, legacy engine, or Idempotency-Key reused with a different request body.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"502":{"description":"Revolut charge or refund failed.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/subscription/cancel":{"post":{"tags":["subscription"],"summary":"Cancel the active subscription","description":"Calls Revolut /subscriptions/{id}/cancel and marks the local row cancelled. Effective immediately at Revolut.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID v4. Required on mutating operations that touch money or upstream pbx provisioning. Server replays the original response on conflict.","example":"b0e2f5b1-1f5a-4d9d-8b8a-1f9c1f9c1f9c"},"required":true,"name":"idempotency-key","in":"header"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CancelSubscriptionRequest"}}}},"responses":{"200":{"description":"cancelled.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Subscription"}}}},"400":{"description":"Idempotency-Key missing.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"No active subscription.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Already cancelled, or Idempotency-Key reused with a different request body.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"502":{"description":"Revolut Merchant API failure.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/subscription/reactivate":{"post":{"tags":["subscription"],"summary":"Reactivate a `cancel_at_period_end` subscription before period end","description":"Plan 0029 F2 (#5). Default-ship behavior per the plan: clear `cancel_at_period_end` and rewrite `status='active'` so the next renewal cycle charges normally. NO money moves at reactivation time - the customer continues on the existing cycle and is billed again at the next period boundary. Idempotent via the standard Idempotency-Key wrapper; a second call with the same key replays the same response.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID v4. Required on mutating operations that touch money or upstream pbx provisioning. Server replays the original response on conflict.","example":"b0e2f5b1-1f5a-4d9d-8b8a-1f9c1f9c1f9c"},"required":true,"name":"idempotency-key","in":"header"}],"responses":{"200":{"description":"Active subscription.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Subscription"}}}},"400":{"description":"Idempotency-Key missing.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"No subscription on this account.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Subscription is not in cancel_at_period_end state.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/payment-methods":{"get":{"tags":["payment-methods"],"summary":"List saved payment methods","description":"Returns the signed-in account's verified cards. Pending / failed verification rows are filtered out so the SPA only sees usable methods. Cards are sorted by creation time (oldest first) to keep the default-first ordering stable.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Saved methods.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/PaymentMethod"}}},"required":["data"]}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}},"post":{"tags":["payment-methods"],"summary":"Begin adding a payment method (Revolut hosted flow)","description":"Returns a Revolut-hosted checkout URL for the signed-in customer. Raw card data never touches this API. The webhook ORDER_AUTHORISED handler in webhooks.ts (plan 0024 A1) captures + refunds the auth and saves the resulting payment_method row.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID v4. Required on mutating operations that touch money or upstream pbx provisioning. Server replays the original response on conflict.","example":"b0e2f5b1-1f5a-4d9d-8b8a-1f9c1f9c1f9c"},"required":true,"name":"idempotency-key","in":"header"}],"responses":{"200":{"description":"Hosted-flow URL returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddPaymentMethodResponse"}}}},"400":{"description":"Idempotency-Key missing.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Account row not found for this token.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"502":{"description":"Tokenisation failed at upstream.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/payment-methods/verify":{"post":{"tags":["payment-methods"],"summary":"Verify and save a card on file (no subscription)","description":"Standard Revolut \"save card on file\" pattern. Mints a manual-capture order with save_payment_method_for=merchant. The customer completes hosted-checkout; on ORDER_AUTHORISED our webhook saves the payment_method and immediately cancels the auth so no charge is made. Use micro_auth (default, EUR 0.01) for production today; zero_auth probes EUR 0 first and transparently falls back to micro_auth on Revolut rejection.","parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID v4. Required on mutating operations that touch money or upstream pbx provisioning. Server replays the original response on conflict.","example":"b0e2f5b1-1f5a-4d9d-8b8a-1f9c1f9c1f9c"},"required":true,"name":"idempotency-key","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyPaymentMethodRequest"}}}},"responses":{"200":{"description":"Verification started; redirect customer to `checkout_url`. Replays the original response if the same Idempotency-Key is used with the same body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyPaymentMethodResponse"}}}},"400":{"description":"Idempotency-Key missing or malformed.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Idempotency-Key reused with a different request body.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"422":{"description":"Validation failure.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Configuration failure.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"502":{"description":"Revolut Merchant API failure.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/payment-methods/{payment_method_id}/default":{"post":{"tags":["payment-methods"],"summary":"Set a payment method as default","description":"Promotes the named card to is_default = true and demotes every other card on the account in one batch so the \"exactly one default per account\" invariant holds even on a half-failure. 403 when the card belongs to another account.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"payment_method_id","in":"path"}],"responses":{"200":{"description":"Updated method.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentMethod"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Payment method belongs to another account.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Unknown payment method.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/payment-methods/{payment_method_id}":{"delete":{"tags":["payment-methods"],"summary":"Remove a saved payment method","description":"Removing the default method is blocked - set another as default first.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"payment_method_id","in":"path"}],"responses":{"204":{"description":"Removed."},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Payment method belongs to another account.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Unknown payment method.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Cannot remove default payment method.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/payments/topups":{"post":{"tags":["payments"],"summary":"Initiate a prepaid call-balance top-up","description":"Mints a Revolut hosted-checkout order. On webhook ORDER_COMPLETED the prepaid balance ledger is credited.","parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID v4. Required on mutating operations that touch money or upstream pbx provisioning. Server replays the original response on conflict.","example":"b0e2f5b1-1f5a-4d9d-8b8a-1f9c1f9c1f9c"},"required":true,"name":"idempotency-key","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TopupRequest"}}}},"responses":{"200":{"description":"Topup created on Revolut, or replayed cached response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TopupResponse"}}}},"400":{"description":"Idempotency-Key missing or malformed.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Idempotency-Key reused with a different request body.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Configuration failure.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"502":{"description":"Revolut Merchant API failure.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/invoices":{"get":{"tags":["invoices"],"summary":"List invoices (paginated)","description":"Returns the signed-in account's invoices, newest issued_at first. Cursor pagination: the opaque `cursor` is the last seen `issued_at` epoch and the next page contains rows strictly older than that timestamp. `next_cursor` is null when the page is the final one.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","description":"Opaque cursor from a prior `next_cursor`."},"required":false,"name":"cursor","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":200,"default":50,"description":"Page size (default 50, max 200)."},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Invoices.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvoiceListResponse"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/invoices/{invoice_id}":{"get":{"tags":["invoices"],"summary":"Get one invoice","description":"Returns the InvoiceSchema projection for one invoice owned by the signed-in account. 403 when the invoice exists but belongs to another account; 404 when the id is unknown.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"invoice_id","in":"path"}],"responses":{"200":{"description":"Invoice.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Invoice"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Invoice belongs to another account.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Unknown invoice.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/invoices/{invoice_id}/pdf":{"get":{"tags":["invoices"],"summary":"Download invoice PDF","description":"Streams the rendered PDF directly from R2 with `Content-Type: application/pdf`. The PDF is generated at issuance time by `@codus-nullus/billing-primitives` and uploaded to the `INVOICES` R2 bucket; this endpoint reads the bytes back. Customer-scoped: a 403 is returned when the invoice belongs to another account.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"invoice_id","in":"path"},{"schema":{"type":"string","description":"BCP 47 tag. Honored on endpoints that emit user-visible text or trigger server-rendered output (welcome email, invoice PDF). Defaults to `en`.","example":"lt-LT"},"required":false,"name":"accept-language","in":"header"}],"responses":{"200":{"description":"PDF bytes.","content":{"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Invoice belongs to another account.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Unknown invoice or PDF not yet generated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/me/credit-notes":{"get":{"tags":["invoices"],"summary":"List credit notes for the signed-in account","description":"Returns every credit note issued against one of the signed-in account's invoices, newest first. Cross-tenant scoping is enforced by the underlying JOIN against the invoices table (the credit_notes ledger has no account_id column of its own).","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Credit notes.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreditNoteListResponse"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/tenant":{"get":{"tags":["tenants"],"summary":"Get the signed-in account's primary tenant (singular)","description":"Returns the upstream pbx tenant id mirrored on the account row plus a deep-link to https://my.pbx.lt. When the operator service token is provisioned, real tenant info (sip_domain, deep-link) is pulled from upstream and cached 60s. Falls back to a local-row projection when the token is unset OR the upstream call fails. The richer per-tenant projection lives at `GET /v1/tenants/{tenant_id}` (not yet implemented).","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Tenant info derived from the account row + upstream pull.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TenantInfoResponse"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Account row not found for this token.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/tenants":{"get":{"tags":["tenants"],"summary":"List pbx tenants owned by this account","description":"Subset view; field reference in src/schemas/tenant.ts. Mirrored from `https://my.pbx.lt/api/v1/admin/tenants` filtered by account ownership. SIP-password generation is NOT exposed here - that lives on my.pbx.lt itself for security boundary reasons.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Tenants.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TenantListResponse"}}}},"502":{"description":"Upstream pbx core unreachable.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/tenants/{tenant_id}":{"get":{"tags":["tenants"],"summary":"Get one tenant","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"tenant_id","in":"path"}],"responses":{"200":{"description":"Tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Tenant"}}}},"403":{"description":"Tenant not owned by this account.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Unknown tenant.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/support/tickets":{"get":{"tags":["support"],"summary":"List support tickets","description":"Returns the signed-in account's tickets, most-recent-activity first. Cursor pagination: opaque `cursor` is the last seen `last_activity_at` epoch; the next page returns tickets with `last_activity_at` strictly older than it.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","description":"Opaque cursor from a prior `next_cursor`."},"required":false,"name":"cursor","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":200,"default":50,"description":"Page size (default 50, max 200)."},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Tickets.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SupportTicketListResponse"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}},"post":{"tags":["support"],"summary":"Open a new support ticket","description":"Creates the ticket row + its opening message in one call. Status starts as `open` and `last_activity_at` is set to the create time. Idempotency-Key required; replays return the originally-created ticket without writing a second row.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID v4. Required on mutating operations that touch money or upstream pbx provisioning. Server replays the original response on conflict.","example":"b0e2f5b1-1f5a-4d9d-8b8a-1f9c1f9c1f9c"},"required":true,"name":"idempotency-key","in":"header"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSupportTicketRequest"}}}},"responses":{"201":{"description":"Created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SupportTicket"}}}},"400":{"description":"Idempotency-Key missing.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Idempotency-Key reused with a different request body.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/support/tickets/{ticket_id}":{"get":{"tags":["support"],"summary":"Get a support ticket with its message thread","description":"Account-scoped: a 403 is returned when the ticket exists but is owned by another account. Plan 0055 (Wave 11 KK): response now includes the chronological message thread so the customer SPA can render the ticket-detail page without a second round-trip.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"ticket_id","in":"path"}],"responses":{"200":{"description":"Ticket with messages.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SupportTicketDetail"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Ticket belongs to another account.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Unknown ticket.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/admin/tenants":{"get":{"tags":["admin"],"summary":"Admin-wide tenant list","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","description":"Opaque cursor from a prior `next_cursor`."},"required":false,"name":"cursor","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":200,"default":50,"description":"Page size (default 50, max 200)."},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","enum":["active","suspended","deleted","past_due"]},"required":false,"name":"status","in":"query"},{"schema":{"type":"string"},"required":false,"name":"search","in":"query"}],"responses":{"200":{"description":"Tenants.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminTenantListResponse"}}}},"403":{"description":"Admin role required.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/admin/tenants/provision":{"post":{"tags":["admin"],"summary":"Manual provisioning override","description":"Used when automated provisioning fails (e.g. transient pbx-core outage at signup). Calls the same upstream sequence with admin attribution in the audit trail.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID v4. Required on mutating operations that touch money or upstream pbx provisioning. Server replays the original response on conflict.","example":"b0e2f5b1-1f5a-4d9d-8b8a-1f9c1f9c1f9c"},"required":true,"name":"idempotency-key","in":"header"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminProvisionTenantRequest"}}}},"responses":{"201":{"description":"Provisioned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Tenant"}}}},"409":{"description":"Idempotency-Key reused with a different request body.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"502":{"description":"Upstream pbx core failed.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/admin/tenants/{tenant_id}/state":{"post":{"tags":["admin"],"summary":"Suspend or restore a tenant","description":"Calls `PUT /admin/tenants/{id}` upstream on my.pbx.lt with status=suspended/active.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"tenant_id","in":"path"},{"schema":{"type":"string","format":"uuid","description":"UUID v4. Required on mutating operations that touch money or upstream pbx provisioning. Server replays the original response on conflict.","example":"b0e2f5b1-1f5a-4d9d-8b8a-1f9c1f9c1f9c"},"required":true,"name":"idempotency-key","in":"header"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminTenantStateTransitionRequest"}}}},"responses":{"200":{"description":"Updated tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Tenant"}}}},"409":{"description":"Idempotency-Key reused with a different request body.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"502":{"description":"Upstream pbx core failed.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/admin/invoices/credit-note":{"post":{"tags":["admin"],"summary":"Issue a manual credit note against an invoice","description":"Plan 0029 F2 (#3). Inserts a `credit_notes` ledger row, decrements the invoice's open balance via applyCreditToInvoice, and emits an `admin.invoice_credit_note` audit row. Amount must be > 0 and <= the invoice's current `total_minor`; currencies must match. The invoice's status flips to `refunded` when the balance zeroes and `partially_refunded` otherwise. The handler does NOT regenerate the invoice PDF - that is a separate operator action (POST /v1/admin/invoices/{id}/regenerate-pdf). Plan 0063 (Wave 12 MM): when `revolut_refund: true`, the handler ALSO invokes the Revolut Merchant API refund endpoint against the source billing-cycle order BEFORE inserting the credit-note row (atomic: a Revolut 4xx/5xx returns 502 and persists nothing) and dispatches the `refundIssued` customer email instead of `creditNoteIssued`.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID v4. Required on mutating operations that touch money or upstream pbx provisioning. Server replays the original response on conflict.","example":"b0e2f5b1-1f5a-4d9d-8b8a-1f9c1f9c1f9c"},"required":true,"name":"idempotency-key","in":"header"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminCreditNoteRequest"}}}},"responses":{"201":{"description":"Credit note issued.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Invoice"}}}},"400":{"description":"Amount mismatched currency, zero, or exceeds invoice balance.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Unknown invoice.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Idempotency-Key reused with a different request body.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"422":{"description":"revolut_refund requested but the invoice has no linked Revolut order.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"502":{"description":"Revolut refund failed; credit note not issued.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/admin/invoices/{invoice_id}/regenerate-pdf":{"post":{"tags":["admin"],"summary":"Force PDF regeneration for an invoice","description":"Used after template fixes or VAT-rule corrections. Honors `Accept-Language` for the rendered PDF locale.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"invoice_id","in":"path"},{"schema":{"type":"string","format":"uuid","description":"UUID v4. Required on mutating operations that touch money or upstream pbx provisioning. Server replays the original response on conflict.","example":"b0e2f5b1-1f5a-4d9d-8b8a-1f9c1f9c1f9c"},"required":true,"name":"idempotency-key","in":"header"}],"responses":{"202":{"description":"Regeneration queued."},"404":{"description":"Unknown invoice.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Idempotency-Key reused with a different request body.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/admin/subscriptions/{subscription_id}/transition":{"post":{"tags":["admin"],"summary":"Manual subscription state transition","description":"Operator-driven transitions outside the normal lifecycle (refund-induced cancellation, dunning override). Always logged with reason for audit.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"subscription_id","in":"path"},{"schema":{"type":"string","format":"uuid","description":"UUID v4. Required on mutating operations that touch money or upstream pbx provisioning. Server replays the original response on conflict.","example":"b0e2f5b1-1f5a-4d9d-8b8a-1f9c1f9c1f9c"},"required":true,"name":"idempotency-key","in":"header"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminSubscriptionTransitionRequest"}}}},"responses":{"204":{"description":"Transitioned."},"404":{"description":"Unknown subscription.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Idempotency-Key reused with a different request body.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/admin/subscriptions/{subscription_id}/change-plan":{"post":{"tags":["admin"],"summary":"Operator override for plan changes","description":"Phase D (plan 0014). `effective: 'next_cycle'` is the customer-flow analogue: writes plan_tier_pending so the next cycle picks up the new tier. `effective: 'immediate'` rewrites plan_tier + price_amount_minor immediately and clears any pending change; the current cycle is NOT retroactively re-priced (proration is out of scope for v1).","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"subscription_id","in":"path"},{"schema":{"type":"string","format":"uuid","description":"UUID v4. Required on mutating operations that touch money or upstream pbx provisioning. Server replays the original response on conflict.","example":"b0e2f5b1-1f5a-4d9d-8b8a-1f9c1f9c1f9c"},"required":true,"name":"idempotency-key","in":"header"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminChangePlanRequest"}}}},"responses":{"200":{"description":"Plan change applied or scheduled.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangePlanResponse"}}}},"404":{"description":"Unknown subscription.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Same tier as current, legacy engine, or Idempotency-Key reused with a different request body.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/admin/billing-cycles":{"get":{"tags":["admin"],"summary":"List billing-cycle rows","description":"Inspection surface for the cn-scheduler planning pass (phase A of plan 0014). Returns the most recent cycles first, optionally filtered by subscription and/or state. NO money is moved by this endpoint or by phase A in general; it exists so we can read the planned cycles like an EXPLAIN PLAN before phase B turns charging on.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","description":"Restrict to one subscription. Combine with `state` to filter further."},"required":false,"name":"subscription_id","in":"query"},{"schema":{"type":"string","enum":["planned","charging","charged","failed","skipped","abandoned"],"description":"Restrict to cycles in this state."},"required":false,"name":"state","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":500,"default":100,"description":"Max rows to return. Defaults to 100; capped at 500 for the inspection use-case."},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Billing cycles, newest first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminBillingCycleListResponse"}}}},"403":{"description":"Admin role required.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/admin/scheduler/health":{"get":{"tags":["admin"],"summary":"cn-scheduler health summary","description":"Phase E of plan 0014. Returns the last-24h cycle outcome counts (planned / charged / failed / abandoned), the age of the oldest cycle stuck in `charging`, and the current dunning_state distribution across cn_scheduler subscriptions. All counts come from D1 - the Analytics Engine binding is for dashboarding only.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Scheduler health snapshot.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SchedulerHealthResponse"}}}},"403":{"description":"Admin role required.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/admin/scheduler/alerts":{"get":{"tags":["admin"],"summary":"Active cn-scheduler alerts","description":"Phase E of plan 0014. Returns active alerts for the cn-scheduler. Two checks today: (1) any cycle stuck in `charging` for more than 1 hour, (2) more than 10% of cycles updated in the last 24h ended in `failed` or `abandoned`. An empty `alerts` array means the scheduler is healthy. Notification routing (CF Email Routing? PagerDuty?) is deferred per plan 0014 phase E step 5; this endpoint just exposes the data.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Active alerts (empty when healthy).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SchedulerAlertsResponse"}}}},"403":{"description":"Admin role required.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/admin/metrics":{"get":{"tags":["admin"],"summary":"Signup + MRR + dunning + churn across MTD / 30d / 90d","description":"Plan 0035 (Wave 4 M2). Returns three windows in one response (`mtd`, `last_30_days`, `last_90_days`), each carrying totals + a per-plan-tier breakdown. MRR is sum(active subs * TIER_PRICE_MINOR). Churn rate is cancellations-in-window / active-at-window-start. Cached for 5 minutes in KV (RATE_LIMITS namespace, prefix `mc:metrics:`).","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Metrics.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminMetricsResponse"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Admin role required.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/webhooks/revolut":{"post":{"tags":["webhooks"],"summary":"Revolut Merchant API webhook receiver","description":"Verifies signature + freshness, deduplicates by event id, advances signup / subscription / topup state.","parameters":[{"schema":{"type":"string","example":"v1=4f8d..."},"required":true,"name":"revolut-signature","in":"header"},{"schema":{"type":"string","example":"1715688600000"},"required":true,"name":"revolut-request-timestamp","in":"header"}],"responses":{"200":{"description":"Acknowledged.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RevolutWebhookAck"}}}},"400":{"description":"Missing or malformed headers.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Invalid signature or replay window exceeded.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/me/notifications":{"get":{"tags":["me-notifications"],"summary":"List in-app notifications for the signed-in user","description":"Cursor-paginated, newest-first. `cursor_next` is the `created_at` of the last item on the page; pass it back as `cursor` to fetch the next page. `unread_count` is the total unread count for the account (drives the SPA bell badge).","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","description":"Opaque cursor from a prior `cursor_next`."},"required":false,"name":"cursor","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":200,"description":"Page size (default 20, max 200)."},"required":false,"name":"per_page","in":"query"}],"responses":{"200":{"description":"Page of notifications.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedNotificationsResponse"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/me/notifications/{id}/read":{"post":{"tags":["me-notifications"],"summary":"Mark a single notification as read","description":"Idempotent: a second call on the same id is a no-op success. 404 when the notification does not exist or does not belong to the signed-in account.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"204":{"description":"Marked as read."},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Notification not found.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/me/notifications/mark-all-read":{"post":{"tags":["me-notifications"],"summary":"Mark every notification as read","description":"Flips every unread notification for the signed-in account. Idempotent.","security":[{"bearerAuth":[]}],"responses":{"204":{"description":"All marked as read."},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/me/banners":{"get":{"tags":["me-banners"],"summary":"Active account banners for the signed-in customer","description":"Severity-ordered list (critical first). Empty array when nothing is active. Banners are produced by the dunning state machine and by admin operators; expired rows are filtered out server-side.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Active banners.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountBannerListResponse"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/admin/accounts/{account_id}/banners":{"get":{"tags":["admin"],"summary":"Admin: list every banner ever set on an account","description":"Includes inactive rows for audit. Newest-first. The customer view at `/v1/me/banners` returns only the active subset.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"account_id","in":"path"}],"responses":{"200":{"description":"Banners.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminAccountBannerListResponse"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}},"post":{"tags":["admin"],"summary":"Admin: set (or update) an account banner","description":"Upsert by `(account_id, kind)` over the active set. A request whose `kind` collides with an existing active banner updates that row in place; a new `kind` inserts a fresh row. `source` is recorded as `admin` and `source_ref` carries the operator id from the JWT.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"account_id","in":"path"},{"schema":{"type":"string","format":"uuid","description":"UUID v4. Required on mutating operations that touch money or upstream pbx provisioning. Server replays the original response on conflict.","example":"b0e2f5b1-1f5a-4d9d-8b8a-1f9c1f9c1f9c"},"required":true,"name":"idempotency-key","in":"header"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminCreateBannerRequest"}}}},"responses":{"201":{"description":"Banner created or updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountBanner"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Idempotency-Key reused with a different request body.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/admin/accounts/{account_id}/banners/{banner_id}/clear":{"post":{"tags":["admin"],"summary":"Admin: clear an active banner","description":"Sets `active = 0`; the row is preserved for audit.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"account_id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"banner_id","in":"path"},{"schema":{"type":"string","format":"uuid","description":"UUID v4. Required on mutating operations that touch money or upstream pbx provisioning. Server replays the original response on conflict.","example":"b0e2f5b1-1f5a-4d9d-8b8a-1f9c1f9c1f9c"},"required":true,"name":"idempotency-key","in":"header"}],"responses":{"204":{"description":"Cleared (or already inactive - idempotent)."},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Idempotency-Key reused with a different request body.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/admin/vat-proofs":{"get":{"tags":["admin"],"summary":"Admin: cursor-paginated VAT proof review queue","description":"Defaults to `pending_review` when no `status` is given. Cursor is the `created_at` of the last item on the previous page.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","enum":["pending_review","approved","rejected"]},"required":false,"name":"status","in":"query"},{"schema":{"type":"string"},"required":false,"name":"cursor","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100},"required":false,"name":"per_page","in":"query"}],"responses":{"200":{"description":"Queue items.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminVatProofListResponse"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/admin/vat-proofs/{id}/approve":{"post":{"tags":["admin"],"summary":"Admin: approve a VAT proof","description":"Sets the proof to `approved`, flips `accounts.vat_status` to `valid`, writes a `manual` source row into `vat_verifications` so subsequent `/v1/vat` calls return immediately without consulting VIES, and emits a `vat_approved` notification.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","format":"uuid","description":"UUID v4. Required on mutating operations that touch money or upstream pbx provisioning. Server replays the original response on conflict.","example":"b0e2f5b1-1f5a-4d9d-8b8a-1f9c1f9c1f9c"},"required":true,"name":"idempotency-key","in":"header"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminVatProofDecisionRequest"}}}},"responses":{"200":{"description":"Decided proof.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminVatProof"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Unknown proof id.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Idempotency-Key reused with a different request body.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/admin/vat-proofs/{id}/reject":{"post":{"tags":["admin"],"summary":"Admin: reject a VAT proof","description":"Sets the proof to `rejected`, flips `accounts.vat_status` to `invalid`, and emits a `vat_rejected` notification. Does NOT write to `vat_verifications`: a rejection on the proof side is about this document specifically, not about the customer's VAT id - we keep VIES as the canonical source for the id itself.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","format":"uuid","description":"UUID v4. Required on mutating operations that touch money or upstream pbx provisioning. Server replays the original response on conflict.","example":"b0e2f5b1-1f5a-4d9d-8b8a-1f9c1f9c1f9c"},"required":true,"name":"idempotency-key","in":"header"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminVatProofDecisionRequest"}}}},"responses":{"200":{"description":"Decided proof.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminVatProof"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Unknown proof id.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Idempotency-Key reused with a different request body.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/admin/vat-proofs/{id}/file":{"get":{"tags":["admin"],"summary":"Admin: stream the proof file from R2","description":"Streams the bytes inline with the original `Content-Type`. Mirrors the customer invoice PDF endpoint pattern.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"File bytes.","content":{"*/*":{"schema":{"type":"string","format":"binary"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Unknown proof id or file missing from R2.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/admin/kyb-intakes":{"get":{"tags":["admin"],"summary":"Admin: cursor-paginated KYB intake review queue","description":"Defaults to `submitted` when no `status` is given. Cursor is the `created_at` of the last item on the previous page.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"allOf":[{"$ref":"#/components/schemas/ComplianceIntakeStatus"},{"description":"Filter by intake status. Defaults to `submitted` when omitted."}]},"required":false,"name":"status","in":"query"},{"schema":{"type":"string","description":"Opaque pagination cursor; the `cursor_next` from the previous page."},"required":false,"name":"cursor","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100,"default":20,"description":"Page size; capped at 100."},"required":false,"name":"per_page","in":"query"}],"responses":{"200":{"description":"Queue items.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminKybIntakeListResponse"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/admin/kyb-intakes/{id}/approve":{"post":{"tags":["admin"],"summary":"Admin: approve a KYB intake","description":"Sets the intake to `approved`, stamps reviewed_at + reviewed_by, and emits a `kyb_approved` notification. Idempotent on the intake id; replaying the request returns the same row.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","format":"uuid","description":"UUID v4. Required on mutating operations that touch money or upstream pbx provisioning. Server replays the original response on conflict.","example":"b0e2f5b1-1f5a-4d9d-8b8a-1f9c1f9c1f9c"},"required":true,"name":"idempotency-key","in":"header"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminKybIntakeDecisionRequest"}}}},"responses":{"200":{"description":"Decided intake.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceIntake"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Unknown intake id.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Idempotency-Key reused with a different request body.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/admin/kyb-intakes/{id}/reject":{"post":{"tags":["admin"],"summary":"Admin: reject a KYB intake","description":"Sets the intake to `rejected`, stamps reviewed_at + reviewed_by, and emits a `kyb_rejected` notification. The customer can open a fresh intake with corrected documents.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","format":"uuid","description":"UUID v4. Required on mutating operations that touch money or upstream pbx provisioning. Server replays the original response on conflict.","example":"b0e2f5b1-1f5a-4d9d-8b8a-1f9c1f9c1f9c"},"required":true,"name":"idempotency-key","in":"header"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminKybIntakeDecisionRequest"}}}},"responses":{"200":{"description":"Decided intake.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceIntake"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Unknown intake id.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Idempotency-Key reused with a different request body.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/admin/kyb-intakes/{id}/documents/{doc_id}/file":{"get":{"tags":["admin"],"summary":"Admin: stream a KYB document file from R2","description":"Streams the bytes inline with the original `Content-Type`. Mirrors the admin VAT proof file endpoint pattern.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"doc_id","in":"path"}],"responses":{"200":{"description":"File bytes.","content":{"*/*":{"schema":{"type":"string","format":"binary"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Unknown intake id, document id, or file missing from R2.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/admin/audit":{"get":{"tags":["admin"],"summary":"Admin: list audit-log events (Wave C1)","description":"Cursor-paginated query over the durable `audit_log` table (7-year retention). Newest-first by `ts`. Filters compose with AND. Returns at most `limit` rows + an opaque `next_cursor` when more rows exist beyond the page.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string"},"required":false,"name":"account_id","in":"query"},{"schema":{"type":"string"},"required":false,"name":"action","in":"query"},{"schema":{"type":"integer","nullable":true,"description":"Inclusive lower bound on `ts` (unix ms)."},"required":false,"name":"since","in":"query"},{"schema":{"type":"integer","nullable":true,"description":"Exclusive upper bound on `ts` (unix ms)."},"required":false,"name":"until","in":"query"},{"schema":{"type":"string"},"required":false,"name":"cursor","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":200,"default":50,"description":"Page size (default 50, max 200)."},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Audit-log page.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuditLogListResponse"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Authenticated but missing the admin role.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/admin/customers":{"get":{"tags":["admin"],"summary":"Admin: list customer accounts (Plan 0037 P1)","description":"Paginated list of all customer accounts. Optional `search` substring matches case-insensitively on email + company_name + full_name. Each row aggregates the most recent active subscription so the SPA can render plan_tier + subscription_status + dunning_state without a follow-up call.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","description":"Case-insensitive substring match on email + company_name + full_name."},"required":false,"name":"search","in":"query"},{"schema":{"type":"string","description":"Opaque cursor (last row created_at) from the previous page."},"required":false,"name":"cursor","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":200,"default":50,"description":"Page size (default 50, max 200)."},"required":false,"name":"per_page","in":"query"}],"responses":{"200":{"description":"Customer page.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminCustomerListResponse"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Admin role required.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/admin/customers/{account_id}":{"get":{"tags":["admin"],"summary":"Admin: customer detail aggregate (Plan 0037 P2)","description":"Single-call hydrate of the customer detail page: account row + active subscription summary (nullable) + verified payment methods + invoice history. Mirrors the shape K's mock returns so the SPA can render without per-field branching.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"account_id","in":"path"}],"responses":{"200":{"description":"Customer detail.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminCustomerDetailResponse"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Admin role required.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Unknown customer.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/admin/customers/{account_id}/subscription":{"get":{"tags":["admin"],"summary":"Admin: customer subscription summary (Plan 0037 P3)","description":"Mirror of GET /v1/subscription but reads the target account_id from the URL path, not the bearer's JWT. Returns the same shape K's mock exposes.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"account_id","in":"path"}],"responses":{"200":{"description":"Subscription view.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminCustomerSubscriptionView"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Admin role required.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Unknown customer or no subscription on this account.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/admin/customers/{account_id}/subscription/reactivate":{"post":{"tags":["admin"],"summary":"Admin: reactivate a customer's cancel_at_period_end subscription","description":"Mirror of POST /v1/subscription/reactivate. Clears cancel_at_period_end and rewrites status='active'. NO money moves. Audit row records actor='admin' with the target account_id in meta.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"account_id","in":"path"},{"schema":{"type":"string","format":"uuid","description":"UUID v4. Required on mutating operations that touch money or upstream pbx provisioning. Server replays the original response on conflict.","example":"b0e2f5b1-1f5a-4d9d-8b8a-1f9c1f9c1f9c"},"required":true,"name":"idempotency-key","in":"header"}],"responses":{"200":{"description":"Subscription reactivated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminCustomerSubscriptionView"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Admin role required.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Unknown customer or no subscription on this account.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Subscription is not in cancel_at_period_end state.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/admin/customers/{account_id}/payment-methods":{"get":{"tags":["admin"],"summary":"Admin: list saved payment methods for a customer (Plan 0037 P4)","description":"Mirror of GET /v1/payment-methods. Returns only verified rows (pending / failed verifications are filtered out, same as the customer surface).","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"account_id","in":"path"}],"responses":{"200":{"description":"Payment methods.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminCustomerPaymentMethodsResponse"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Admin role required.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Unknown customer.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}},"post":{"tags":["admin"],"summary":"Admin: begin adding a payment method on behalf of a customer (Plan 0037 P4)","description":"Mirror of POST /v1/payment-methods. Returns a Revolut hosted-checkout URL the operator can send to the customer (or use during a co-managed session). Raw card data never touches this API.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"account_id","in":"path"},{"schema":{"type":"string","format":"uuid","description":"UUID v4. Required on mutating operations that touch money or upstream pbx provisioning. Server replays the original response on conflict.","example":"b0e2f5b1-1f5a-4d9d-8b8a-1f9c1f9c1f9c"},"required":true,"name":"idempotency-key","in":"header"}],"responses":{"200":{"description":"Hosted-flow URL returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminAddPaymentMethodResponse"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Admin role required.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Unknown customer.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"502":{"description":"Tokenisation failed at upstream.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/admin/customers/{account_id}/account":{"patch":{"tags":["admin"],"summary":"Admin: update customer company fields + VAT id (Plan 0037 P5)","description":"Mirror of PATCH /v1/account. On a vat_id change the handler consults the 30-day VIES cache, falls back to a live SOAP lookup, and persists the result. VIES outage is fail-OPEN: vat_status flips to 'pending_review' and the PATCH still succeeds. Address fields are not handled here (use the customer-scoped address endpoints).","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"account_id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminUpdateCustomerAccountRequest"}}}},"responses":{"200":{"description":"Updated account projection.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminCustomerAccount"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Admin role required.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Unknown customer.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"422":{"description":"VAT id failed VIES validation (registry says invalid).","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/admin/support/tickets":{"get":{"tags":["admin","support"],"summary":"Admin: list support tickets (Plan 0055)","description":"Paginated queue of all support tickets across all accounts, newest-activity first. Optional `status` query filters to one of the public statuses. Cursor pagination: `cursor` is the previous page's last `last_activity_at` epoch.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"$ref":"#/components/schemas/SupportTicketStatus"},"required":false,"name":"status","in":"query"},{"schema":{"type":"string"},"required":false,"name":"cursor","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100,"default":25},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Ticket queue page.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminSupportTicketListResponse"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Admin role required.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/admin/support/tickets/{ticket_id}":{"get":{"tags":["admin","support"],"summary":"Admin: get a support ticket (Plan 0055)","description":"Returns the ticket row + its chronological message thread + the owning customer email so the admin SPA can render the detail page without a second round-trip against /admin/customers.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"ticket_id","in":"path"}],"responses":{"200":{"description":"Ticket detail.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminSupportTicketDetail"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Admin role required.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Unknown ticket.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/admin/support/tickets/{ticket_id}/reply":{"post":{"tags":["admin","support"],"summary":"Admin: reply to a support ticket (Plan 0055)","description":"Appends an operator reply to the ticket. Flips ticket status to `awaiting_customer` and bumps `last_activity_at`. Best-effort emails the customer with the reply body. Idempotency-Key required.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"ticket_id","in":"path"},{"schema":{"type":"string","format":"uuid","description":"UUID v4. Required on mutating operations that touch money or upstream pbx provisioning. Server replays the original response on conflict.","example":"b0e2f5b1-1f5a-4d9d-8b8a-1f9c1f9c1f9c"},"required":true,"name":"idempotency-key","in":"header"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminSupportTicketReplyRequest"}}}},"responses":{"201":{"description":"Reply appended; updated ticket detail returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminSupportTicketDetail"}}}},"400":{"description":"Idempotency-Key missing.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Admin role required.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Unknown ticket.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Idempotency-Key reused with a different request body.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/admin/support/tickets/{ticket_id}/close":{"post":{"tags":["admin","support"],"summary":"Admin: close a support ticket (Plan 0067)","description":"Flips ticket status to `closed` and bumps `last_activity_at`. Idempotent: re-issuing the request on an already-closed ticket is a no-op.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"ticket_id","in":"path"}],"responses":{"200":{"description":"Ticket closed; updated detail returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminSupportTicketDetail"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Admin role required.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Unknown ticket.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/admin/support/tickets/{ticket_id}/reopen":{"post":{"tags":["admin","support"],"summary":"Admin: reopen a closed or duplicate ticket (Plan 0067)","description":"Flips ticket status back to `open`, clears `duplicate_of_ticket_id`, bumps `last_activity_at`. Idempotent.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"ticket_id","in":"path"}],"responses":{"200":{"description":"Ticket reopened; updated detail returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminSupportTicketDetail"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Admin role required.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Unknown ticket.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/admin/support/tickets/{ticket_id}/merge-into/{target_ticket_id}":{"post":{"tags":["admin","support"],"summary":"Admin: mark ticket as duplicate of another (Plan 0067)","description":"Marks the source ticket as `duplicate` and records the target id in `duplicate_of_ticket_id`. Returns 400 if the target equals the source. Returns 404 if either ticket is missing.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"ticket_id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"target_ticket_id","in":"path"}],"responses":{"200":{"description":"Ticket merged; updated detail returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminSupportTicketDetail"}}}},"400":{"description":"Self-merge rejected.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Admin role required.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Unknown source or target ticket.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/admin/support/reply-templates":{"get":{"tags":["admin","support"],"summary":"Admin: list canned reply templates (Plan 0067)","description":"Returns the static list of curated reply templates rendered in the admin reply box dropdown. Hardcoded today; a future templating service can replace this without an SPA change.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Reply templates.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminSupportReplyTemplateList"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Admin role required.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/admin/invoices":{"get":{"tags":["admin"],"summary":"Admin: cross-customer invoice list (Plan 0042 U)","description":"Cursor-paginated list of invoices across every account. Multi-select on `status` collapses the internal vocabulary into open / paid / void / overdue. `overdue` is computed at read time (open + due_at < now). Cursor is opaque (base64url).","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"anyOf":[{"$ref":"#/components/schemas/AdminInvoiceListStatus"},{"type":"array","items":{"$ref":"#/components/schemas/AdminInvoiceListStatus"}}],"description":"One or more invoice statuses. Repeat the query parameter to multi-select. `overdue` is computed at read time (open + due_at < now)."},"required":false,"name":"status","in":"query"},{"schema":{"type":"integer","nullable":true,"description":"Inclusive lower bound on issued_at (unix seconds)."},"required":false,"name":"from","in":"query"},{"schema":{"type":"integer","nullable":true,"description":"Inclusive upper bound on issued_at (unix seconds)."},"required":false,"name":"to","in":"query"},{"schema":{"type":"string","description":"Opaque cursor returned by a prior page; base64url-encoded."},"required":false,"name":"cursor","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100,"default":50,"description":"Page size (default 50, max 100)."},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Invoice page.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminInvoiceListResponse"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Admin role required.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/admin/payments":{"get":{"tags":["admin"],"summary":"Admin: cross-customer payment list (Plan 0042 U)","description":"Cursor-paginated list of payments across every account. UNION of `topups` + `billing_cycles`; statuses are normalized into the public enum (pending|completed|failed|refunded). Cursor is opaque (base64url).","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"anyOf":[{"$ref":"#/components/schemas/AdminPaymentStatus"},{"type":"array","items":{"$ref":"#/components/schemas/AdminPaymentStatus"}}],"description":"One or more payment statuses; repeat to multi-select."},"required":false,"name":"status","in":"query"},{"schema":{"anyOf":[{"$ref":"#/components/schemas/AdminPaymentKind"},{"type":"array","items":{"$ref":"#/components/schemas/AdminPaymentKind"}}],"description":"One or more payment kinds; repeat to multi-select."},"required":false,"name":"kind","in":"query"},{"schema":{"type":"integer","nullable":true},"required":false,"name":"from","in":"query"},{"schema":{"type":"integer","nullable":true},"required":false,"name":"to","in":"query"},{"schema":{"type":"string"},"required":false,"name":"cursor","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Payment page.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminPaymentListResponse"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Admin role required.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/admin/dashboard/operational":{"get":{"tags":["admin"],"summary":"Admin: operational dashboard (Wave D1)","description":"Aggregates structured-error + audit-event signals from the CF Analytics Engine datasets (`pbx_billing_portal_api_<env>` + `pbx_billing_portal_api_audit_<env>`) into a single SPA-friendly payload. Range defaults to last 24h with hourly buckets. Results are cached in-isolate for 60 seconds keyed on (since,until,granularity). Latency percentiles are null in v1 (CF Workers do not emit latency to AE by default). requests section is a placeholder until per-request sampling is wired.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"integer","nullable":true,"description":"Inclusive lower bound on ts (unix ms). Defaults to until - 24h."},"required":false,"name":"since","in":"query"},{"schema":{"type":"integer","nullable":true,"description":"Exclusive upper bound on ts (unix ms). Defaults to now."},"required":false,"name":"until","in":"query"},{"schema":{"type":"string","enum":["hour","day"],"default":"hour","description":"Time-series bucket size for ts_buckets."},"required":false,"name":"granularity","in":"query"}],"responses":{"200":{"description":"Operational dashboard payload.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OperationalDashboardResponse"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Authenticated but missing the admin role (2FA-gated).","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"502":{"description":"Upstream Analytics Engine SQL API call failed.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/admin/dashboard/security":{"get":{"tags":["admin"],"summary":"Admin: security-posture dashboard (Wave D2)","description":"Aggregates admin 2FA enrolment, recent failed sign-ins, magic-link usage, and JWT rotation state from D1. No Analytics Engine reads here. Hot accounts = >5 failed sign-ins in the last hour. jwt_rotation.rotation_active_since is always null in v1; the marker is not persisted.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Security-posture payload.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SecurityDashboardResponse"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Authenticated but missing the admin role (2FA-gated).","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/admin/dashboard/billing":{"get":{"tags":["admin"],"summary":"Admin: billing dashboard (Wave D3)","description":"Subscription distribution, scheduler health, revenue time-series, top-revenue accounts, and dunning-state counts over the selected range. Range = 24h | 7d | 30d (default) | 90d. In-isolate 60s cache.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","enum":["24h","7d","30d","90d"],"default":"30d","description":"Aggregation window relative to now."},"required":false,"name":"range","in":"query"}],"responses":{"200":{"description":"Billing dashboard payload.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BillingDashboardResponse"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Authenticated but missing the admin role (2FA-gated).","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/admin/dashboard/compliance":{"get":{"tags":["admin"],"summary":"Admin: compliance dashboard (Wave D3)","description":"Pending GDPR deletion requests + KYB intakes + VAT-pending accounts. All read from D1; no AE involvement.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Compliance dashboard payload.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceDashboardResponse"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Authenticated but missing the admin role (2FA-gated).","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/admin/2fa/enroll":{"post":{"tags":["admin"],"summary":"Admin: enrol in TOTP 2FA","description":"Generates a fresh TOTP secret + QR URI + 10 plaintext recovery codes. The DB stores SHA-256 hashes of the recovery codes; the plaintext is returned ONCE in this response. The operator MUST save them out-of-band before the response is discarded.","security":[{"bearerAuth":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Admin2faEnrollBody"}}}},"responses":{"200":{"description":"Enrolment complete.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Admin2faEnrollResponse"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Authenticated but missing admin role.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Admin already enrolled in TOTP 2FA.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/admin/2fa/verify":{"post":{"tags":["admin"],"summary":"Admin: complete sign-in by verifying a TOTP code","description":"Consumes the 2fa-pending tmp_token issued by /v1/auth/sign-in. Verifies the 6-digit TOTP code against the stored secret with +/- 1 step (30s) tolerance. On success, mints the real session (access JWT with roles=admin + refresh token).","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Admin2faVerifyBody"}}}},"responses":{"200":{"description":"Session minted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Admin2faSessionResponse"}}}},"401":{"description":"tmp_token invalid / expired, or TOTP code wrong.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Admin is not enrolled.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/admin/2fa/recovery-code":{"post":{"tags":["admin"],"summary":"Admin: complete sign-in by consuming a recovery code","description":"Consumes the 2fa-pending tmp_token plus one recovery code. The code is hashed (SHA-256) + compared constant-time against the active list; on match it is moved into the consumed list. If this consume empties the active set, the enrolment row is deleted to force re-enrolment on the next sign-in (`re_enrol_required=true` in the response).","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Admin2faRecoveryBody"}}}},"responses":{"200":{"description":"Session minted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Admin2faSessionResponse"}}}},"401":{"description":"tmp_token invalid / expired, or recovery code wrong.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Admin is not enrolled.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/admin/2fa":{"delete":{"tags":["admin"],"summary":"Admin: delete TOTP 2FA enrolment (testing escape hatch)","description":"Deletes the admin_2fa row for the authenticated admin. Used during integration tests + as a self-service recovery path when the operator loses both their TOTP device and all recovery codes (still requires a valid bearer token, so it does not bypass the gate when the operator is genuinely locked out - that path is operator-pair-work).","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Enrolment removed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Admin2faDeleteResponse"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Authenticated but missing admin role.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Admin is not enrolled.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/me/2fa/status":{"get":{"tags":["auth (customer)"],"summary":"Customer: check whether the authenticated account is enrolled in TOTP 2FA","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Enrolment status.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Me2faStatusResponse"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/me/2fa/enroll/start":{"post":{"tags":["auth (customer)"],"summary":"Customer: begin TOTP 2FA enrolment (returns QR + enroll_token)","description":"Mints a fresh TOTP secret + QR URI + a short-lived (5min) enroll_token carrying the candidate secret. The SPA shows the QR to the user, collects the first 6-digit code, then POSTs to /v1/me/2fa/enroll/verify with the enroll_token + code to commit the enrolment. No DB row is written until verify succeeds, so abandoned flows have zero cleanup cost.","security":[{"bearerAuth":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Me2faEnrollStartBody"}}}},"responses":{"200":{"description":"Enrolment started.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Me2faEnrollStartResponse"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Account is already enrolled in TOTP 2FA.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/me/2fa/enroll/verify":{"post":{"tags":["auth (customer)"],"summary":"Customer: confirm TOTP 2FA enrolment with the first 6-digit code","description":"Consumes the enroll_token (verifies the secret-binding) + the 6-digit TOTP code. On success, persists the secret + 10 SHA-256 recovery-code hashes and returns the 10 plaintext recovery codes (shown ONCE). The DB does NOT store the plaintext; the user must save them out-of-band.","security":[{"bearerAuth":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Me2faEnrollVerifyBody"}}}},"responses":{"200":{"description":"Enrolment committed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Me2faEnrollVerifyResponse"}}}},"401":{"description":"Unauthenticated, enroll_token invalid, or TOTP code wrong.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Account is already enrolled (raced with another tab).","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/me/2fa/disable":{"post":{"tags":["auth (customer)"],"summary":"Customer: disable TOTP 2FA (requires a current TOTP code)","description":"Deletes the account_2fa row for the authenticated user. Requires a fresh 6-digit TOTP code so a stolen session token alone cannot drop the second factor.","security":[{"bearerAuth":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Me2faDisableBody"}}}},"responses":{"200":{"description":"Enrolment removed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Me2faDisableResponse"}}}},"401":{"description":"Unauthenticated or TOTP code wrong.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Account is not enrolled.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/me/2fa/sign-in/verify":{"post":{"tags":["auth (customer)"],"summary":"Customer: complete sign-in by verifying a TOTP code","description":"Consumes the 2fa-pending tmp_token issued by /v1/auth/magic-link/verify (or /v1/auth/first-login when the user is enrolled). Verifies the 6-digit TOTP code against the stored secret with +/- 1 step tolerance. On success, mints the real session pair.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Me2faSignInVerifyBody"}}}},"responses":{"200":{"description":"Session minted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Me2faSignInSessionResponse"}}}},"401":{"description":"tmp_token invalid / expired, or TOTP code wrong.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Account is not enrolled.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/me/2fa/sign-in/recovery-code":{"post":{"tags":["auth (customer)"],"summary":"Customer: complete sign-in by consuming a recovery code","description":"Consumes the tmp_token + one recovery code. The code is hashed (SHA-256) + compared constant-time against the active list; on match it is moved to the consumed list. If the consume empties the active set, the enrolment row is deleted and `re_enrol_required:true` is returned.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Me2faSignInRecoveryBody"}}}},"responses":{"200":{"description":"Session minted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Me2faSignInSessionResponse"}}}},"401":{"description":"tmp_token invalid / expired, or recovery code wrong.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Account is not enrolled.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/me/export":{"get":{"tags":["me-data"],"summary":"GDPR Art. 20: data-portability bundle for the authenticated account","description":"Returns one JSON object containing every record the portal-api stores about the authenticated account. Includes the account row, all user rows, addresses, subscriptions, invoices (with auth-gated download paths), payment-methods (Revolut tokens stripped), topups, KYB intakes (with auth-gated document paths), VAT verification + proofs, banners, notifications, audit_log (last 90 days, capped at 10000 rows), and magic-link + refresh-token history (token hashes stripped). Audits the export.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Bundle.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeExportBundle"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/me/delete-request":{"post":{"tags":["me-data"],"summary":"GDPR Art. 17: request account deletion (step 1 - email confirmation)","description":"Records a pending deletion request and emails the authenticated user a magic link (1h TTL) to confirm. The customer-facing email lists what is deleted vs retained for legal reasons (LT accounting law mandates 10y retention on invoices, payments, subscriptions per GDPR Art. 17(3)(b)+(e) exemption). Confirm with POST /v1/me/delete-confirm. Cancel with POST /v1/me/delete-cancel before the link is consumed.","security":[{"bearerAuth":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeDeleteRequestBody"}}}},"responses":{"202":{"description":"Pending request recorded; confirmation email queued.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeDeleteRequestResponse"}}}},"400":{"description":"confirm_email does not match the authenticated user.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"A pending deletion request already exists.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/me/delete-confirm":{"post":{"tags":["me-data"],"summary":"GDPR Art. 17: confirm account deletion via the emailed magic link","description":"Consumes a magic-link token (purpose=delete_confirm) and atomically executes the deletion per the strategy in docs/runbooks/gdpr-deletion.md. Anonymizes the user row, redacts the account row, keeps invoices/payments/subscriptions (10y LT retention), deletes the KYB R2 PDFs, hard-deletes magic-link + refresh-token + banner + notification rows, and revokes all sessions. The audit-log entry of the deletion itself is kept.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeDeleteConfirmBody"}}}},"responses":{"200":{"description":"Account deletion executed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeDeleteConfirmResponse"}}}},"401":{"description":"Magic-link token invalid, expired, consumed, or wrong purpose.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"No pending deletion request for this link.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/me/delete-cancel":{"post":{"tags":["me-data"],"summary":"Cancel a pending account-deletion request","description":"Marks the latest pending deletion_request for the authenticated account as cancelled. No-op if none is pending. The associated magic link, if not yet consumed, will simply expire on its 1h TTL.","security":[{"bearerAuth":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeDeleteCancelBody"}}}},"responses":{"200":{"description":"Pending request cancelled.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeDeleteCancelResponse"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"No pending request to cancel.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/vat":{"get":{"tags":["vat"],"summary":"Get the VAT verification status for the signed-in account","description":"Consults the EU VIES service for VAT validation, with a 30-day cache. On VIES outage falls back to the last-known-good cached result; if there is no cache row at all, returns `{ status: \"pending\" }`.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"VAT status snapshot.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VatStatusResponse"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Account row not found for this token.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/vat/proof":{"post":{"tags":["vat"],"summary":"Upload a VAT proof document (multipart)","description":"Accepts a PDF or image (jpg/png/webp) up to 10MB. Stored in R2 under `vat-proofs/{account_id}/{ulid}.{ext}`; a `vat_proofs` row is created in `pending_review` for the admin review queue. The customer receives an in-app notification (`vat_proof_received`) immediately and a follow-up notification (`vat_approved` or `vat_rejected`) once an admin has reviewed.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"proof_file":{"type":"string","format":"binary"},"note":{"type":"string","maxLength":500}},"required":["proof_file"]}}}},"responses":{"202":{"description":"Document received; verification will follow.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VatProofResponse"}}}},"400":{"description":"Required `proof_file` field is missing.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"413":{"description":"Uploaded file exceeds 10MB.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"415":{"description":"Content type not in {pdf, jpg, png, webp}.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/compliance/intakes":{"get":{"tags":["compliance"],"summary":"List KYB compliance intakes for the signed-in account","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"List of intakes (newest first).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceIntakeList"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}},"post":{"tags":["compliance"],"summary":"Open a new KYB compliance intake","description":"Creates an intake in `open` status with the default required-docs checklist (`articles_of_incorporation`, `beneficial_ownership_declaration`, `proof_of_address`). The customer uploads documents one-by-one and then calls `POST /compliance/intakes/{id}/submit`.","security":[{"bearerAuth":[]}],"responses":{"201":{"description":"Created intake.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceIntake"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/compliance/intakes/{id}":{"get":{"tags":["compliance"],"summary":"Get one KYB compliance intake","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Intake row.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceIntake"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Unknown intake id (or intake belongs to another account).","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/compliance/intakes/{id}/documents":{"post":{"tags":["compliance"],"summary":"Upload a KYB document against an intake (multipart)","description":"Multipart fields: `doc_kind` (string, must be one of the intake's `required_docs`) and `file` (PDF or jpg/png/webp, up to 10MB). Stored in R2 under `kyb-documents/{account_id}/{intake_id}/{doc_id}.{ext}`. Inserts a `kyb_intake_documents` row and appends the doc kind to the intake's `uploaded_docs` list.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"doc_kind":{"type":"string"},"file":{"type":"string","format":"binary"}},"required":["doc_kind","file"]}}}},"responses":{"201":{"description":"Document stored; returns the new document row + the updated intake.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceDocumentUploadResponse"}}}},"400":{"description":"Required field missing or doc_kind not in the intake checklist.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Intake does not belong to the signed-in account.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Unknown intake id.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Intake is already submitted / approved / rejected.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"413":{"description":"Uploaded file exceeds 10MB.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"415":{"description":"Content type not in {pdf, jpg, png, webp}.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/compliance/intakes/{id}/submit":{"post":{"tags":["compliance"],"summary":"Submit a KYB intake for admin review","description":"Transitions the intake from `open` to `submitted`. Requires every doc in `required_docs` to appear in `uploaded_docs`. Emits a `kyb_submitted` notification so the customer sees an in-app ack while admins are reviewing.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Submitted intake row.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceIntake"}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Intake does not belong to the signed-in account.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Unknown intake id.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Intake is not in `open` status, or required docs are missing.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/me/compliance/status":{"get":{"tags":["me-compliance"],"summary":"Get combined VAT + KYB compliance status for the signed-in account","description":"Read-only projection over `accounts.vat_status`, `vat_proofs`, and `kyb_intakes`. Returns the current compliance picture without exposing admin-only details. All data is scoped to the authenticated account (multi-tenant safe).","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Combined compliance status.","content":{"application/json":{"schema":{"type":"object","properties":{"vat":{"type":"object","properties":{"status":{"type":"string","enum":["approved","pending","rejected","none","reverse_charge"]},"vat_number":{"type":"string","nullable":true},"verified_at":{"type":"number","nullable":true},"verification_source":{"type":"string","nullable":true,"enum":["vies","manual_proof"]},"pending_proof":{"type":"object","nullable":true,"properties":{"id":{"type":"string"},"uploaded_at":{"type":"number"},"rejected_reason":{"type":"string","nullable":true}},"required":["id","uploaded_at","rejected_reason"]}},"required":["status","vat_number","verified_at","verification_source","pending_proof"]},"kyb":{"type":"object","properties":{"status":{"type":"string","enum":["not_started","in_progress","submitted","approved","rejected"]},"required_docs":{"type":"array","items":{"type":"string"}},"submitted_docs":{"type":"array","items":{"type":"string"}},"submitted_at":{"type":"number","nullable":true},"decided_at":{"type":"number","nullable":true},"rejected_reason":{"type":"string","nullable":true}},"required":["status","required_docs","submitted_docs","submitted_at","decided_at","rejected_reason"]}},"required":["vat","kyb"]}}}},"401":{"description":"Unauthenticated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}}}}