{"openapi":"3.1.0","info":{"title":"Darb Public API","version":"1.0.0"},"servers":[{"url":"https://api.darbhq.com/api/v1"}],"paths":{"/routes":{"post":{"tags":["routes"],"summary":"Create Route","description":"Reserve quota, persist + enqueue, and return 202 + the poll seed (D-06/D-07/D-16).\n\n``Idempotency-Key`` is OPTIONAL and additive (D-08): a MISSING header keeps the 16-02 create\nbyte-identical (``_plain_create``, ``commit=True``, no claim). A PRESENT header routes through\nthe SHARED idempotency helper — ``ensure_key_length`` caps it (T-17-13), ``compute_fingerprint``\ncanonicalizes the body (D-07), and ``begin`` decides: a REPLAY loads the ORIGINAL route by its\nclaim's ``resource_id`` and echoes it (202 + ``Idempotent-Replay: true``) reserving ZERO new\nquota (D-16a), a swapped body raises 409 ``idempotency_key_conflict`` (D-07), and a CLAIM\nproceeds to ``_claim_create`` (reserve → create → single atomic commit → enqueue). Routes remain\nNON paid-gated (D-08/D-12) — the header adds durability, not a plan gate.","operationId":"create_route_routes_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Idempotency-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RouteCreate"}}}},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RouteResource"}}}},"401":{"description":"Missing or invalid API key (`unauthorized`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Refused (`test_mode_unavailable` / `email_unverified` / `paid_plan_required` / `live_key_browser_forbidden`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Request failed validation (`validation_error`; field errors under `details.errors`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limit exceeded (`rate_limited`; honest `Retry-After` + `X-RateLimit-*` headers).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Unexpected server error (`internal_error`; no internals leak).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"503":{"description":"Rate-limit backend unavailable (`service_unavailable`; retry after `Retry-After`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Subscription, quota or credit refusal (`quota_exceeded` / `credits_exhausted` / `overage_cap_reached` / `subscription_canceled` / `subscription_locked`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Unknown or foreign resource id (`not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Conflict (`idempotency_key_conflict` — a reused Idempotency-Key with a different body; `webhook_limit_reached` — the per-tenant webhook subscription cap).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/routes/{route_id}":{"get":{"tags":["routes"],"summary":"Get Route","description":"Poll a tenant-owned route; embed the lazy driver hand-off once optimized (D-06/D-08).\n\nLoads the route scoped to ``principal.tenant_id`` AND mode-filtered to the key's mode\n(``is_test=(principal.mode == KeyMode.test)``, D-14) — an unknown, foreign, OR cross-mode id\nrenders the uniform ``not_found`` 404 (no 403-vs-404 oracle, T-16-06). Once the route is done it\nget-or-creates the driver hand-off token via ``ensure_share_token`` (idempotent — two polls\nyield the SAME ``/d/{token}`` link, D-08) and embeds the absolute URL from the configured SPA.","operationId":"get_route_routes__route_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"route_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Route Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RouteResource"}}}},"401":{"description":"Missing or invalid API key (`unauthorized`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Refused (`test_mode_unavailable` / `email_unverified` / `paid_plan_required` / `live_key_browser_forbidden`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Request failed validation (`validation_error`; field errors under `details.errors`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limit exceeded (`rate_limited`; honest `Retry-After` + `X-RateLimit-*` headers).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Unexpected server error (`internal_error`; no internals leak).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"503":{"description":"Rate-limit backend unavailable (`service_unavailable`; retry after `Retry-After`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Unknown or foreign resource id (`not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/usage":{"get":{"tags":["usage"],"summary":"Read Usage","description":"Return the caller tenant's current-period usage + limits + subscription state (D-18/D-05).\n\nFunnels the SAME two dashboard reads: ``get_usage`` yields ``(period_label, [(metric, used,\nlimit), ...])`` for routes/whatsapp/sms, and ``get_subscription`` yields the tenant's\nsubscription + plan key (its ``status`` / ``current_period_end`` / ``cancel_at_period_end`` are\nthe D-18 subscription state). Both scope strictly to ``principal.tenant_id`` (T-16-11) and each\nreturns ``None`` for a no-subscription tenant — handled as the null/zeroed shape below. The read\nis serialized through the public ``UsageResource`` (D-04); no internal cost fields cross over.\n\nUnder a ``dk_test_`` key (``principal.mode == KeyMode.test``, 19-07 / D-05) the metric usage +\nthe ``period`` label instead come from the TEST namespace via ``get_test_usage`` (the\n``test:{period}`` counters against the named test caps) and ``livemode`` is ``False`` — the live\ncounter rows are byte-untouched. The subscription-state fields are unchanged (account state is\northogonal to test mode). A live key keeps the byte-stable D-18 shape with ``livemode`` true.","operationId":"read_usage_usage_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UsageResource"}}}},"401":{"description":"Missing or invalid API key (`unauthorized`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Refused (`test_mode_unavailable` / `email_unverified` / `paid_plan_required` / `live_key_browser_forbidden`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limit exceeded (`rate_limited`; honest `Retry-After` + `X-RateLimit-*` headers).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Unexpected server error (`internal_error`; no internals leak).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"503":{"description":"Rate-limit backend unavailable (`service_unavailable`; retry after `Retry-After`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"security":[{"HTTPBearer":[]}]}},"/campaigns":{"post":{"tags":["campaigns"],"summary":"Create Campaign","description":"Paid-gate, reserve, persist + enqueue, and return 202 + the poll seed (API-03/API-08/API-09).\n\nThe ORDER is load-bearing: (1) the PAID GATE FIRST, but ONLY for a LIVE key (D-18) — a LIVE\n``trialing`` / no-sub tenant is refused 403 ``paid_plan_required`` before any reserve/claim/send\n(D-10); a ``dk_test_`` key SKIPS the gate so test-mode creation is open to trialing tenants.\n(2) ``Idempotency-Key`` is OPTIONAL and additive (D-08): a MISSING header runs ``_plain_create``\n(``commit=True``, no claim); a PRESENT header routes through the SHARED idempotency helper —\n``ensure_key_length`` caps it (T-17-13), ``compute_fingerprint`` canonicalizes the body (D-07),\nand ``begin`` decides: a REPLAY loads the ORIGINAL campaign by its claim's ``resource_id`` and\nechoes it (202 + ``Idempotent-Replay: true``) reserving ZERO new quota and enqueuing NO send\n(D-16a), a swapped body raises 409 ``idempotency_key_conflict`` (D-07), and a CLAIM proceeds to\n``_claim_create`` (READ pre-gate → create → single atomic commit → enqueue). ``canceled`` /\n``locked`` surface their existing 402 via ``check_quota``; an invalid phone → 422 (D-14).","operationId":"create_campaign_campaigns_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Idempotency-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampaignCreate"}}}},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampaignResource"}}}},"401":{"description":"Missing or invalid API key (`unauthorized`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Refused (`test_mode_unavailable` / `email_unverified` / `paid_plan_required` / `live_key_browser_forbidden`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Request failed validation (`validation_error`; field errors under `details.errors`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limit exceeded (`rate_limited`; honest `Retry-After` + `X-RateLimit-*` headers).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Unexpected server error (`internal_error`; no internals leak).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"503":{"description":"Rate-limit backend unavailable (`service_unavailable`; retry after `Retry-After`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Subscription, quota or credit refusal (`quota_exceeded` / `credits_exhausted` / `overage_cap_reached` / `subscription_canceled` / `subscription_locked`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Unknown or foreign resource id (`not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Conflict (`idempotency_key_conflict` — a reused Idempotency-Key with a different body; `webhook_limit_reached` — the per-tenant webhook subscription cap).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/campaigns/{campaign_id}":{"get":{"tags":["campaigns"],"summary":"Get Campaign","description":"Poll a tenant-owned campaign — contact states + coords as they arrive + honest purged state.\n\nLoads the campaign scoped to ``principal.tenant_id`` AND mode-filtered to the key's mode\n(``is_test=(principal.mode == KeyMode.test)``, D-14; ``selectinload`` contacts) — an unknown,\nforeign, OR cross-mode id renders the uniform ``not_found`` 404 (no 403-vs-404 oracle, T-17-20).\nOtherwise it serializes through ``to_campaign_resource``: contact ``state`` + lat/lng where a\nlocation has arrived, ``route_id`` once auto-optimize links the route, and — once ``purged_at``\nis set — an HONEST scrubbed resource (lat/lng/phone forced null) that is NEVER a 404 and never\nresurrects PII through the API (D-13/D-16e).","operationId":"get_campaign_campaigns__campaign_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"campaign_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Campaign Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampaignResource"}}}},"401":{"description":"Missing or invalid API key (`unauthorized`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Refused (`test_mode_unavailable` / `email_unverified` / `paid_plan_required` / `live_key_browser_forbidden`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Request failed validation (`validation_error`; field errors under `details.errors`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limit exceeded (`rate_limited`; honest `Retry-After` + `X-RateLimit-*` headers).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Unexpected server error (`internal_error`; no internals leak).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"503":{"description":"Rate-limit backend unavailable (`service_unavailable`; retry after `Retry-After`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Unknown or foreign resource id (`not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/webhooks":{"get":{"tags":["webhooks"],"summary":"List Webhooks","description":"List the caller's subscriptions, newest-first — tenant-scoped; NO secret crosses (D-13).","operationId":"list_webhooks_webhooks_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/WebhookSubscriptionResource"},"type":"array","title":"Response List Webhooks Webhooks Get"}}}},"401":{"description":"Missing or invalid API key (`unauthorized`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Refused (`test_mode_unavailable` / `email_unverified` / `paid_plan_required` / `live_key_browser_forbidden`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Request failed validation (`validation_error`; field errors under `details.errors`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limit exceeded (`rate_limited`; honest `Retry-After` + `X-RateLimit-*` headers).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Unexpected server error (`internal_error`; no internals leak).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"503":{"description":"Rate-limit backend unavailable (`service_unavailable`; retry after `Retry-After`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"security":[{"HTTPBearer":[]}]},"post":{"tags":["webhooks"],"summary":"Create Webhook","description":"Register a subscription and return the resource + the signing secret exactly once (WHK-01).\n\nDelegates to ``create_subscription`` (registration-time https-only + SSRF validation, the D-19\ncap over active rows, a fresh ``whsec_`` secret Fernet-encrypted at rest). A refused URL →\n422 ``validation_error``; the cap → 409 ``webhook_limit_reached``. No paid gate / Idempotency\n-Key / quota draw (D-18 — CONFIG not spend). The raw secret rides the response ONCE (D-13).\n\nThe subscription mode is DERIVED FROM THE KEY (D-15): ``key_mode = WebhookMode(principal.mode\n.value)`` — a ``dk_test_`` key registers a ``mode=test`` subscription, a live key a live one, so\na test key can never register a live subscription. ``body.mode`` is OPTIONAL: omitted → the key\nmode; present-and-contradictory → the ``validation_error`` envelope (the key always wins).","operationId":"create_webhook_webhooks_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookSubscriptionCreate"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookSubscriptionCreateResource"}}}},"401":{"description":"Missing or invalid API key (`unauthorized`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Refused (`test_mode_unavailable` / `email_unverified` / `paid_plan_required` / `live_key_browser_forbidden`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Request failed validation (`validation_error`; field errors under `details.errors`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limit exceeded (`rate_limited`; honest `Retry-After` + `X-RateLimit-*` headers).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Unexpected server error (`internal_error`; no internals leak).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"503":{"description":"Rate-limit backend unavailable (`service_unavailable`; retry after `Retry-After`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Conflict (`idempotency_key_conflict` — a reused Idempotency-Key with a different body; `webhook_limit_reached` — the per-tenant webhook subscription cap).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"security":[{"HTTPBearer":[]}]}},"/webhooks/{subscription_id}":{"get":{"tags":["webhooks"],"summary":"Get Webhook","description":"Read one tenant-owned subscription; an unknown/foreign id is the uniform not_found.","operationId":"get_webhook_webhooks__subscription_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"subscription_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Subscription Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookSubscriptionResource"}}}},"401":{"description":"Missing or invalid API key (`unauthorized`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Refused (`test_mode_unavailable` / `email_unverified` / `paid_plan_required` / `live_key_browser_forbidden`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Request failed validation (`validation_error`; field errors under `details.errors`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limit exceeded (`rate_limited`; honest `Retry-After` + `X-RateLimit-*` headers).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Unexpected server error (`internal_error`; no internals leak).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"503":{"description":"Rate-limit backend unavailable (`service_unavailable`; retry after `Retry-After`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Unknown or foreign resource id (`not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}},"patch":{"tags":["webhooks"],"summary":"Update Webhook","description":"Update a subscription's URL and/or events; the URL is re-validated (registration SSRF, D-11).\n\nA present ``url`` re-runs the https-only + SSRF guard (a rebind to an internal target is refused\non update too) → 422 ``validation_error`` on refusal. A foreign/unknown id is the uniform 404.","operationId":"update_webhook_webhooks__subscription_id__patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"subscription_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Subscription Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookSubscriptionUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookSubscriptionResource"}}}},"401":{"description":"Missing or invalid API key (`unauthorized`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Refused (`test_mode_unavailable` / `email_unverified` / `paid_plan_required` / `live_key_browser_forbidden`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Request failed validation (`validation_error`; field errors under `details.errors`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limit exceeded (`rate_limited`; honest `Retry-After` + `X-RateLimit-*` headers).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Unexpected server error (`internal_error`; no internals leak).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"503":{"description":"Rate-limit backend unavailable (`service_unavailable`; retry after `Retry-After`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Unknown or foreign resource id (`not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}},"delete":{"tags":["webhooks"],"summary":"Delete Webhook","description":"Delete a tenant-owned subscription (204); a foreign/unknown id is the uniform not_found.","operationId":"delete_webhook_webhooks__subscription_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"subscription_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Subscription Id"}}],"responses":{"204":{"description":"Successful Response"},"401":{"description":"Missing or invalid API key (`unauthorized`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Refused (`test_mode_unavailable` / `email_unverified` / `paid_plan_required` / `live_key_browser_forbidden`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Request failed validation (`validation_error`; field errors under `details.errors`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limit exceeded (`rate_limited`; honest `Retry-After` + `X-RateLimit-*` headers).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Unexpected server error (`internal_error`; no internals leak).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"503":{"description":"Rate-limit backend unavailable (`service_unavailable`; retry after `Retry-After`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Unknown or foreign resource id (`not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/webhooks/{subscription_id}/regenerate-secret":{"post":{"tags":["webhooks"],"summary":"Regenerate Webhook Secret","description":"Rotate the signing secret and return the fresh value ONCE; the old secret stops verifying.\n\nTenant-scoped: a foreign/unknown id is the uniform not_found 404. The new Fernet token replaces\nthe old immediately (no grace window — a rotating tenant registers a second subscription for\noverlap). The raw secret is returned exactly once (D-13).","operationId":"regenerate_webhook_secret_webhooks__subscription_id__regenerate_secret_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"subscription_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Subscription Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookSubscriptionCreateResource"}}}},"401":{"description":"Missing or invalid API key (`unauthorized`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Refused (`test_mode_unavailable` / `email_unverified` / `paid_plan_required` / `live_key_browser_forbidden`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Request failed validation (`validation_error`; field errors under `details.errors`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limit exceeded (`rate_limited`; honest `Retry-After` + `X-RateLimit-*` headers).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Unexpected server error (`internal_error`; no internals leak).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"503":{"description":"Rate-limit backend unavailable (`service_unavailable`; retry after `Retry-After`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Unknown or foreign resource id (`not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}},"components":{"schemas":{"CampaignContactCreate":{"properties":{"phone":{"type":"string","maxLength":32,"minLength":3,"title":"Phone"},"name":{"anyOf":[{"type":"string","maxLength":120},{"type":"null"}],"title":"Name"},"language":{"anyOf":[{"type":"string","enum":["ar","en"]},{"type":"null"}],"title":"Language"},"channel":{"type":"string","enum":["whatsapp","sms"],"title":"Channel","default":"whatsapp"},"reference":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Reference","description":"Your own shipment identifier for this delivery — an AWB, an order id, an invoice number, a courier tracking code. Optional per contact. When present it is shown to the customer in the location-request message; when absent or blank the message is sent without it. Surrounding whitespace is stripped and internal whitespace runs are collapsed; a blank value is treated as absent."}},"type":"object","required":["phone"],"title":"CampaignContactCreate","description":"One pasted contact in a public campaign-create body (mirrors ``campaigns.ContactIn``).\n\n``phone`` is length-bounded at the client trust boundary; the AUTHORITATIVE E.164 validation is\nthe internal service (national-format numbers resolve against the tenant region). ``language``\noverrides the campaign default per contact; ``channel`` routes the send via WhatsApp (default)\nor the SMS fallback. Every bound is re-declared here, never imported from the internal schema."},"CampaignContactResource":{"properties":{"phone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Phone"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"state":{"type":"string","enum":["pending","sent","delivered","read","replied","location_received","out_of_radius","excluded","expired","failed"],"title":"State"},"lat":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Lat"},"lng":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Lng"},"failure_reason":{"anyOf":[{"type":"string","enum":["credits_exhausted","overage_cap_reached","quota_exceeded","subscription_locked","subscription_canceled","invalid_recipient","provider_error"]},{"type":"null"}],"title":"Failure Reason"}},"type":"object","required":["state"],"title":"CampaignContactResource","description":"One contact in a public campaign resource — echoed phone/name, derived state, coordinates.\n\n``state`` is the CLOSED public ``Literal`` DERIVED from contact fields by\n``mapping._contact_state`` (never the internal ``display_state`` UI copy — D-03). ``lat`` /\n``lng`` are ``None`` until the customer shares a location, and are FORCED to ``None`` with\n``phone``, ``name`` and ``failure_reason`` once the campaign is purged\n(``mapping.to_campaign_resource`` strip — D-13/D-16e; the mirror never resurrects PII).\n\n``failure_reason`` (D-38-03/D-38-04) names WHY a stopped contact stopped, from the CLOSED\nseven-value vocabulary above. It is ADDITIVE and OPTIONAL — ``None`` on every contact that never\nfailed and on every row written before the column existed — and ``state`` stays ``failed``\nbeside it, so no shipped contract moved to carry it."},"CampaignCreate":{"properties":{"contacts":{"items":{"$ref":"#/components/schemas/CampaignContactCreate"},"type":"array","maxItems":200,"minItems":1,"title":"Contacts"},"start":{"$ref":"#/components/schemas/RouteStopCreate"},"end":{"anyOf":[{"$ref":"#/components/schemas/RouteStopCreate"},{"type":"null"}],"description":"Optional distinct end point. Honored ONLY when `round_trip` is false; with the default `round_trip=true` the route returns to `start` and `end` is ignored."},"traffic":{"type":"boolean","title":"Traffic","default":false},"round_trip":{"type":"boolean","title":"Round Trip","description":"Return to `start` after the last stop (default). Set false for an open route — finishing at `end` when provided, else at the last optimized stop.","default":true},"deadline_at":{"type":"string","format":"date-time","title":"Deadline At"},"language":{"type":"string","enum":["ar","en"],"title":"Language","default":"ar"},"reminder_hours":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Reminder Hours","default":6}},"type":"object","required":["contacts","start","deadline_at"],"title":"CampaignCreate","description":"``POST /api/v1/campaigns`` body — contacts + depot/end + deadline + toggles (API-03).\n\nRe-declares the internal ``campaigns.schemas.CampaignCreateIn`` bounds VERBATIM (contacts\n1..200, WGS-84 depot/end reusing the ``RouteStopCreate`` pattern, the D-06 round-trip + D-08\ntraffic-OFF defaults, a future-only tz-aware ``deadline_at``, per-campaign language, a 1..48h\nreminder cadence) so the public boundary rejects the same bad input — but importing ZERO\ninternal schemas (D-04)."},"CampaignResource":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"status":{"type":"string","enum":["collecting","completed","expired","cancelled"],"title":"Status"},"route_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Route Id"},"purged_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Purged At"},"contacts":{"items":{"$ref":"#/components/schemas/CampaignContactResource"},"type":"array","title":"Contacts"}},"type":"object","required":["id","status","contacts"],"title":"CampaignResource","description":"The public campaign resource — id, PUBLIC status, linked route, purge marker, contacts.\n\n``status`` is the CLOSED public ``Literal`` (``mapping._CAMPAIGN_STATUS`` resolves it through\n``CampaignStatus(...)`` so an unknown value raises). ``route_id`` is ``None`` until the campaign\nauto-optimizes and links its route (D-04); ``purged_at`` is set once the SEC-02 rolling purge\nscrubs the campaign — its presence is why every contact's coordinates/phone read ``None``. Built\nby ``mapping.to_campaign_resource`` — never by re-exposing the internal ``CampaignOut`` (D-04)."},"ErrorBody":{"properties":{"code":{"type":"string","title":"Code"},"message":{"type":"string","title":"Message"},"details":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Details"}},"type":"object","required":["code","message"],"title":"ErrorBody","description":"The inner object of the ONE public error envelope (D-10) — code / message / details?.\n\n``code`` is the machine contract: a closed snake_case discriminator vocabulary (D-11).\n``message`` is an advisory human summary and may change without notice. ``details`` is present\nonly when a code carries structure (``validation_error`` field errors, ``quota_exceeded``\nmetric/used/limit)."},"ErrorEnvelope":{"properties":{"error":{"$ref":"#/components/schemas/ErrorBody"}},"type":"object","required":["error"],"title":"ErrorEnvelope","description":"Every non-2xx response of the public sub-app — ``{\"error\": {code, message, details?}}``.\n\nThis is the shape ``errors.register``'s handlers ACTUALLY emit on the wire for 4xx/5xx\n(CR-03): declaring it on every documented error response replaces FastAPI's default 422\n``HTTPValidationError`` promise (which the wire never returns) and gives the Phase-20 portal /\ngenerated clients the real error types — BEFORE the oasdiff gate locks the baseline in."},"RouteCreate":{"properties":{"start":{"$ref":"#/components/schemas/RouteStopCreate"},"end":{"anyOf":[{"$ref":"#/components/schemas/RouteStopCreate"},{"type":"null"}],"description":"Optional distinct end point. Honored ONLY when `round_trip` is false; with the default `round_trip=true` the route returns to `start` and `end` is ignored."},"stops":{"items":{"$ref":"#/components/schemas/RouteStopCreate"},"type":"array","maxItems":100,"minItems":2,"title":"Stops"},"traffic":{"type":"boolean","title":"Traffic","default":false},"round_trip":{"type":"boolean","title":"Round Trip","description":"Return to `start` after the last stop (default). Set false for an open route — finishing at `end` when provided, else at the last optimized stop.","default":true}},"type":"object","required":["start","stops"],"title":"RouteCreate","description":"``POST /api/v1/routes`` body — start + 2..100 stops + optional end (mirrors RouteCreateIn).\n\nThe bounds mirror the internal create verbatim (2..:data:`_MAX_STOPS_ABSOLUTE` stops, the D-06\nround-trip default, the\nD-08 traffic-OFF default) so the public boundary rejects the same bad input — but every bound\nis re-declared here, never imported from ``routing.schemas`` (D-04). Downstream 16-02 wires the\npath operation that consumes this shape.\n\nThe mirror is load-bearing for more than the published contract: the handler converts this\nbody into the INTERNAL ``RouteCreateIn`` (``routes_routes._to_internal``), so a public bound\nLOOSER than the internal one would turn an accepted public body into a server-side\nValidationError rather than a clean refusal."},"RouteResource":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"status":{"type":"string","enum":["optimizing","optimized","failed"],"title":"Status"},"stops":{"items":{"$ref":"#/components/schemas/RouteStopResource"},"type":"array","title":"Stops"},"total_distance_m":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Total Distance M"},"total_duration_s":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Total Duration S"},"polyline":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Polyline"},"handoff_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Handoff Url"}},"type":"object","required":["id","status","stops"],"title":"RouteResource","description":"The public route resource — id, PUBLIC status, ordered stops, totals, polyline, hand-off.\n\n``status`` is the CLOSED public ``Literal`` (``optimized``, never ``done`` — D-09). ``stops``\nare returned in input order carrying their optimized sequence + ETA. ``handoff_url`` is the\ndriver-ready hand-off link, populated ONLY once the route is ``optimized`` (``None`` while\noptimizing / on failure). Built by ``mapping.to_route_resource`` — never by re-exposing the\ninternal route read schema (which carries ``tenant_id`` / the cached matrix; D-04)."},"RouteStopCreate":{"properties":{"lat":{"type":"number","maximum":90.0,"minimum":-90.0,"title":"Lat"},"lng":{"type":"number","maximum":180.0,"minimum":-180.0,"title":"Lng"},"label":{"type":"string","maxLength":120,"minLength":1,"title":"Label"}},"type":"object","required":["lat","lng","label"],"title":"RouteStopCreate","description":"One point in a public route-create body — a start, a stop, or an end (mirrors StopIn).\n\nWGS-84 coordinate bounds are the client trust boundary (ASVS V5): the same ``lat``/``lng``\nranges and the same 1..120 ``label`` bound the internal ``routing.schemas.StopIn`` enforces,\nre-declared here rather than imported (D-04). No ``saved_place_id`` — a public integrator has\nno dashboard saved-place catalogue."},"RouteStopResource":{"properties":{"seq_input":{"type":"integer","title":"Seq Input"},"seq_optimized":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Seq Optimized"},"eta":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Eta"},"label":{"type":"string","title":"Label"},"lat":{"type":"number","title":"Lat"},"lng":{"type":"number","title":"Lng"}},"type":"object","required":["seq_input","label","lat","lng"],"title":"RouteStopResource","description":"One stop in a public route resource — input + optimized order, clock ETA, coordinates.\n\nDeliberately a driver-safe projection: displayed sequence, address label, coordinates, and the\nper-stop ETA. It NEVER carries ``tenant_id``, ``saved_place_id``, a dispatcher note, the cached\n``travel_matrix``, or any other internal-only field (the ``routing.schemas.DriverStopOut``\nsafety-by-omission precedent)."},"UsageMetricResource":{"properties":{"used":{"type":"integer","title":"Used"},"limit":{"type":"integer","title":"Limit"}},"type":"object","required":["used","limit"],"title":"UsageMetricResource","description":"One metered dimension's used/limit for the caller's current period (routes/whatsapp/sms)."},"UsageResource":{"properties":{"plan":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Plan"},"subscription_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Subscription Status"},"current_period_end":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Current Period End"},"cancel_at_period_end":{"type":"boolean","title":"Cancel At Period End","default":false},"period":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Period"},"livemode":{"type":"boolean","title":"Livemode","default":true},"routes":{"$ref":"#/components/schemas/UsageMetricResource"},"whatsapp":{"$ref":"#/components/schemas/UsageMetricResource"},"sms":{"$ref":"#/components/schemas/UsageMetricResource"}},"type":"object","required":["routes","whatsapp","sms"],"title":"UsageResource","description":"The public usage + account read shape (API-04; wired by ``GET /api/v1/usage`` in 16-03).\n\nQuota consumption, plan limits, the current-period window, and subscription state — sourced from\nthe SAME plan quotas + usage counters the dashboard Billing page reads (``billing.service``), so\nthe numbers a tenant sees over the API match the dashboard exactly (D-18). Hand-written so the\npublic shape never drifts by re-exporting an internal billing schema (D-04); it carries NO cost\ndata and NO per-key breakdown.\n\nSubscription state is FLAT — ``plan`` (the plan key), ``subscription_status``,\n``current_period_end``, ``cancel_at_period_end`` — the four fields D-18 names. A tenant with NO\nsubscription is the honest null shape: ``plan`` / ``subscription_status`` / ``period`` /\n``current_period_end`` are ``None``, ``cancel_at_period_end`` ``False``, every metric a zeroed\n``used=0/limit=0``. The read never 404s (an auth-only account read, D-16); the nullable fields\nkeep it additive-safe.\n\n``livemode`` (additive, D-05/19-07 — defaults ``True`` so the field is non-breaking under\noasdiff) is ``False`` under a ``dk_test_`` key: then the metric ``used``/``limit`` and the\n``period`` label report the ``test:{period}`` namespace against the named test caps, while the\nsubscription-state fields are unchanged (test mode is zero-money, account state is orthogonal)."},"WebhookSubscriptionCreate":{"properties":{"url":{"type":"string","maxLength":2048,"minLength":1,"title":"Url"},"events":{"items":{"type":"string","enum":["route.optimized","location.collected","campaign.completed"]},"type":"array","minItems":1,"title":"Events"},"mode":{"anyOf":[{"type":"string","enum":["live","test"]},{"type":"null"}],"title":"Mode"}},"type":"object","required":["url","events"],"title":"WebhookSubscriptionCreate","description":"``POST /api/v1/webhooks`` body — the delivery URL + the events to subscribe to (WHK-01).\n\n``url`` is length-bounded at the client trust boundary; the AUTHORITATIVE https-only + SSRF\nvalidation is the internal service (``_validate_url`` — it needs DNS resolution, D-11), which\nsurfaces a refusal as the ``validation_error`` envelope. ``events`` is a non-empty list of the\nclosed ``WebhookEventTypeLiteral`` set (an unknown event is a schema-level 422). ``mode`` is\nOPTIONAL (additive, D-15): omitted → the subscription inherits the KEY's mode (a ``dk_test_``\nkey registers a test subscription, a live key a live one); a present-and-contradictory ``mode``\n→ the ``validation_error`` envelope (the key wins, never the body)."},"WebhookSubscriptionCreateResource":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"url":{"type":"string","title":"Url"},"events":{"items":{"type":"string","enum":["route.optimized","location.collected","campaign.completed"]},"type":"array","title":"Events"},"mode":{"type":"string","enum":["live","test"],"title":"Mode"},"status":{"type":"string","enum":["active","disabled"],"title":"Status"},"disabled_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Disabled At"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"secret":{"type":"string","title":"Secret"}},"type":"object","required":["id","url","events","mode","status","created_at","secret"],"title":"WebhookSubscriptionCreateResource","description":"The create/regenerate result — a ``WebhookSubscriptionResource`` + the raw secret ONCE.\n\n``secret`` (the full ``whsec_…`` value) is the ONLY place the raw signing secret ever leaves the\nserver; the DB stores only its Fernet ciphertext, so a lost secret is regenerated\n(``POST /{id}/regenerate-secret``), never recovered."},"WebhookSubscriptionResource":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"url":{"type":"string","title":"Url"},"events":{"items":{"type":"string","enum":["route.optimized","location.collected","campaign.completed"]},"type":"array","title":"Events"},"mode":{"type":"string","enum":["live","test"],"title":"Mode"},"status":{"type":"string","enum":["active","disabled"],"title":"Status"},"disabled_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Disabled At"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","url","events","mode","status","created_at"],"title":"WebhookSubscriptionResource","description":"The public webhook subscription resource — id, url, events, mode, DERIVED status, timestamps.\n\nThe signing secret NEVER crosses this shape (D-13 copy-once — it rides only the create/regen\n``WebhookSubscriptionCreateResource``). ``status`` is the DERIVED active|disabled display\n(D-09), not the internal ``is_active`` bool + ``consecutive_failures`` counter (auto-disable\nmachinery an integrator does not branch on). Built by ``mapping.to_webhook_resource`` — never\nby re-exposing the internal ``WebhookSubscriptionOut`` (D-04)."},"WebhookSubscriptionUpdate":{"properties":{"url":{"anyOf":[{"type":"string","maxLength":2048,"minLength":1},{"type":"null"}],"title":"Url"},"events":{"anyOf":[{"items":{"type":"string","enum":["route.optimized","location.collected","campaign.completed"]},"type":"array","minItems":1},{"type":"null"}],"title":"Events"}},"type":"object","title":"WebhookSubscriptionUpdate","description":"``PATCH /api/v1/webhooks/{id}`` body — optional URL and/or events (each re-validated).\n\nAn absent field leaves the stored value untouched; a present ``url`` is re-run through the\nregistration-time SSRF guard (a rebind to an internal target is refused on update too, D-11)."}},"securitySchemes":{"HTTPBearer":{"type":"http","scheme":"bearer"}}}}