{"openapi":"3.1.0","info":{"title":"Links API","version":"3.0.0","description":"Public link-management API for zpe.vc/links, the link layer of the zpe.vc estate. Every operation lives under the versioned base `/_/api/v1`, and this document is served at `/_/api/v1/openapi.json`. Authenticate with an account key issued at the door (auth.zpe.vc) and granted `zpe-links`.\n\n## Permissions\n\nA key carries the identity of whoever issued it, and that identity owns everything the key creates. Reads are open across owners: any key with the `read` scope lists and fetches every link, bundle and analytics figure on the deployment. Writes are owner-scoped: updating, disabling, enabling or deleting a link or a bundle, and changing its slugs, is refused with 403 for any identity other than the owner. Two calls are open by design and carry no owner check: adding a custom slug to a link, and adding a link to a bundle.\n\nA link or a custom slug can be deleted only while it has recorded zero clicks. After that the delete is refused with 400 and disable is the operation that works, since a clicked short link is already in circulation and deleting it frees its slug for reuse. Bundles carry no such rule.\n\nScopes are a separate gate applied first: a `read` key is refused on every write with 403 before ownership is consulted.\n\nBuilt and maintained by Oddbit (https://oddbit.id).","contact":{"name":"Oddbit","url":"https://oddbit.id"},"license":{"name":"Apache 2.0","url":"https://www.apache.org/licenses/LICENSE-2.0"}},"servers":[{"url":"/links"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"zpk1.*","description":"Account key issued at the door (auth.zpe.vc) and granted zpe-links. Pass as `Authorization: Bearer zpk1...`."}},"schemas":{"Link":{"type":"object","properties":{"id":{"type":"integer","example":42},"url":{"type":"string","maxLength":2048,"format":"uri","example":"https://example.com/long-url"},"label":{"type":["string","null"],"example":"Marketing landing"},"created_at":{"type":"integer"},"expires_at":{"type":["integer","null"],"description":"Unix seconds; null means no expiry."},"created_via":{"type":["string","null"]},"created_by":{"type":"string"},"slugs":{"type":"array","items":{"$ref":"#/components/schemas/Slug"}},"total_clicks":{"type":"integer","minimum":0,"example":137},"delta_pct":{"type":"number","description":"Click count change as percentage. Optional; absent when comparison data is unavailable."}},"required":["id","url","label","created_at","expires_at","created_via","created_by","slugs","total_clicks"],"description":"A short link with its slugs, total click count, and metadata."},"Slug":{"type":"object","properties":{"link_id":{"type":"integer"},"slug":{"type":"string"},"is_custom":{"type":"integer"},"is_primary":{"type":"integer"},"click_count":{"type":"integer","minimum":0},"created_at":{"type":"integer"},"disabled_at":{"type":["integer","null"]}},"required":["link_id","slug","is_custom","is_primary","click_count","created_at","disabled_at"],"description":"A slug attached to a link."},"ErrorResponse":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false,"description":"Error response with a single human-readable message."},"CreateLinkBody":{"type":"object","properties":{"url":{"type":"string","maxLength":2048,"format":"uri"},"label":{"type":"string"},"slug_length":{"type":"integer","minimum":3,"maximum":128},"expires_at":{"type":"integer","minimum":0},"allow_duplicate":{"type":"boolean"}},"required":["url"],"additionalProperties":false},"UpdateLinkBody":{"type":"object","properties":{"url":{"type":"string","maxLength":2048,"format":"uri"},"label":{"type":["string","null"]},"expires_at":{"type":["integer","null"],"minimum":0}},"additionalProperties":false},"AddSlugBody":{"type":"object","properties":{"slug":{"type":"string","minLength":1,"maxLength":128,"pattern":"^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i"}},"required":["slug"],"additionalProperties":false},"Bundle":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"},"description":{"type":["string","null"]},"icon":{"type":["string","null"]},"accent":{"type":"string","enum":["orange","red","green","blue","purple"]},"archived_at":{"type":["integer","null"]},"created_via":{"type":["string","null"]},"created_by":{"type":"string"},"created_at":{"type":"integer"},"updated_at":{"type":"integer"}},"required":["id","name","description","icon","accent","archived_at","created_via","created_by","created_at","updated_at"]},"ClickStats":{"type":"object","properties":{"total_clicks":{"type":"integer","minimum":0},"countries":{"type":"array","items":{"$ref":"#/components/schemas/NameCountBucket"}},"referrers":{"type":"array","items":{"$ref":"#/components/schemas/NameCountBucket"}},"referrer_hosts":{"type":"array","items":{"$ref":"#/components/schemas/NameCountBucket"}},"devices":{"type":"array","items":{"$ref":"#/components/schemas/NameCountBucket"}},"os":{"type":"array","items":{"$ref":"#/components/schemas/NameCountBucket"}},"browsers":{"type":"array","items":{"$ref":"#/components/schemas/NameCountBucket"}},"link_modes":{"type":"array","items":{"$ref":"#/components/schemas/NameCountBucket"}},"channels":{"type":"array","items":{"$ref":"#/components/schemas/NameCountBucket"}},"clicks_over_time":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string"},"count":{"type":"integer","minimum":0}},"required":["date","count"]}},"slug_clicks":{"type":"array","items":{"type":"object","properties":{"slug":{"type":"string"},"count":{"type":"integer","minimum":0}},"required":["slug","count"]}},"num_countries":{"type":"integer","minimum":0},"num_referrers":{"type":"integer","minimum":0},"num_referrer_hosts":{"type":"integer","minimum":0},"num_os":{"type":"integer","minimum":0},"num_browsers":{"type":"integer","minimum":0}},"required":["total_clicks","countries","referrers","referrer_hosts","devices","os","browsers","link_modes","channels","clicks_over_time","slug_clicks","num_countries","num_referrers","num_referrer_hosts","num_os","num_browsers"],"description":"Per-link click analytics breakdown."},"NameCountBucket":{"type":"object","properties":{"name":{"type":"string"},"count":{"type":"integer","minimum":0}},"required":["name","count"]},"TimelineData":{"type":"object","properties":{"range":{"type":"string","enum":["24h","7d","30d","90d","1y","all"]},"buckets":{"type":"array","items":{"$ref":"#/components/schemas/TimelineBucket"}},"summary":{"type":"object","properties":{"last_24h":{"type":"integer","minimum":0},"last_7d":{"type":"integer","minimum":0},"last_30d":{"type":"integer","minimum":0},"last_90d":{"type":"integer","minimum":0},"last_1y":{"type":"integer","minimum":0}},"required":["last_24h","last_7d","last_30d","last_90d","last_1y"]}},"required":["range","buckets","summary"],"description":"Click timeline with bucketed counts and period summaries."},"TimelineBucket":{"type":"object","properties":{"label":{"type":"string"},"count":{"type":"integer","minimum":0}},"required":["label","count"]},"BreakdownPage":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/NameCountBucket"},"description":"Rows for this page, ordered by count descending."},"total":{"type":"integer","minimum":0,"description":"Total distinct values across all pages for this dimension and range."}},"required":["items","total"],"description":"One page of a paginated analytics breakdown."},"BundleWithSummary":{"allOf":[{"$ref":"#/components/schemas/Bundle"},{"type":"object","properties":{"link_count":{"type":"integer","minimum":0},"total_clicks":{"type":"integer","minimum":0},"delta_pct":{"type":"number"},"sparkline":{"type":"array","items":{"type":"integer","minimum":0}},"top_links":{"type":"array","items":{"type":"object","properties":{"slug":{"type":"string"},"click_count":{"type":"integer","minimum":0}},"required":["slug","click_count"]}}},"required":["link_count","total_clicks","sparkline","top_links"]}],"description":"Bundle with aggregated click and link counts."},"CreateBundleBody":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120},"description":{"type":["string","null"]},"icon":{"type":["string","null"]},"accent":{"type":"string","enum":["orange","red","green","blue","purple"]}},"required":["name"],"additionalProperties":false},"UpdateBundleBody":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120},"description":{"type":["string","null"]},"icon":{"type":["string","null"]},"accent":{"type":"string","enum":["orange","red","green","blue","purple"]}},"additionalProperties":false},"AddBundleLinkBody":{"type":"object","properties":{"link_id":{"type":"integer","exclusiveMinimum":0}},"required":["link_id"],"additionalProperties":false},"Page":{"type":"object","properties":{"name":{"type":"string"},"headline":{"type":"string"},"bio":{"type":"string"},"email":{"type":["string","null"]},"theme":{"type":"string","enum":["ink","paper"]},"accent":{"type":"string"},"created_at":{"type":"integer"},"updated_at":{"type":"integer"},"blocks":{"type":"array","items":{"$ref":"#/components/schemas/PageBlock"}}},"required":["name","headline","bio","email","theme","accent","created_at","updated_at","blocks"],"description":"A page, with every block including the hidden ones."},"PageBlock":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"},"position":{"type":"integer"},"kind":{"type":"string","enum":["link","app","form","image","text"]},"title":{"type":"string"},"subtitle":{"type":["string","null"]},"href":{"type":["string","null"]},"app_id":{"type":["string","null"]},"r2_key":{"type":["string","null"]},"alt":{"type":["string","null"]},"body":{"type":["string","null"]},"is_first":{"type":"boolean"},"visible":{"type":"boolean"},"created_at":{"type":"integer"},"updated_at":{"type":"integer"}},"required":["id","name","position","kind","title","subtitle","href","app_id","r2_key","alt","body","is_first","visible","created_at","updated_at"],"description":"One row on a page: a link, an app of this universe, a question, a picture, or words. A form block carries neither an href nor an app id; what it asks is stored beside the page (page_forms). A picture carries the key its bytes were stored under, and the page serves them from its own host. A text block carries its words in `body` and nothing that can be followed: it is drawn as a heading when it has a title and as a paragraph when it does not."},"PageWithReport":{"allOf":[{"$ref":"#/components/schemas/Page"},{"type":"object","properties":{"report":{"$ref":"#/components/schemas/PageReport"}},"required":["report"]}],"description":"A page and its report, answered together so the two cannot disagree."},"PageReport":{"type":"object","properties":{"views":{"type":"integer","minimum":0,"description":"Views of the page, bots excluded."},"taps":{"type":"integer","minimum":0,"description":"Every block tap, bots excluded."},"intents":{"type":"integer","minimum":0,"example":3,"description":"Visits where somebody tapped the line under the page to ask for a page of their own, bots excluded. Beside views and taps, never inside `acts`: a page performs no intent, a visitor does."},"acts":{"type":"object","properties":{"card":{"type":"integer","minimum":0},"email":{"type":"integer","minimum":0},"share":{"type":"integer","minimum":0},"submit":{"type":"integer","minimum":0,"description":"Every answer a visitor sent, counted from the event ledger: it survives an answer's delete and never ages out of a window. A block's `answers` is the rows an owner can read, so the two differ after a delete by design."}},"required":["card","email","share","submit"],"description":"The acts a page performs itself, and the answer a visitor sends: every one all time, from the page's events."},"blocks":{"type":"array","items":{"$ref":"#/components/schemas/PageReportBlock"}},"rules":{"type":"array","items":{"$ref":"#/components/schemas/PageReportRule"},"description":"Taps per rule, most-tapped first: the tap row carries the rule id, so the report says which rule earned it."},"countries":{"type":"array","items":{"$ref":"#/components/schemas/PageReportRow"},"description":"Where the views came from, busiest first, at most 5."},"hosts":{"type":"array","items":{"$ref":"#/components/schemas/PageReportRow"},"description":"The host a view arrived from, busiest first, at most 5."},"bot_views":{"type":"integer","minimum":0,"description":"Rows a bot left in the store: evidence, and excluded from every number beside it."}},"required":["views","taps","intents","acts","blocks","rules","countries","hosts","bot_views"],"description":"What happened on one page, counted from one grouped read. Bots are stored and not counted."},"PageReportBlock":{"type":"object","properties":{"id":{"type":"integer","example":7},"title":{"type":"string","example":"Book a call"},"taps":{"type":"integer","minimum":0,"example":4},"answers":{"type":"integer","minimum":0,"example":3,"description":"Answers the block's question holds in the last ninety days, the window the tab's inbox lists. Zero for a block that asks nothing. `acts.submit` is the lifetime count of answers sent."},"on_page":{"type":"boolean","description":"False for a block since removed: its taps are kept, under the title the tap stored."}},"required":["id","title","taps","answers","on_page"],"description":"One block's taps and answers, most-tapped first."},"PageReportRule":{"type":"object","properties":{"id":{"type":"integer","example":3},"kind":{"type":["string","null"],"enum":["host","utm","country","lang",null],"description":"What the rule matched. Null when the rule was deleted and its taps remain."},"value":{"type":["string","null"],"example":"instagram.com","description":"The value the rule matched. Null when the rule was deleted."},"taps":{"type":"integer","minimum":0,"example":5},"on_page":{"type":"boolean","description":"False for a rule since deleted: the taps it earned are kept, as a block's are."}},"required":["id","kind","value","taps","on_page"],"description":"One rule's taps, most-tapped first: which rule earned the tap it labelled, and how many."},"PageReportRow":{"type":"object","properties":{"value":{"type":"string","example":"ID"},"count":{"type":"integer","minimum":0,"example":12}},"required":["value","count"],"description":"One ranked value, and how many rows carried it."},"PageSubmissions":{"type":"object","properties":{"answers":{"type":"array","items":{"$ref":"#/components/schemas/PageSubmission"},"description":"Every answer the page's questions hold in the last 90 days, newest first. Bots are stored and not listed, as they are not counted in the report."}},"required":["answers"],"description":"A page's answers in the inbox's window, newest first."},"PageSubmission":{"type":"object","properties":{"id":{"type":"integer","example":12},"block_id":{"type":"integer","example":7},"block_title":{"type":["string","null"],"example":"Ask me anything","description":"The block's title as it reads now, so a rename shows up once rather than once per answer. Null when the block has since been removed and its answers remain: the answer outlives its question by design, and no title was stored on it."},"value":{"type":"string","example":"hello@example.test","description":"What the visitor typed, exactly as stored. The one column that holds it."},"at":{"type":"integer","description":"When the answer arrived, in Unix milliseconds."},"country":{"type":["string","null"],"example":"TH","description":"The country Cloudflare named for the visitor. Null when it named none."}},"required":["id","block_id","block_title","value","at","country"],"description":"One answer, and the block it answers."},"PageState":{"type":"object","properties":{"name":{"type":"string"},"state":{"type":"string","enum":["claimed","live","retired"]},"published":{"type":"boolean"}},"required":["name","state","published"],"description":"Whether the address answers, after the write."}},"parameters":{}},"paths":{"/_/api/v1/links":{"post":{"tags":["links"],"summary":"Create a short link","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateLinkBody"}}}},"responses":{"200":{"description":"Existing link returned (duplicate URL).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Link"}}}},"201":{"description":"Created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Link"}}}},"400":{"description":"Validation error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Scope insufficient.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Conflict.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"postLinks"},"get":{"tags":["links"],"summary":"List all short links","parameters":[{"schema":{"type":"string","enum":["24h","7d","30d","90d","1y","all"],"description":"Time range for analytics. Defaults are documented per endpoint."},"required":false,"description":"Time range for analytics. Defaults are documented per endpoint.","name":"range","in":"query"}],"responses":{"200":{"description":"OK.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Link"}}}}},"400":{"description":"Validation error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Scope insufficient.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"getLinks"}},"/_/api/v1/links/{id}":{"get":{"tags":["links"],"summary":"Get a link with click stats","parameters":[{"schema":{"type":"integer","exclusiveMinimum":0,"example":42},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","enum":["24h","7d","30d","90d","1y","all"],"description":"Time range for analytics. Defaults are documented per endpoint."},"required":false,"description":"Time range for analytics. Defaults are documented per endpoint.","name":"range","in":"query"}],"responses":{"200":{"description":"OK.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Link"}}}},"400":{"description":"Validation error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Scope insufficient.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"getLinksById"},"put":{"tags":["links"],"summary":"Update a link's URL, label, or expiry","description":"Owner only. The identity behind the API key must be the link's `created_by`, otherwise the call is refused with 403.","parameters":[{"schema":{"type":"integer","exclusiveMinimum":0,"example":42},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateLinkBody"}}}},"responses":{"200":{"description":"Updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Link"}}}},"400":{"description":"Validation error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Refused. Either the key lacks the `create` scope, or the caller is not the link owner. Ownership is fixed at creation and is not transferable: only the identity that created a link can change it. Reading is open to every authenticated key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"putLinksById"},"delete":{"tags":["links"],"summary":"Delete a link permanently","description":"Owner only, and only while the link has recorded zero clicks. Once any click exists the link is permanent and this call returns 400; disable it instead. Deleting removes the link, its slugs, and frees those slugs for reuse by anyone.","parameters":[{"schema":{"type":"integer","exclusiveMinimum":0,"example":42},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean"}},"required":["deleted"]}}}},"400":{"description":"Refused. The link has recorded at least one click, so it cannot be deleted: `Cannot delete a link with clicks, disable it instead`. A clicked short link is already in circulation, in email, in print and in QR codes, and deleting it frees the slug for reuse. Call `POST /links/{id}/disable` instead, which stops the redirect and keeps the history. The lifetime count is unfiltered, so a single bot click is enough. Also returned for a malformed request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Refused. Either the key lacks the `create` scope, or the caller is not the link owner. Ownership is fixed at creation and is not transferable: only the identity that created a link can change it. Reading is open to every authenticated key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"deleteLinksById"}},"/_/api/v1/links/{id}/disable":{"post":{"tags":["links"],"summary":"Disable a link","description":"Owner only. Stops every slug on the link from redirecting while keeping the link and its click history. This is the correct operation for a link that is already in circulation, and the only one available once the link has recorded a click.","parameters":[{"schema":{"type":"integer","exclusiveMinimum":0,"example":42},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Disabled.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Link"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Refused. Either the key lacks the `create` scope, or the caller is not the link owner. Ownership is fixed at creation and is not transferable: only the identity that created a link can change it. Reading is open to every authenticated key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"postLinksByIdDisable"}},"/_/api/v1/links/{id}/enable":{"post":{"tags":["links"],"summary":"Re-enable a disabled link","description":"Owner only. Clears `expires_at` outright, including an expiry set at create or update, so the link resolves again and no longer expires.","parameters":[{"schema":{"type":"integer","exclusiveMinimum":0,"example":42},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Enabled.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Link"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Refused. Either the key lacks the `create` scope, or the caller is not the link owner. Ownership is fixed at creation and is not transferable: only the identity that created a link can change it. Reading is open to every authenticated key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"postLinksByIdEnable"}},"/_/api/v1/links/{id}/slugs":{"post":{"tags":["slugs"],"summary":"Add a custom slug to a link","description":"Open to any authenticated caller with the `create` scope, including on a link owned by someone else. Adding a slug creates an additional address for the existing destination; it cannot change, disable or delete anything. Removing or disabling that slug afterwards is owner-gated.","parameters":[{"schema":{"type":"integer","exclusiveMinimum":0,"example":42},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddSlugBody"}}}},"responses":{"201":{"description":"Slug added.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Slug"}}}},"400":{"description":"Validation error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Scope insufficient.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Conflict.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"postLinksByIdSlugs"}},"/_/api/v1/links/{id}/slugs/{slug}/disable":{"post":{"tags":["slugs"],"summary":"Disable a specific slug on a link","description":"Owner of the parent link only. Custom slugs only: the system-generated slug cannot be disabled. The slug stops resolving and keeps its click history.","parameters":[{"schema":{"type":"integer","exclusiveMinimum":0,"example":42},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","pattern":"^[a-zA-Z0-9_-]+$"},"required":true,"name":"slug","in":"path"}],"responses":{"200":{"description":"Disabled.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Slug"}}}},"400":{"description":"Refused. The system-generated slug cannot be disabled, only custom slugs can. Disable the whole link instead. Also returned for a malformed request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Refused. Either the key lacks the `create` scope, or the caller is not the link owner. Ownership is fixed at creation and is not transferable: only the identity that created a link can change it. Reading is open to every authenticated key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"postLinksByIdSlugsBySlugDisable"}},"/_/api/v1/links/{id}/slugs/{slug}/enable":{"post":{"tags":["slugs"],"summary":"Re-enable a disabled slug on a link","description":"Owner of the parent link only.","parameters":[{"schema":{"type":"integer","exclusiveMinimum":0,"example":42},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","pattern":"^[a-zA-Z0-9_-]+$"},"required":true,"name":"slug","in":"path"}],"responses":{"200":{"description":"Enabled.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Slug"}}}},"400":{"description":"Validation error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Refused. Either the key lacks the `create` scope, or the caller is not the link owner. Ownership is fixed at creation and is not transferable: only the identity that created a link can change it. Reading is open to every authenticated key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"postLinksByIdSlugsBySlugEnable"}},"/_/api/v1/links/{id}/slugs/{slug}":{"delete":{"tags":["slugs"],"summary":"Remove a custom slug from a link","description":"Owner of the parent link only, custom slugs only, and only while the slug has recorded zero clicks. Once any click exists the slug is permanent and this call returns 400; disable it instead.","parameters":[{"schema":{"type":"integer","exclusiveMinimum":0,"example":42},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","pattern":"^[a-zA-Z0-9_-]+$"},"required":true,"name":"slug","in":"path"}],"responses":{"200":{"description":"Removed.","content":{"application/json":{"schema":{"type":"object","properties":{"removed":{"type":"boolean"}},"required":["removed"]}}}},"400":{"description":"Refused. Either the slug has recorded at least one click (`Cannot remove a slug with clicks, disable it instead`), or it is the link's system-generated slug, which can never be removed. For a clicked custom slug, call `POST /links/{id}/slugs/{slug}/disable` instead: it stops that slug resolving and keeps its click history. For the system-generated slug, disable the whole link with `POST /links/{id}/disable`. Also returned for a malformed request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Refused. Either the key lacks the `create` scope, or the caller is not the link owner. Ownership is fixed at creation and is not transferable: only the identity that created a link can change it. Reading is open to every authenticated key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"deleteLinksByIdSlugsBySlug"}},"/_/api/v1/links/{id}/qr":{"get":{"tags":["qr"],"summary":"Get a QR code SVG for a link","parameters":[{"schema":{"type":"integer","exclusiveMinimum":0,"example":42},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","pattern":"^[a-zA-Z0-9_-]+$","description":"Optional specific slug. Defaults to the link's primary slug."},"required":false,"description":"Optional specific slug. Defaults to the link's primary slug.","name":"slug","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":2048,"description":"QR pixel dimensions (square). Integer 1-2048; defaults to 220."},"required":false,"description":"QR pixel dimensions (square). Integer 1-2048; defaults to 220.","name":"size","in":"query"}],"responses":{"200":{"description":"SVG image","content":{"image/svg+xml":{"schema":{"type":"string"}}}},"400":{"description":"Validation error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Scope insufficient.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"getLinksByIdQr"}},"/_/api/v1/links/{id}/bundles":{"get":{"tags":["links","bundles"],"summary":"List bundles a link belongs to","parameters":[{"schema":{"type":"integer","exclusiveMinimum":0,"example":42},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"OK.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Bundle"}}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Scope insufficient.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"getLinksByIdBundles"}},"/_/api/v1/links/{id}/analytics":{"get":{"tags":["links","analytics"],"summary":"Get analytics for a link (defaults to all-time)","parameters":[{"schema":{"type":"integer","exclusiveMinimum":0,"example":42},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","enum":["24h","7d","30d","90d","1y","all"],"description":"Time range for analytics. Defaults are documented per endpoint."},"required":false,"description":"Time range for analytics. Defaults are documented per endpoint.","name":"range","in":"query"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClickStats"}}}},"400":{"description":"Validation error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Scope insufficient.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"getLinksByIdAnalytics"}},"/_/api/v1/links/{id}/timeline":{"get":{"tags":["links","analytics"],"summary":"Get click timeline for a link (defaults to all-time)","parameters":[{"schema":{"type":"integer","exclusiveMinimum":0,"example":42},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","enum":["24h","7d","30d","90d","1y","all"],"description":"Time range for analytics. Defaults are documented per endpoint."},"required":false,"description":"Time range for analytics. Defaults are documented per endpoint.","name":"range","in":"query"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TimelineData"}}}},"400":{"description":"Validation error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Scope insufficient.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"getLinksByIdTimeline"}},"/_/api/v1/links/{id}/breakdown":{"get":{"tags":["links","analytics"],"summary":"Page through a link's countries, sources, or domains breakdown","parameters":[{"schema":{"type":"integer","exclusiveMinimum":0,"example":42},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","enum":["countries","referrers","referrer_hosts"],"description":"Which panel to page through: countries, referrers (sources), or referrer_hosts (domains)."},"required":true,"description":"Which panel to page through: countries, referrers (sources), or referrer_hosts (domains).","name":"dimension","in":"query"},{"schema":{"type":"string","enum":["24h","7d","30d","90d","1y","all"],"description":"Time range. Defaults to all-time."},"required":false,"description":"Time range. Defaults to all-time.","name":"range","in":"query"},{"schema":{"type":["integer","null"],"minimum":0,"description":"Row offset into the ranked list. Defaults to 0."},"required":false,"description":"Row offset into the ranked list. Defaults to 0.","name":"offset","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100,"description":"Page size, 1-100. Defaults to 10."},"required":false,"description":"Page size, 1-100. Defaults to 10.","name":"limit","in":"query"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BreakdownPage"}}}},"400":{"description":"Validation error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Scope insufficient.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"getLinksByIdBreakdown"}},"/_/api/v1/slugs/{slug}":{"get":{"tags":["slugs"],"summary":"Look up a link by its slug","description":"Open to every authenticated key, whoever owns the link. Reads are not owner-scoped.","parameters":[{"schema":{"type":"string","pattern":"^[a-zA-Z0-9_-]+$"},"required":true,"name":"slug","in":"path"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Link"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Scope insufficient.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Slug not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"getSlugsBySlug"}},"/_/api/v1/bundles":{"get":{"tags":["bundles"],"summary":"List bundles","description":"Every bundle on the deployment, not only the caller's. Reads are open across owners.","parameters":[{"schema":{"type":"string","enum":["1","true","only","all"],"description":"true|1|only -> archived only. all -> include archived. Omit -> active only."},"required":false,"description":"true|1|only -> archived only. all -> include archived. Omit -> active only.","name":"archived","in":"query"},{"schema":{"type":"string","enum":["24h","7d","30d","90d","1y","all"],"description":"Time range for analytics. Defaults are documented per endpoint."},"required":false,"description":"Time range for analytics. Defaults are documented per endpoint.","name":"range","in":"query"}],"responses":{"200":{"description":"OK.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/BundleWithSummary"}}}}},"400":{"description":"Validation error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Scope insufficient.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"getBundles"},"post":{"tags":["bundles"],"summary":"Create a bundle","description":"The identity behind the API key becomes the bundle's owner and is the only one who can later change it.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateBundleBody"}}}},"responses":{"201":{"description":"Created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Bundle"}}}},"400":{"description":"Validation error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Scope insufficient.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"postBundles"}},"/_/api/v1/bundles/{id}":{"get":{"tags":["bundles"],"summary":"Get a bundle","description":"Open to every authenticated key, whoever owns the bundle.","parameters":[{"schema":{"type":"integer","exclusiveMinimum":0,"example":42},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","enum":["24h","7d","30d","90d","1y","all"],"description":"Time range for analytics. Defaults are documented per endpoint."},"required":false,"description":"Time range for analytics. Defaults are documented per endpoint.","name":"range","in":"query"}],"responses":{"200":{"description":"OK.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BundleWithSummary"}}}},"400":{"description":"Validation error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Scope insufficient.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"getBundlesById"},"put":{"tags":["bundles"],"summary":"Update a bundle","description":"Owner only. Refused with 403 for any other identity.","parameters":[{"schema":{"type":"integer","exclusiveMinimum":0,"example":42},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateBundleBody"}}}},"responses":{"200":{"description":"Updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Bundle"}}}},"400":{"description":"Validation error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Refused. Either the key lacks the `create` scope, or the caller is not the bundle owner. Ownership is fixed at creation and is not transferable: only the identity that created a bundle can change it. Reading a bundle, its links and its analytics is open to every authenticated key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"putBundlesById"},"delete":{"tags":["bundles"],"summary":"Delete a bundle permanently","description":"Owner only. Unlike a link, a bundle carries no zero-click rule: a bundle is a grouping, not an address, so deleting one drops the memberships and leaves every member link and its click history in place. Archive instead when you only want it out of the default listing.","parameters":[{"schema":{"type":"integer","exclusiveMinimum":0,"example":42},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean"}},"required":["deleted"]}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Refused. Either the key lacks the `create` scope, or the caller is not the bundle owner. Ownership is fixed at creation and is not transferable: only the identity that created a bundle can change it. Reading a bundle, its links and its analytics is open to every authenticated key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"deleteBundlesById"}},"/_/api/v1/bundles/{id}/archive":{"post":{"tags":["bundles"],"summary":"Archive a bundle","description":"Owner only. Hides the bundle from the default listing without deleting anything. Reversible through unarchive.","parameters":[{"schema":{"type":"integer","exclusiveMinimum":0,"example":42},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Archived.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Bundle"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Refused. Either the key lacks the `create` scope, or the caller is not the bundle owner. Ownership is fixed at creation and is not transferable: only the identity that created a bundle can change it. Reading a bundle, its links and its analytics is open to every authenticated key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"postBundlesByIdArchive"}},"/_/api/v1/bundles/{id}/unarchive":{"post":{"tags":["bundles"],"summary":"Unarchive a bundle","description":"Owner only. Returns an archived bundle to the default listing.","parameters":[{"schema":{"type":"integer","exclusiveMinimum":0,"example":42},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Unarchived.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Bundle"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Refused. Either the key lacks the `create` scope, or the caller is not the bundle owner. Ownership is fixed at creation and is not transferable: only the identity that created a bundle can change it. Reading a bundle, its links and its analytics is open to every authenticated key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"postBundlesByIdUnarchive"}},"/_/api/v1/bundles/{id}/analytics":{"get":{"tags":["bundles"],"summary":"Get bundle analytics. Defaults to all-time if no range given.","parameters":[{"schema":{"type":"integer","exclusiveMinimum":0,"example":42},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","enum":["24h","7d","30d","90d","1y","all"],"description":"Time range for analytics. Defaults are documented per endpoint."},"required":false,"description":"Time range for analytics. Defaults are documented per endpoint.","name":"range","in":"query"}],"responses":{"200":{"description":"OK.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClickStats"}}}},"400":{"description":"Validation error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Scope insufficient.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"getBundlesByIdAnalytics"}},"/_/api/v1/bundles/{id}/breakdown":{"get":{"tags":["bundles"],"summary":"Page through a bundle's countries, sources, or domains breakdown","parameters":[{"schema":{"type":"integer","exclusiveMinimum":0,"example":42},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","enum":["countries","referrers","referrer_hosts"],"description":"Which panel to page through: countries, referrers (sources), or referrer_hosts (domains)."},"required":true,"description":"Which panel to page through: countries, referrers (sources), or referrer_hosts (domains).","name":"dimension","in":"query"},{"schema":{"type":"string","enum":["24h","7d","30d","90d","1y","all"],"description":"Time range. Defaults to all-time."},"required":false,"description":"Time range. Defaults to all-time.","name":"range","in":"query"},{"schema":{"type":["integer","null"],"minimum":0,"description":"Row offset into the ranked list. Defaults to 0."},"required":false,"description":"Row offset into the ranked list. Defaults to 0.","name":"offset","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100,"description":"Page size, 1-100. Defaults to 10."},"required":false,"description":"Page size, 1-100. Defaults to 10.","name":"limit","in":"query"}],"responses":{"200":{"description":"OK.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BreakdownPage"}}}},"400":{"description":"Validation error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Scope insufficient.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"getBundlesByIdBreakdown"}},"/_/api/v1/bundles/{id}/links":{"get":{"tags":["bundles"],"summary":"List links in a bundle","description":"Open to every authenticated key, whoever owns the bundle or its links.","parameters":[{"schema":{"type":"integer","exclusiveMinimum":0,"example":42},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"OK.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Link"}}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Scope insufficient.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"getBundlesByIdLinks"},"post":{"tags":["bundles"],"summary":"Add a link to a bundle","description":"Open to any authenticated caller with the `create` scope: adding requires owning neither the bundle nor the link. Idempotent. Removing the link again is owner-gated.","parameters":[{"schema":{"type":"integer","exclusiveMinimum":0,"example":42},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddBundleLinkBody"}}}},"responses":{"200":{"description":"Added.","content":{"application/json":{"schema":{"type":"object","properties":{"added":{"type":"boolean"}},"required":["added"]}}}},"400":{"description":"Validation error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Scope insufficient.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"postBundlesByIdLinks"}},"/_/api/v1/bundles/{id}/links/{linkId}":{"delete":{"tags":["bundles"],"summary":"Remove a link from a bundle","description":"Bundle owner only, whoever owns the link. Owning the link does not grant removal from someone else's bundle. The link itself is not deleted.","parameters":[{"schema":{"type":"integer","exclusiveMinimum":0,"example":42},"required":true,"name":"id","in":"path"},{"schema":{"type":"integer","exclusiveMinimum":0,"example":17},"required":true,"name":"linkId","in":"path"}],"responses":{"200":{"description":"Removed.","content":{"application/json":{"schema":{"type":"object","properties":{"removed":{"type":"boolean"}},"required":["removed"]}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Refused. Either the key lacks the `create` scope, or the caller is not the bundle owner. Ownership is fixed at creation and is not transferable: only the identity that created a bundle can change it. Reading a bundle, its links and its analytics is open to every authenticated key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"deleteBundlesByIdLinksByLinkId"}},"/_/api/v1/pages":{"get":{"tags":["Pages"],"summary":"List the pages this key can write","description":"Every name the key's owner holds that carries a page, with its blocks. An account holds at most five pages, so this is one page of results and takes no paging parameters.","responses":{"200":{"description":"The owner's pages.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Page"}}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The key's scope does not allow this verb.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such page for this key's owner.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The name is retired.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"The request is well formed and the page cannot honour it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"getPages"}},"/_/api/v1/pages/{name}":{"get":{"tags":["Pages"],"summary":"Read one page, and its numbers","description":"The page's fields and every block, in the order a visitor would see them, plus the report the editor draws: views, taps per block, the answers each question holds, the acts the page performs, and the countries and hosts the visits came from. Both doors read the report from one grouped read, so the JSON and the tab cannot disagree. Bots are stored and excluded from every count.","parameters":[{"schema":{"type":"string"},"required":true,"name":"name","in":"path"}],"responses":{"200":{"description":"The page, with its report.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PageWithReport"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The key's scope does not allow this verb.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such page for this key's owner.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The name is retired.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"The request is well formed and the page cannot honour it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"getPagesByName"},"put":{"tags":["Pages"],"summary":"Write a page's fields","description":"The one line, the longer note, the address and the look. The first save creates the page row, which is what publishing requires.","parameters":[{"schema":{"type":"string"},"required":true,"name":"name","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"headline":{"type":"string","maxLength":120},"bio":{"type":"string","maxLength":280},"email":{"type":["string","null"]},"theme":{"type":"string","enum":["ink","paper","midnight","forest","cocoa","sand","mono","rosewater","neon"]},"accent":{"type":"string","enum":["ember","lime","mint","sky","violet","rose","gold","slate"]}}}}}},"responses":{"200":{"description":"The page as written.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The key's scope does not allow this verb.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such page for this key's owner.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The name is retired.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"The request is well formed and the page cannot honour it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"putPagesByName"}},"/_/api/v1/pages/{name}/submissions":{"get":{"tags":["Pages"],"summary":"Read a page's answers","description":"Every answer the page's questions collected in the last 90 days, newest first, each with the block it answers and that block's title as it reads now. The window and the bot rule are the report's own, so a block's `answers` figure and this list count the same rows. An answer whose block was since removed is still listed, with a null title: the answer outlives its question by design.","parameters":[{"schema":{"type":"string"},"required":true,"name":"name","in":"path"}],"responses":{"200":{"description":"The answers in the window, newest first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PageSubmissions"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The key's scope does not allow this verb.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such page for this key's owner.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The name is retired.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"The request is well formed and the page cannot honour it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"getPagesByNameSubmissions"}},"/_/api/v1/pages/{name}/blocks":{"post":{"tags":["Pages"],"summary":"Add a block","description":"A page holds 12 blocks. The first ask is exclusive: adding a block with is_first clears whichever one held it.","parameters":[{"schema":{"type":"string"},"required":true,"name":"name","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"kind":{"type":"string","enum":["link","app","form","image","text"]},"title":{"type":"string"},"subtitle":{"type":"string"},"href":{"type":"string"},"app_id":{"type":"string"},"field":{"type":"string","enum":["email","text","tel"]},"label":{"type":"string"},"consent":{"type":"string"},"r2_key":{"type":"string"},"alt":{"type":"string"},"body":{"type":"string","maxLength":1200},"is_first":{"type":"boolean"},"visible":{"type":"boolean"}}}}}},"responses":{"201":{"description":"The block as written.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PageBlock"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The key's scope does not allow this verb.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such page for this key's owner.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The name is retired.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"The request is well formed and the page cannot honour it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"postPagesByNameBlocks"}},"/_/api/v1/pages/{name}/blocks/{id}":{"patch":{"tags":["Pages"],"summary":"Change one block","description":"Only the keys sent are written. The kind cannot change. Hiding the first ask clears it, because an ask nobody can see is not an ask.","parameters":[{"schema":{"type":"string"},"required":true,"name":"name","in":"path"},{"schema":{"type":["integer","null"]},"required":false,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"kind":{"type":"string","enum":["link","app","form","image","text"]},"title":{"type":"string"},"subtitle":{"type":"string"},"href":{"type":"string"},"app_id":{"type":"string"},"field":{"type":"string","enum":["email","text","tel"]},"label":{"type":"string"},"consent":{"type":"string"},"r2_key":{"type":"string"},"alt":{"type":"string"},"body":{"type":"string","maxLength":1200},"is_first":{"type":"boolean"},"visible":{"type":"boolean"}}}}}},"responses":{"200":{"description":"The block as written.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PageBlock"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The key's scope does not allow this verb.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such page for this key's owner.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The name is retired.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"The request is well formed and the page cannot honour it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"patchPagesByNameBlocksById"},"delete":{"tags":["Pages"],"summary":"Remove one block","description":"The taps a block earned are kept: the report says what happened, and a removed block does not un-happen it.","parameters":[{"schema":{"type":"string"},"required":true,"name":"name","in":"path"},{"schema":{"type":["integer","null"]},"required":false,"name":"id","in":"path"}],"responses":{"204":{"description":"Removed."},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The key's scope does not allow this verb.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such page for this key's owner.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The name is retired.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"The request is well formed and the page cannot honour it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"deletePagesByNameBlocksById"}},"/_/api/v1/pages/{name}/publish":{"post":{"tags":["Pages"],"summary":"Publish a page","description":"The name starts answering at its address with no destination beside it. Refused when the page was never written or has nothing visible.","parameters":[{"schema":{"type":"string"},"required":true,"name":"name","in":"path"}],"responses":{"200":{"description":"The name now answers.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PageState"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The key's scope does not allow this verb.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such page for this key's owner.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The name is retired.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"The request is well formed and the page cannot honour it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"postPagesByNamePublish"}},"/_/api/v1/pages/{name}/unpublish":{"post":{"tags":["Pages"],"summary":"Unpublish a page","description":"The address stops answering and the page is left whole, so publishing again brings the same page back.","parameters":[{"schema":{"type":"string"},"required":true,"name":"name","in":"path"}],"responses":{"200":{"description":"The name stopped answering.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PageState"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The key's scope does not allow this verb.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such page for this key's owner.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The name is retired.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"The request is well formed and the page cannot honour it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"operationId":"postPagesByNameUnpublish"}},"/new":{"get":{"operationId":"getNew","summary":"The intake page","description":"**Public.** The form a stranger opens at `zpe.vc/links/new`: the markup, the caps and the script that posts the drop. No key and no session — this is the one address a person who has never heard of the door is sent to. Any other method on this path is the intake's own 404 page, not a second operation.","tags":["intake"],"x-audience":"public","security":[],"responses":{"200":{"description":"The intake page.","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/new/api/form":{"get":{"operationId":"getNewApiForm","summary":"The form was opened (the dwell anchor's row)","description":"**Public.** The page calls this once when the form first paints. The call writes the dwell anchor's row (a salted hash of the caller, and the first time it was seen) and answers the caps the page shows: the dwell floor, the per-hour send cap and the text cap. It reads nothing back and refuses nothing.","tags":["intake"],"x-audience":"public","security":[],"responses":{"200":{"description":"The caps, and when the anchor was first seen.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/new/api/submit":{"post":{"operationId":"postNewApiSubmit","summary":"One drop: the answers and the files in one body","description":"**Public.** The only write a stranger makes. The browser posts the form (multipart) and a script posts JSON; both reach the same fields and the same file walk. A browser is answered `303` to its receipt page; a script is answered `201` with `{receipt, status}`. A replayed drop under the dedupe window answers `201` with the SAME receipt and writes no second row. Every refusal (`413` cap, `429` per-address, `422` fields) is the app's `{ok:false, code, errors}` body.","tags":["intake"],"x-audience":"public","security":[],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object"}},"application/json":{"schema":{"type":"object"}}}},"responses":{"201":{"description":"The drop was stored. The body carries the receipt and the build's status.","content":{"application/json":{"schema":{"type":"object"}}}},"303":{"description":"A browser's drop: the receipt page, which is where the person waits.","headers":{"location":{"schema":{"type":"string"}}}},"413":{"description":"The body, one file, or the store's whole submission cap was exceeded.","content":{"application/json":{"schema":{"type":"object"}}}},"422":{"description":"A field was missing or over its limit; the body names which.","content":{"application/json":{"schema":{"type":"object"}}}},"429":{"description":"The caller's address is over the per-hour send cap.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/new/api/{receipt}/files":{"post":{"operationId":"postNewApiByReceiptFiles","summary":"The receipt holder adds files after the drop","description":"**Public, with the receipt as the credential.** The receipt page's file picker posts here. The receipt names the build and nothing else authorises the write: a person holding the eight characters is the person the drop belongs to. A file already stored under the same digest is a duplicate and is not written twice.","tags":["intake"],"x-audience":"public","security":[],"parameters":[{"name":"receipt","in":"path","required":true,"description":"The eight-character receipt the submit answered with. It is the only key a stranger holds.","schema":{"type":"string","minLength":4,"maxLength":16,"pattern":"^[A-Za-z0-9]{4,16}$"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"What was stored, what was a duplicate, and what was skipped and why.","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"description":"No build carries that receipt.","content":{"application/json":{"schema":{"type":"object"}}}},"413":{"description":"The file, the part count, or the submission's byte cap was exceeded.","content":{"application/json":{"schema":{"type":"object"}}}},"422":{"description":"No file in the batch could be read.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/new/api/{receipt}/content":{"get":{"operationId":"getNewApiByReceiptContent","summary":"The assembled object (key)","description":"**Key.** The runner reads what the assembler wrote: the extracted content, the build's status and when it moved. Refused with `401` without an `Authorization: Bearer` account key granted `zpe-links`, and with `403` when the grant is read-only.","tags":["intake"],"x-audience":"key","parameters":[{"name":"receipt","in":"path","required":true,"description":"The eight-character receipt the submit answered with. It is the only key a stranger holds.","schema":{"type":"string","minLength":4,"maxLength":16,"pattern":"^[A-Za-z0-9]{4,16}$"}}],"responses":{"200":{"description":"The assembled object and the build's status.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"description":"No `Authorization` header, or one the door refuses. The path needs the app's own account key.","content":{"application/problem+json":{"schema":{"type":"object"}}}},"403":{"description":"The key is valid and its grant does not allow this verb.","content":{"application/problem+json":{"schema":{"type":"object"}}}},"404":{"description":"No build carries that receipt.","content":{"application/problem+json":{"schema":{"type":"object"}}}}}},"post":{"operationId":"postNewApiByReceiptContent","summary":"The assembler writes the object, once (key)","description":"**Key.** The one write that puts the assembled object on the build. It is written once: a second write to a build that already holds content is refused rather than merged.","tags":["intake"],"x-audience":"key","parameters":[{"name":"receipt","in":"path","required":true,"description":"The eight-character receipt the submit answered with. It is the only key a stranger holds.","schema":{"type":"string","minLength":4,"maxLength":16,"pattern":"^[A-Za-z0-9]{4,16}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Written. The body carries the build's new status.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"description":"No `Authorization` header, or one the door refuses. The path needs the app's own account key.","content":{"application/problem+json":{"schema":{"type":"object"}}}},"403":{"description":"The key is valid and its grant does not allow this verb.","content":{"application/problem+json":{"schema":{"type":"object"}}}},"404":{"description":"No build carries that receipt.","content":{"application/problem+json":{"schema":{"type":"object"}}}},"409":{"description":"The build is past the state where content can be written.","content":{"application/problem+json":{"schema":{"type":"object"}}}},"413":{"description":"The object was over the record's byte cap.","content":{"application/problem+json":{"schema":{"type":"object"}}}}}}},"/new/api/{receipt}/message":{"get":{"operationId":"getNewApiByReceiptMessage","summary":"The held message, its sign-off and its attempts (key)","description":"**Key.** Reads the draft that is held for this build: the subject and body, the recipient, the latest sign-off and every send attempt so far. A caller that only wants to see the words can stop here — nothing is sent by a read.","tags":["intake"],"x-audience":"key","parameters":[{"name":"receipt","in":"path","required":true,"description":"The eight-character receipt the submit answered with. It is the only key a stranger holds.","schema":{"type":"string","minLength":4,"maxLength":16,"pattern":"^[A-Za-z0-9]{4,16}$"}}],"responses":{"200":{"description":"The held message, its sign-off and its attempts.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"description":"No `Authorization` header, or one the door refuses. The path needs the app's own account key.","content":{"application/problem+json":{"schema":{"type":"object"}}}},"403":{"description":"The key is valid and its grant does not allow this verb.","content":{"application/problem+json":{"schema":{"type":"object"}}}},"404":{"description":"This build holds no message yet.","content":{"application/problem+json":{"schema":{"type":"object"}}}}}},"post":{"operationId":"postNewApiByReceiptMessage","summary":"Write the held message for a row already read (key)","description":"**Key.** Writes the draft the sender will read. A second post to a build that already holds a message is refused: the draft is written once, and a correction is a new build rather than a quiet rewrite of words somebody may have signed.","tags":["intake"],"x-audience":"key","parameters":[{"name":"receipt","in":"path","required":true,"description":"The eight-character receipt the submit answered with. It is the only key a stranger holds.","schema":{"type":"string","minLength":4,"maxLength":16,"pattern":"^[A-Za-z0-9]{4,16}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"The message the build now holds.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"description":"No `Authorization` header, or one the door refuses. The path needs the app's own account key.","content":{"application/problem+json":{"schema":{"type":"object"}}}},"403":{"description":"The key is valid and its grant does not allow this verb.","content":{"application/problem+json":{"schema":{"type":"object"}}}},"404":{"description":"No build carries that receipt.","content":{"application/problem+json":{"schema":{"type":"object"}}}},"409":{"description":"The build already holds a message, or its recipient is not settled.","content":{"application/problem+json":{"schema":{"type":"object"}}}},"422":{"description":"The message was missing a field the composer needs.","content":{"application/problem+json":{"schema":{"type":"object"}}}}}}},"/new/api/{receipt}/message/sign":{"post":{"operationId":"postNewApiByReceiptMessageSign","summary":"Record a sign-off naming the body and the recipient (key)","description":"**Key.** Records that a person read this exact draft, addressed to this exact recipient: the sign-off names the draft's hash, so a body that moved after the signature no longer matches it. `release` refuses without one.","tags":["intake"],"x-audience":"key","parameters":[{"name":"receipt","in":"path","required":true,"description":"The eight-character receipt the submit answered with. It is the only key a stranger holds.","schema":{"type":"string","minLength":4,"maxLength":16,"pattern":"^[A-Za-z0-9]{4,16}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"The sign-off that is now the newest for this message.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"The sign-off named nobody as the reader.","content":{"application/problem+json":{"schema":{"type":"object"}}}},"401":{"description":"No `Authorization` header, or one the door refuses. The path needs the app's own account key.","content":{"application/problem+json":{"schema":{"type":"object"}}}},"403":{"description":"The key is valid and its grant does not allow this verb.","content":{"application/problem+json":{"schema":{"type":"object"}}}},"404":{"description":"This build holds no message to sign.","content":{"application/problem+json":{"schema":{"type":"object"}}}},"409":{"description":"The message has no recipient, so there is nothing to sign for.","content":{"application/problem+json":{"schema":{"type":"object"}}}}}}},"/new/api/{receipt}/release":{"post":{"operationId":"postNewApiByReceiptRelease","summary":"The app's only send (key, and refused without a sign-off)","description":"**Key.** The one operation in this app that sends mail. It refuses unless the held message carries a sign-off whose draft hash still matches, so a caller cannot post words nobody read. The refusal codes are the contract `tools/mail-release.mjs` turns into exit codes — `409` is 'nobody signed this', not 'the mail service refused it'.","tags":["intake"],"x-audience":"key","parameters":[{"name":"receipt","in":"path","required":true,"description":"The eight-character receipt the submit answered with. It is the only key a stranger holds.","schema":{"type":"string","minLength":4,"maxLength":16,"pattern":"^[A-Za-z0-9]{4,16}$"}}],"responses":{"200":{"description":"The send was attempted; the body carries its outcome.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"description":"No `Authorization` header, or one the door refuses. The path needs the app's own account key.","content":{"application/problem+json":{"schema":{"type":"object"}}}},"403":{"description":"The key is valid and its grant does not allow this verb.","content":{"application/problem+json":{"schema":{"type":"object"}}}},"404":{"description":"This build holds no message to send.","content":{"application/problem+json":{"schema":{"type":"object"}}}},"409":{"description":"The message is unsigned, or its sign-off names a body that has since moved.","content":{"application/problem+json":{"schema":{"type":"object"}}}},"503":{"description":"The mail service or the store could not be reached; nothing was sent.","content":{"application/problem+json":{"schema":{"type":"object"}}}}}}},"/new/r/{receipt}":{"get":{"operationId":"getNewRByReceipt","summary":"The page the submitter waits on","description":"**Public, with the receipt as the credential.** The receipt page: what was received, the files, the held message's state, the address box when the record holds no address, and the confirm link. A receipt nobody holds answers the same page at `404`, so the answer never says whether a receipt exists.","tags":["intake"],"x-audience":"public","security":[],"parameters":[{"name":"receipt","in":"path","required":true,"description":"The eight-character receipt the submit answered with. It is the only key a stranger holds.","schema":{"type":"string","minLength":4,"maxLength":16,"pattern":"^[A-Za-z0-9]{4,16}$"}}],"responses":{"200":{"description":"The receipt page.","content":{"text/html":{"schema":{"type":"string"}}}},"404":{"description":"The same page, saying it is not on this server.","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/new/r/{receipt}/claim":{"post":{"operationId":"postNewRByReceiptClaim","summary":"Keep the published page, once, for the door's principal","description":"**Door.** Unreachable without the door's session: the receipt opens the record, and only a signed-in principal can become its owner — answered `401` with no session, and `503` when the door itself cannot be reached (which is not a refusal, because the person may well be signed in). A browser is sent back to the receipt page with `303`; a script is told `{ok:true, receipt, owner_principal}`. Only a published row nobody has kept can be claimed, once.","tags":["intake"],"x-audience":"door","parameters":[{"name":"receipt","in":"path","required":true,"description":"The eight-character receipt the submit answered with. It is the only key a stranger holds.","schema":{"type":"string","minLength":4,"maxLength":16,"pattern":"^[A-Za-z0-9]{4,16}$"}}],"responses":{"200":{"description":"The row now belongs to the caller's principal.","content":{"application/json":{"schema":{"type":"object"}}}},"303":{"description":"A browser's claim: back to the receipt page.","headers":{"location":{"schema":{"type":"string"}}}},"401":{"description":"No door session.","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"description":"No build carries that receipt.","content":{"application/json":{"schema":{"type":"object"}}}},"409":{"description":"The row is not published, or somebody already owns it.","content":{"application/json":{"schema":{"type":"object"}}}},"503":{"description":"The door could not be reached, so the claim was not decided.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/new/r/{receipt}/email":{"post":{"operationId":"postNewRByReceiptEmail","summary":"The address the page asks for when the record holds none","description":"**Public, with the receipt as the credential.** The person says where their link should go. This recomposes the held draft and writes nothing anybody can send: the message still waits for a read and a sign-off. A browser is sent back to the receipt page with `303`.","tags":["intake"],"x-audience":"public","security":[],"parameters":[{"name":"receipt","in":"path","required":true,"description":"The eight-character receipt the submit answered with. It is the only key a stranger holds.","schema":{"type":"string","minLength":4,"maxLength":16,"pattern":"^[A-Za-z0-9]{4,16}$"}}],"requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"type":"object"}},"application/json":{"schema":{"type":"object"}}}},"responses":{"303":{"description":"The address was recorded; back to the receipt page.","headers":{"location":{"schema":{"type":"string"}}}},"404":{"description":"No build carries that receipt.","content":{"text/html":{"schema":{"type":"string"}}}},"409":{"description":"The message is already signed, or the row is past the ask.","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"That is not an address the app can write to.","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/new/r/{receipt}/confirm":{"get":{"operationId":"getNewRByReceiptConfirm","summary":"The correction page","description":"**Public, with the receipt as the credential.** The page that lets the person fix a field before the press. It is a read: nothing is confirmed until the POST below.","tags":["intake"],"x-audience":"public","security":[],"parameters":[{"name":"receipt","in":"path","required":true,"description":"The eight-character receipt the submit answered with. It is the only key a stranger holds.","schema":{"type":"string","minLength":4,"maxLength":16,"pattern":"^[A-Za-z0-9]{4,16}$"}}],"responses":{"200":{"description":"The correction page.","content":{"text/html":{"schema":{"type":"string"}}}},"404":{"description":"No build carries that receipt.","content":{"text/html":{"schema":{"type":"string"}}}},"409":{"description":"The build is closed; the page says so.","content":{"text/html":{"schema":{"type":"string"}}}}}},"post":{"operationId":"postNewRByReceiptConfirm","summary":"The person's press, which is what confirms","description":"**Public, with the receipt as the credential.** The only act that moves a build to `confirmed`: the page's press, with the corrected fields. It answers `303` back to the receipt page, which is where the published address appears.","tags":["intake"],"x-audience":"public","security":[],"parameters":[{"name":"receipt","in":"path","required":true,"description":"The eight-character receipt the submit answered with. It is the only key a stranger holds.","schema":{"type":"string","minLength":4,"maxLength":16,"pattern":"^[A-Za-z0-9]{4,16}$"}}],"requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"type":"object"}},"application/json":{"schema":{"type":"object"}}}},"responses":{"303":{"description":"Confirmed; back to the receipt page.","headers":{"location":{"schema":{"type":"string"}}}},"404":{"description":"No build carries that receipt.","content":{"text/html":{"schema":{"type":"string"}}}},"409":{"description":"The build is closed, or somebody pressed first.","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"A corrected field was refused; the page says which.","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/new/r/{receipt}/f/{id}":{"get":{"operationId":"getNewRByReceiptFById","summary":"One stored file, streamed through the worker","description":"**Public, with the receipt as the credential.** The bytes of one file on this build, streamed from R2 through the worker so the bucket is never public. The receipt and the file id must belong together: a file id from somebody else's build is not a row and answers `404`.","tags":["intake"],"x-audience":"public","security":[],"parameters":[{"name":"receipt","in":"path","required":true,"description":"The eight-character receipt the submit answered with. It is the only key a stranger holds.","schema":{"type":"string","minLength":4,"maxLength":16,"pattern":"^[A-Za-z0-9]{4,16}$"}},{"name":"id","in":"path","required":true,"description":"The file row's UUID, as the receipt page links it.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The file's own bytes, at the type the row stored.","content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"404":{"description":"That file is not on this server.","content":{"application/problem+json":{"schema":{"type":"object"}}}}}}},"/u/{slug}":{"servers":[{"url":"/"}],"get":{"operationId":"getPublishedPage","summary":"The published page","description":"**Public.** The product the intake exists for: the page a drop becomes after the press and the publish, at `zpe.vc/u/<name>`. No key and no session — this is the address the business prints. It answers at that mount only: measured 2026-11-06, `zpe.vc/u/<name>` is 200 and `zpe.vc/links/u/<name>` is 404, which is why this path item carries its own `servers`. A name that holds no live page answers the signpost's own `404`, never somebody else's page.","tags":["pages"],"x-audience":"public","security":[],"parameters":[{"name":"slug","in":"path","required":true,"description":"The page's published name, which is also its address: `zpe.vc/u/<name>`.","schema":{"type":"string","minLength":1,"maxLength":64}}],"responses":{"200":{"description":"The published page.","content":{"text/html":{"schema":{"type":"string"}}}},"404":{"description":"No live page holds that name.","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/u/{slug}/card.vcf":{"servers":[{"url":"/"}],"get":{"operationId":"getPublishedPageCard","summary":"The published page's card, as a vCard","description":"**Public.** The page's 'save my card' act at `zpe.vc/u/<name>/card.vcf`: the page's own name, headline, address and contact as `text/vcard`, built from the same row the page reads. The read records its row before it answers, so the operator can see the card was taken.","tags":["pages"],"x-audience":"public","security":[],"parameters":[{"name":"slug","in":"path","required":true,"description":"The page's published name, which is also its address: `zpe.vc/u/<name>`.","schema":{"type":"string","minLength":1,"maxLength":64}}],"responses":{"200":{"description":"The card.","content":{"text/vcard":{"schema":{"type":"string"}}}},"404":{"description":"No live page holds that name.","content":{"text/html":{"schema":{"type":"string"}}}}}}}},"webhooks":{},"x-surface":{"app":"zpe-links","base":"/_/api/v1","document":"/_/api/v1/openapi.json","generated_by":"src/api/router.ts getOpenAPI31Document"},"x-zpe-routes":57,"x-zpe-documented":56,"x-zpe-internal":["GET /_/api/v1/openapi.json"]}