{"openapi":"3.1.0","info":{"title":"Strait API","version":"1.0.0","description":"Deep-linking + deferred-match API. Two keys, both in Dashboard → Get started: the **secret key** (`Authorization: Bearer st_live_…`, server-side only) for the link endpoints, and the **publishable key** (`publishableKey: \"st_pub_live_…\"` in the JSON body, safe in apps and websites) for /v1/match, /v1/referrer, /v1/resolve, /v1/open and /v1/event. Rotating the publishable key keeps the previous key working for 7 days; responses to the old key carry `Strait-Key-Deprecated: <ISO expiry>`, and after that it gets 401. Operations marked `x-internal: true` are used by the Strait dashboard or by Stripe and are not part of the public API. Errors are always JSON `{ \"error\": \"…\" }`: malformed JSON bodies get 400, unknown /v1 routes 404."},"servers":[{"url":"https://strait.link"}],"tags":[{"name":"Links","description":"Create, read, update and deactivate short links (secret key, server-side)."},{"name":"Analytics","description":"Taps, app opens, installs and conversions per link, channel, day or platform (secret key)."},{"name":"Referrals","description":"Preview, not switched on yet: referral codes that ride on a tap and come back with the deferred match (secret key for the report)."},{"name":"SDK","description":"Called by the Strait app and web SDKs with the publishable key: deferred matching, link resolution, open reports."},{"name":"Events","description":"Conversion and revenue events, attributed to the tap they came from."},{"name":"Webhooks","description":"Test webhooks, read their delivery history and redeliver."},{"name":"Export","description":"Everything your workspace needs to leave Strait."},{"name":"Tools","description":"Dry runs and public checkers: simulate a tap, check App Links / Universal Links files, read a Live Tap Trace."},{"name":"Billing","description":"Used by the Strait dashboard and by Stripe. Not part of the public API."}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer"}},"schemas":{"WebhookDelivery":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Also sent as X-Strait-Delivery"},"webhookId":{"type":"string","format":"uuid"},"event":{"type":"string","example":"link.clicked"},"status":{"type":"string","enum":["pending","delivered","failed"]},"attempts":{"type":"integer"},"lastStatus":{"type":["integer","null"],"description":"The receiver's last HTTP status"},"lastError":{"type":["string","null"],"example":"HTTP 500 | timeout | unsafe_target | webhook_removed"},"nextAttemptAt":{"type":["string","null"],"format":"date-time","description":"pending only"},"createdAt":{"type":"string","format":"date-time"},"deliveredAt":{"type":["string","null"],"format":"date-time"}}},"Link":{"type":"object","properties":{"id":{"type":"string"},"slug":{"type":"string","description":"Unique among the workspace's active links (another workspace may use the same slug)."},"shortUrl":{"type":"string","format":"uri","description":"The link to share, on your workspace link domain: https://<handle>.strait.link/<slug>. Links live on their workspace's host; the bare root only answers a slug no other workspace uses."},"longUrl":{"type":"string","format":"uri"},"tenantId":{"type":"string","description":"The workspace the link belongs to."},"isActive":{"type":"boolean","description":"false once deactivated (DELETE /v1/links/{slug})."},"iosFallbackUrl":{"type":"string","format":"uri"},"androidFallbackUrl":{"type":"string","format":"uri"},"desktopUrl":{"type":"string","format":"uri"},"description":{"type":"string"},"imageUrl":{"type":"string","format":"uri"},"campaign":{"type":"string","description":"Analytics campaign; defaults to longUrl's utm_campaign."},"variants":{"type":"array","items":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"weight":{"type":"number"}}},"description":"A/B destination variants, when set."},"passwordProtected":{"type":"boolean","description":"Present (true) when the link has a password."},"blockedReason":{"type":"string","description":"Set when the link was disabled under the Acceptable Use Policy."},"title":{"type":["string","null"]},"tags":{"type":"array","items":{"type":"string"}},"expiresAt":{"type":["integer","null"],"description":"epoch ms"},"expiredAction":{"type":["string","null"],"enum":["page","store","url",null],"description":"Where a tap goes once the link has expired. page = the \"This link has expired\" page (410). store = Google Play on Android / the App Store on iPhone (302, from your app configuration; the Play install referrer carries the tap); computers, and phones whose store is not configured, get the expired-link web page, else the expired page. url = 302 to expiredUrl. Absent/null = the workspace setting (Settings → When a link expires; default page)."},"expiredUrl":{"type":["string","null"],"format":"uri","description":"This link's expired-link web page (https); absent = the workspace's."},"referralCode":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,64}$","description":"Preview (referrals not switched on yet). Only in the POST /v1/links answer, and only when the code was saved. Not returned by the list, get or update endpoints yet."}}},"NewLink":{"type":"object","required":["slug","longUrl"],"properties":{"slug":{"type":"string","pattern":"^[a-zA-Z0-9_-]{1,64}$"},"longUrl":{"type":"string","format":"uri","description":"https only. The app receives its path + query as the in-app route."},"iosFallbackUrl":{"type":"string","format":"uri","description":"iPhone without the app: this page instead of longUrl (https)."},"androidFallbackUrl":{"type":"string","format":"uri","description":"Android without the app (website mode): this page instead of longUrl (https)."},"desktopUrl":{"type":"string","format":"uri","description":"Desktop browsers go here instead of longUrl (https)."},"title":{"type":"string","description":"Social preview title."},"description":{"type":"string","description":"Social preview description."},"imageUrl":{"type":"string","format":"uri","description":"Social preview image (https)."},"campaign":{"type":"string","description":"Campaign name for analytics. Defaults to longUrl's utm_campaign when omitted (and follows it when longUrl changes)."},"tags":{"type":"array","items":{"type":"string"}},"expiresAt":{"oneOf":[{"type":"integer","description":"epoch ms"},{"type":"string","format":"date-time"}],"description":"After this, the link stops opening its destination; expiredAction decides where taps go. Omit for never."},"expiredAction":{"type":"string","enum":["page","store","url"],"description":"Where a tap goes once the link has expired. page = the \"This link has expired\" page (410). store = Google Play on Android / the App Store on iPhone (302, from your app configuration; the Play install referrer carries the tap); computers, and phones whose store is not configured, get the expired-link web page, else the expired page. url = 302 to expiredUrl. Omit to use the workspace setting (default page)."},"expiredUrl":{"type":"string","format":"uri","description":"https. Required with expiredAction url; with store it is where computers go. Passes the same unsafe-destination check as longUrl."},"referralCode":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,64}$","description":"Preview (referrals not switched on yet; until then the field is ignored). This link's referral code: 1 to 64 letters, digits, - or _. Every tap carries it unless the tap has its own ?strait_ref=; it comes back as `referralCode` in the deferred-match answer. Don't use an email address or phone number. Not accepted by the batch or update endpoints yet."},"password":{"type":"string","writeOnly":true,"minLength":1,"maxLength":128,"description":"Optional: visitors must enter it before the link opens (link-preview bots still get the preview card). Stored hashed, never returned; the link shows passwordProtected: true."}}},"AnalyticsMetrics":{"type":"object","properties":{"taps":{"type":"integer","description":"Taps on live links (not deferred-match entries, not taps on expired links)."},"billableTaps":{"type":"integer","description":"Of those, the taps that count toward your plan (a repeat of the same tap within seconds is not billed)."},"appOpens":{"type":"integer","description":"App opens reported by the SDKs that came from one of your links."},"installs":{"type":"integer","description":"Installs tied to one of your links (organic installs are not counted). The installs and first-open reports are merged: reports naming the same tap are one install, the same definition as the dashboard."},"conversions":{"type":"integer","description":"POST /v1/event events tied to one of your links (directly or through their tap)."},"revenue":{"type":"object","additionalProperties":{"type":"number"},"description":"Event values summed per currency code (upper case; '' when an event gave none). Currencies are never added together."}}},"AnalyticsRow":{"allOf":[{"$ref":"#/components/schemas/AnalyticsMetrics"},{"type":"object","properties":{"key":{"type":["string","null"],"description":"The group value: a UTC day (YYYY-MM-DD), a channel (whatsapp, instagram, direct…), a link id, or a platform (android, ios, desktop)."},"slug":{"type":["string","null"],"description":"group=link only: the link's slug."}}}]},"AppLinksFinding":{"type":"object","properties":{"id":{"type":"string","example":"android.valid"},"severity":{"type":"string","enum":["ok","warn","error"]},"title":{"type":"string"},"detail":{"type":"string"},"fix":{"type":"string"}}},"MatchResult":{"type":"object","properties":{"matched":{"type":"boolean"},"longUrl":{"type":["string","null"]},"linkId":{"type":["string","null"]},"matchMethod":{"type":"string","enum":["install_referrer","exact_ext","exact_core","scored","clipboard","none"],"description":"How it matched. 'clipboard' = a claimed clipboard-boost handoff token (POST /v1/handoff/claim). 'scored' is reserved for the scored iPhone matcher, which is not enabled yet."},"confidence":{"type":["string","null"],"enum":["high","medium",null],"description":"Scored matches only (matchMethod 'scored'): 'high' or 'medium'. Null for every other answer of /v1/match."},"clickId":{"type":"string","format":"uuid","description":"When matched: the id of the tap this install came from. The SDKs remember it and send it with conversion events (/v1/event `clickId`)."},"referralCode":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,64}$","description":"Preview (referrals not switched on yet). Present only when matched and the tap carried a referral code: the tap's ?strait_ref=, else its link's referralCode. Absent otherwise (never null). The SDKs pass it to your app as `referralCode` on the deferred link (contract B21)."},"reasons":{"type":"array","items":{"type":"string"},"description":"Why the matcher matched or refused, as short codes (never personal data). Exact matcher: exact_ext, exact_core, no_candidate, ambiguous (two identical-looking taps for different links: Strait refuses rather than guesses), race_lost (another lookup already took this tap), device_matching_off (the workspace turned iPhone install matching off). Clipboard boost: clipboard, handoff_unknown, handoff_used, handoff_expired. Scored matches add signal codes (e.g. ipv6_64, ip_exact, asn, lang_full, dt_42s). New codes may be added; treat unknown codes as informational."}}}}},"paths":{"/v1/links":{"get":{"summary":"List links","operationId":"listLinks","tags":["Links"],"description":"Without `limit`/`cursor`: every active link as a JSON array (unchanged). With either: one page `{ links, nextCursor }`, newest first; pass `nextCursor` back as `cursor` until it is null.","security":[{"bearerAuth":[]}],"parameters":[{"name":"limit","in":"query","required":false,"description":"Page size, 1–100 (larger values are capped at 100). Default 50 when paging.","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque `nextCursor` from the previous page.","schema":{"type":"string"}}],"responses":{"200":{"description":"Links (array), or a page when limit/cursor is given","content":{"application/json":{"schema":{"oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/Link"}},{"type":"object","properties":{"links":{"type":"array","items":{"$ref":"#/components/schemas/Link"}},"nextCursor":{"type":["string","null"]}}}]},"example":[{"id":"lnk_5f0c2a8e-3b1d-4f6a-9c7e-2d8b1a0e4f63","tenantId":"0b8f6d2e-7a41-4c39-9e15-3f2a6c8d1b70","slug":"summer","shortUrl":"https://acme.strait.link/summer","longUrl":"https://shop.example.com/sale/summer?utm_source=whatsapp&utm_campaign=summer-2026","isActive":true,"title":"Summer sale","campaign":"summer-2026","tags":["sale"]}]}}},"400":{"description":"Invalid limit or cursor"}}},"post":{"summary":"Create a link","operationId":"createLink","tags":["Links"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NewLink"},"example":{"slug":"summer","longUrl":"https://shop.example.com/sale/summer?utm_source=whatsapp&utm_campaign=summer-2026","title":"Summer sale","tags":["sale"]}}}},"responses":{"201":{"description":"Created: the link, with the shortUrl to share","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Link"},"example":{"id":"lnk_5f0c2a8e-3b1d-4f6a-9c7e-2d8b1a0e4f63","tenantId":"0b8f6d2e-7a41-4c39-9e15-3f2a6c8d1b70","slug":"summer","shortUrl":"https://acme.strait.link/summer","longUrl":"https://shop.example.com/sale/summer?utm_source=whatsapp&utm_campaign=summer-2026","isActive":true,"title":"Summer sale","campaign":"summer-2026","tags":["sale"]}}}},"400":{"description":"Invalid field, or destination_blocked (a URL failed the unsafe-destination check)"},"401":{"description":"Missing or invalid secret key"},"402":{"description":"Plan limit reached (this month's new links, or taps over twice the plan). Existing links keep working. Body: `{ error, reason: \"links\" | \"clicks\", upgradeUrl }` (\"clicks\" = the monthly tap limit)."},"409":{"description":"An active link with this slug already exists in this workspace"}}}},"/v1/links/batch":{"post":{"summary":"Create up to 500 links in one call","operationId":"batchCreateLinks","tags":["Links"],"description":"For imports and migrations. Each item passes the same checks as POST /v1/links (slug rules, https destinations, the unsafe-destination check, plan limits) and gets its own result; a bad item never fails the batch. A slug already used in the workspace (or earlier in the same batch) is a per-item 409, so re-running an import is safe. The response is 200 whenever the request itself is valid: read `results[i].ok`. Items past a plan limit get 402.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["links"],"properties":{"links":{"type":"array","minItems":1,"maxItems":500,"items":{"$ref":"#/components/schemas/NewLink"}}}},"example":{"links":[{"slug":"summer","longUrl":"https://shop.example.com/sale/summer?utm_source=whatsapp&utm_campaign=summer-2026"},{"slug":"winter","longUrl":"https://shop.example.com/sale/winter"}]}}}},"responses":{"200":{"description":"One result per item, in order, plus totals","content":{"application/json":{"schema":{"type":"object","properties":{"results":{"type":"array","items":{"type":"object","properties":{"index":{"type":"integer","description":"Position of the item in `links`."},"ok":{"type":"boolean"},"status":{"type":"integer","description":"What POST /v1/links would have answered: 201, 400, 402 or 409."},"link":{"$ref":"#/components/schemas/Link"},"error":{"type":"string"},"reason":{"type":"string","description":"For destination_blocked and plan limits."}}}},"summary":{"type":"object","properties":{"total":{"type":"integer"},"created":{"type":"integer"},"failed":{"type":"integer"}}}}},"example":{"results":[{"index":0,"ok":true,"status":201,"link":{"id":"lnk_5f0c2a8e-3b1d-4f6a-9c7e-2d8b1a0e4f63","tenantId":"0b8f6d2e-7a41-4c39-9e15-3f2a6c8d1b70","slug":"summer","shortUrl":"https://acme.strait.link/summer","longUrl":"https://shop.example.com/sale/summer?utm_source=whatsapp&utm_campaign=summer-2026","isActive":true,"title":"Summer sale","campaign":"summer-2026","tags":["sale"]}},{"index":1,"ok":false,"status":409,"error":"slug already exists in this workspace"}],"summary":{"total":2,"created":1,"failed":1}}}}},"400":{"description":"`links` is missing, empty, or has more than 500 items"},"401":{"description":"Missing or invalid secret key"}}}},"/v1/links/{slug}":{"get":{"summary":"Get a link","operationId":"getLink","tags":["Links"],"security":[{"bearerAuth":[]}],"parameters":[{"name":"slug","in":"path","required":true,"description":"The link's slug: the part after your link domain, e.g. `summer`","schema":{"type":"string"}}],"responses":{"200":{"description":"The link (with shortUrl)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Link"},"example":{"id":"lnk_5f0c2a8e-3b1d-4f6a-9c7e-2d8b1a0e4f63","tenantId":"0b8f6d2e-7a41-4c39-9e15-3f2a6c8d1b70","slug":"summer","shortUrl":"https://acme.strait.link/summer","longUrl":"https://shop.example.com/sale/summer?utm_source=whatsapp&utm_campaign=summer-2026","isActive":true,"title":"Summer sale","campaign":"summer-2026","tags":["sale"]}}}},"404":{"description":"No active link with this slug in the key's workspace"}}},"patch":{"summary":"Update a link","operationId":"updateLink","tags":["Links"],"description":"Send only the fields to change. null clears expiresAt (never expires), expiredAction (back to the workspace setting) and expiredUrl.","security":[{"bearerAuth":[]}],"parameters":[{"name":"slug","in":"path","required":true,"description":"The link's slug: the part after your link domain, e.g. `summer`","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"longUrl":{"type":"string","format":"uri","description":"https only"},"title":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"expiresAt":{"oneOf":[{"type":"integer","description":"epoch ms"},{"type":"string","format":"date-time"},{"type":"null"}]},"expiredAction":{"type":["string","null"],"enum":["page","store","url",null],"description":"Where taps go once the link has expired (see NewLink); null = the workspace setting."},"expiredUrl":{"type":["string","null"],"format":"uri","description":"https; required when expiredAction is url."}}},"example":{"longUrl":"https://shop.example.com/sale/summer-v2","expiresAt":"2026-12-31T23:59:59Z"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Link"},"example":{"id":"lnk_5f0c2a8e-3b1d-4f6a-9c7e-2d8b1a0e4f63","tenantId":"0b8f6d2e-7a41-4c39-9e15-3f2a6c8d1b70","slug":"summer","shortUrl":"https://acme.strait.link/summer","longUrl":"https://shop.example.com/sale/summer-v2","isActive":true,"title":"Summer sale","campaign":"summer-2026","tags":["sale"],"expiresAt":1798761599000}}}},"400":{"description":"Invalid field, or destination_blocked (a URL failed the unsafe-destination check)"},"404":{"description":"Not found"}}},"delete":{"summary":"Deactivate a link","operationId":"deactivateLink","tags":["Links"],"security":[{"bearerAuth":[]}],"parameters":[{"name":"slug","in":"path","required":true,"description":"The link's slug: the part after your link domain, e.g. `summer`","schema":{"type":"string"}}],"responses":{"204":{"description":"Deactivated"},"401":{"description":"Missing or invalid secret key"},"404":{"description":"No active link with this slug in the key's workspace"}}}},"/v1/analytics":{"get":{"summary":"Read analytics","operationId":"getAnalytics","tags":["Analytics"],"description":"Taps, app opens, installs, conversions and revenue for your workspace in [from, to), for every link or one, optionally grouped by day, channel, link or platform. The numbers use the same definitions as the dashboard. Each item counts at its own time (a tap at its tap time, an install at its install time); days are UTC. Secret key only (a publishable key gets 401). At most 92 days per call and 30 calls a minute per workspace (429 with Retry-After).","security":[{"bearerAuth":[]}],"parameters":[{"name":"from","in":"query","required":false,"description":"Inclusive start: ISO date (2026-10-01, UTC), ISO date-time or epoch ms. Default: 30 days before `to`.","schema":{"type":"string","example":"2026-09-01"}},{"name":"to","in":"query","required":false,"description":"Exclusive end, same formats. Default: now.","schema":{"type":"string","example":"2026-10-01"}},{"name":"link","in":"query","required":false,"description":"Only this link: an active link's slug, e.g. `summer`.","schema":{"type":"string","pattern":"^[a-zA-Z0-9_-]{1,64}$"}},{"name":"group","in":"query","required":false,"description":"Split the numbers into rows. Default none (totals only).","schema":{"type":"string","enum":["none","day","channel","link","platform"],"default":"none"}}],"responses":{"200":{"description":"Totals, plus one row per group value (biggest first; by date for group=day). `rows` is empty for group=none.","content":{"application/json":{"schema":{"type":"object","properties":{"from":{"type":"string","format":"date-time"},"to":{"type":"string","format":"date-time"},"timezone":{"type":"string","enum":["UTC"]},"link":{"type":["string","null"],"description":"The slug asked for, or null for every link."},"group":{"type":"string","enum":["none","day","channel","link","platform"]},"totals":{"$ref":"#/components/schemas/AnalyticsMetrics"},"rows":{"type":"array","items":{"$ref":"#/components/schemas/AnalyticsRow"}}}},"example":{"from":"2026-09-01T00:00:00.000Z","to":"2026-10-01T00:00:00.000Z","timezone":"UTC","link":null,"group":"channel","totals":{"taps":1840,"billableTaps":1795,"appOpens":1210,"installs":312,"conversions":96,"revenue":{"INR":48210}},"rows":[{"key":"whatsapp","taps":1320,"billableTaps":1290,"appOpens":905,"installs":241,"conversions":71,"revenue":{"INR":35400}},{"key":"instagram","taps":380,"billableTaps":368,"appOpens":221,"installs":52,"conversions":19,"revenue":{"INR":10010}},{"key":"direct","taps":140,"billableTaps":137,"appOpens":84,"installs":19,"conversions":6,"revenue":{"INR":2800}}]}}}},"400":{"description":"Bad from / to (or a range over 92 days), unknown group, malformed link"},"401":{"description":"Missing or invalid secret key, or a publishable key"},"404":{"description":"No active link with this slug in the key's workspace"},"429":{"description":"Over 30 calls a minute for this workspace"}}}},"/v1/referrals":{"get":{"summary":"Read the referral report (preview)","operationId":"getReferrals","tags":["Referrals"],"description":"Preview, not switched on yet: until referrals are switched on for the API, this answers 404 like any unknown /v1 route, and the fields may change before then. For each referral code in [from, to): the taps that carried it and the referred installs (an install that opened and matched a tap with that code, at most one per tap). A tap counts at its tap time, a referred install at its first open; days are UTC. Most conversions first. Secret key only (a publishable key gets 401). At most 92 days per call and 30 calls a minute per workspace (429 with Retry-After). Each new referred install also fires the `referral.converted` webhook.","security":[{"bearerAuth":[]}],"parameters":[{"name":"from","in":"query","required":false,"description":"Inclusive start: ISO date (2026-10-01, UTC), ISO date-time or epoch ms. Default: 30 days before `to`.","schema":{"type":"string","example":"2026-10-01"}},{"name":"to","in":"query","required":false,"description":"Exclusive end, same formats. Default: now.","schema":{"type":"string","example":"2026-11-01"}}],"responses":{"200":{"description":"Totals plus one row per referral code","content":{"application/json":{"schema":{"type":"object","properties":{"preview":{"type":"boolean","enum":[true],"description":"Always true while referrals are a preview."},"from":{"type":"string","format":"date-time"},"to":{"type":"string","format":"date-time"},"timezone":{"type":"string","enum":["UTC"]},"totals":{"type":"object","properties":{"codes":{"type":"integer","description":"Codes with at least one tap or referred install in the range."},"taps":{"type":"integer"},"conversions":{"type":"integer","description":"Referred installs."}}},"rows":{"type":"array","items":{"type":"object","properties":{"code":{"type":"string"},"taps":{"type":"integer","description":"Taps that carried this code."},"conversions":{"type":"integer","description":"Referred installs with this code."},"lastTapAt":{"type":["string","null"],"format":"date-time"},"lastConversionAt":{"type":["string","null"],"format":"date-time"}}}}}},"example":{"preview":true,"from":"2026-10-01T00:00:00.000Z","to":"2026-11-01T00:00:00.000Z","timezone":"UTC","totals":{"codes":2,"taps":57,"conversions":14},"rows":[{"code":"ASHA42","taps":41,"conversions":11,"lastTapAt":"2026-10-30T18:12:00.000Z","lastConversionAt":"2026-10-30T18:20:00.000Z"},{"code":"RAVI7","taps":16,"conversions":3,"lastTapAt":"2026-10-28T09:41:00.000Z","lastConversionAt":"2026-10-27T11:05:00.000Z"}]}}}},"400":{"description":"Bad from / to (or a range over 92 days)"},"401":{"description":"Missing or invalid secret key, or a publishable key"},"404":{"description":"Referrals are not switched on"},"429":{"description":"Over 30 calls a minute for this workspace"}}}},"/v1/match":{"post":{"summary":"Deferred-match lookup (first app open after an install)","description":"Called once by the SDKs on the first launch after an install, to find the tap that led to it. On iPhone it compares coarse device signals (screen, language, time zone, network) with recent taps of this workspace and answers only when exactly one tap fits; ambiguous cases are refused, never guessed. Android uses POST /v1/referrer (Play Install Referrer) instead. Rate limited per IP.","operationId":"matchInstall","tags":["SDK"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["publishableKey","platform","screenWidth","pixelRatio","language","timezone"],"properties":{"publishableKey":{"type":"string","example":"st_pub_live_…"},"platform":{"type":"string"},"screenWidth":{"type":"number"},"pixelRatio":{"type":"number"},"language":{"type":"string"},"timezone":{"type":"string"}}},"example":{"publishableKey":"st_pub_live_…","platform":"ios","screenWidth":393,"pixelRatio":3,"language":"en-IN","timezone":"Asia/Kolkata"}}}},"responses":{"200":{"description":"Match result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatchResult"},"example":{"matched":true,"longUrl":"https://shop.example.com/sale/summer?utm_source=whatsapp&utm_campaign=summer-2026","linkId":"lnk_5f0c2a8e-3b1d-4f6a-9c7e-2d8b1a0e4f63","matchMethod":"exact_ext","confidence":null,"clickId":"9a1c7e52-4b3d-4f8e-a6d1-0c2b5e7f9a34","reasons":["exact_ext"]}}}},"400":{"description":"platform and the device fields are required"},"401":{"description":"Missing or invalid publishable key"},"429":{"description":"Rate limited"}}}},"/v1/handoff/claim":{"post":{"summary":"iPhone clipboard boost: claim a one-time handoff token (exact, single use)","operationId":"claimHandoff","tags":["SDK"],"description":"Only when the workspace turned on the clipboard boost and the app set clipboardBoost: true. The token comes from the handoff link https://<link host>/h/<token> that the tap page copied; it belongs to one tap of this workspace, works once and expires after 24 hours. When it does not match, nothing is recorded and the SDK falls back to POST /v1/match with the same openId. Rate limited per IP.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["publishableKey","token"],"properties":{"publishableKey":{"type":"string","example":"st_pub_live_…"},"token":{"type":"string","pattern":"^[A-Za-z0-9_-]{22}$"},"platform":{"type":"string","example":"ios"},"openId":{"type":"string","description":"The open id of this first app open: the claim also reports the open."},"at":{"type":"number","description":"When the app opened (epoch ms)."}}},"example":{"publishableKey":"st_pub_live_…","token":"Qm9yZWFsaXMtaGFuZG9mZg","platform":"ios","openId":"o_lx3k2a_9f8e7d6c5b4a","at":1790000000000}}}},"responses":{"200":{"description":"Claim result: matchMethod clipboard with clickId, or matched:false with reason handoff_unknown, handoff_used, handoff_expired or not_found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatchResult"},"example":{"matched":true,"longUrl":"https://shop.example.com/sale/summer?utm_source=whatsapp&utm_campaign=summer-2026","linkId":"lnk_5f0c2a8e-3b1d-4f6a-9c7e-2d8b1a0e4f63","matchMethod":"clipboard","confidence":null,"clickId":"9a1c7e52-4b3d-4f8e-a6d1-0c2b5e7f9a34","reasons":["clipboard"]}}}},"400":{"description":"token is not a handoff token"},"401":{"description":"missing or invalid publishable key"},"429":{"description":"rate limited"}}}},"/v1/referrer":{"post":{"summary":"Deferred-match resolution from Android Play Install Referrer (deterministic)","operationId":"resolveReferrer","tags":["SDK"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["publishableKey","linkId"],"properties":{"publishableKey":{"type":"string","example":"st_pub_live_…"},"linkId":{"type":"string","description":"strait_link value from the referrer"},"platform":{"type":"string","default":"android"}}},"example":{"publishableKey":"st_pub_live_…","linkId":"lnk_5f0c2a8e-3b1d-4f6a-9c7e-2d8b1a0e4f63","platform":"android"}}}},"responses":{"200":{"description":"Match result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatchResult"},"example":{"matched":true,"longUrl":"https://shop.example.com/sale/summer?utm_source=whatsapp&utm_campaign=summer-2026","linkId":"lnk_5f0c2a8e-3b1d-4f6a-9c7e-2d8b1a0e4f63","matchMethod":"install_referrer","confidence":null,"clickId":"9a1c7e52-4b3d-4f8e-a6d1-0c2b5e7f9a34","reasons":[]}}}},"401":{"description":"Missing or invalid publishable key"}}}},"/v1/resolve":{"post":{"summary":"Turn a short link that opened your app into its destination","operationId":"resolveLink","tags":["SDK"],"description":"Called by the SDKs when a verified short link (App Link / Universal Link) opens the app directly. With `openId` the lookup also records the app open and the tap (once per openId); `recorded:false` means retry the report via /v1/open.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["publishableKey","url"],"properties":{"publishableKey":{"type":"string","example":"st_pub_live_…"},"url":{"type":"string","example":"https://acme.strait.link/summer"},"platform":{"type":"string","example":"android"},"openId":{"type":"string"},"appState":{"type":"string","enum":["closed","background","foreground"]},"firstLaunch":{"type":"boolean"},"at":{"type":"number"}}},"example":{"publishableKey":"st_pub_live_…","url":"https://acme.strait.link/summer","platform":"android","openId":"o_lx3k2a_9f8e7d6c5b4a","appState":"closed","firstLaunch":false}}}},"responses":{"200":{"description":"{ matched, longUrl?, linkId?, slug?, clickId?, reason?, recorded? } — reason: not_found | expired | password_protected. clickId (when matched) is the id of the tap this open recorded; send it with conversion events (/v1/event `clickId`).","content":{"application/json":{"schema":{"type":"object","properties":{"matched":{"type":"boolean"},"longUrl":{"type":"string"},"linkId":{"type":"string"},"slug":{"type":"string"},"clickId":{"type":"string","format":"uuid"},"reason":{"type":"string","enum":["not_found","expired","password_protected"]},"recorded":{"type":"boolean","description":"With openId: whether the open was recorded (false = retry via /v1/open)."}}},"example":{"matched":true,"longUrl":"https://shop.example.com/sale/summer?utm_source=whatsapp&utm_campaign=summer-2026","linkId":"lnk_5f0c2a8e-3b1d-4f6a-9c7e-2d8b1a0e4f63","slug":"summer","recorded":true,"clickId":"9a1c7e52-4b3d-4f8e-a6d1-0c2b5e7f9a34"}}}},"400":{"description":"url is not a short link"},"401":{"description":"Missing or invalid key"}}}},"/v1/open":{"post":{"summary":"Report that a link opened the app","operationId":"reportOpen","tags":["SDK"],"description":"Sent by the Strait SDKs for every link open they cannot report through /v1/resolve, /v1/referrer or /v1/match (browser hand-offs, your own https links), and to retry any open report that failed to send. De-duplicated by `openId`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["publishableKey","openId","kind","route","platform"],"properties":{"publishableKey":{"type":"string","example":"st_pub_live_…"},"openId":{"type":"string","example":"o_lx3k2a_9f8e7d6c5b4a"},"kind":{"type":"string","enum":["direct","deferred"]},"route":{"type":"string","enum":["app_link","custom_scheme","install_referrer","fingerprint"]},"appState":{"type":"string","enum":["closed","background","foreground"]},"platform":{"type":"string","example":"android"},"url":{"type":"string","description":"The opened URL (only host + path are stored)"},"clickId":{"type":"string","description":"The tap id from strait_click"},"linkId":{"type":"string"},"matched":{"type":"boolean"},"reason":{"type":"string"},"firstLaunch":{"type":"boolean"},"at":{"type":"number","description":"When it opened (epoch ms)"}}},"example":{"publishableKey":"st_pub_live_…","openId":"o_lx3k2a_9f8e7d6c5b4a","kind":"direct","route":"custom_scheme","appState":"background","platform":"android","url":"acme://sale/summer","clickId":"9a1c7e52-4b3d-4f8e-a6d1-0c2b5e7f9a34","linkId":"lnk_5f0c2a8e-3b1d-4f6a-9c7e-2d8b1a0e4f63","matched":true,"at":1790000000000}}}},"responses":{"202":{"description":"Recorded (`duplicate: true` when this openId was already recorded)","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"duplicate":{"type":"boolean"}}},"example":{"ok":true,"duplicate":false}}}},"400":{"description":"Invalid fields"},"401":{"description":"Missing or invalid key"}}}},"/v1/billing/checkout":{"post":{"summary":"Start a paid subscription (dashboard)","operationId":"createCheckoutSession","tags":["Billing"],"x-internal":true,"description":"Called by the dashboard with the signed-in user's session token (`Authorization: Bearer <Supabase access token>`). Only a workspace owner may call it. Returns a Stripe Checkout URL to redirect to.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["tenantId","plan"],"properties":{"tenantId":{"type":"string","format":"uuid"},"plan":{"type":"string","enum":["starter","scale"]}}}}}},"responses":{"200":{"description":"`{ url }` — Stripe Checkout"},"400":{"description":"Bad tenantId or plan"},"401":{"description":"No or expired session"},"403":{"description":"Not an owner of this workspace"},"409":{"description":"Already subscribed — use the portal"},"503":{"description":"Billing not configured"}}}},"/v1/billing/portal":{"post":{"summary":"Open the billing portal (dashboard)","operationId":"createBillingPortalSession","tags":["Billing"],"x-internal":true,"description":"Same auth as /v1/billing/checkout. Returns a Stripe Customer Portal URL (card, cancel, invoices).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["tenantId"],"properties":{"tenantId":{"type":"string","format":"uuid"}}}}}},"responses":{"200":{"description":"`{ url }` — Stripe Customer Portal"},"401":{"description":"No or expired session"},"403":{"description":"Not an owner of this workspace"},"409":{"description":"No billing account yet"},"503":{"description":"Billing not configured"}}}},"/v1/billing/webhook":{"post":{"summary":"Stripe webhook receiver","operationId":"receiveStripeWebhook","tags":["Billing"],"x-internal":true,"description":"For Stripe only: verified by the `Stripe-Signature` header; idempotent by event id.","responses":{"200":{"description":"Received"},"400":{"description":"Missing or invalid signature"},"503":{"description":"Billing not configured"}}}},"/v1/event":{"post":{"summary":"Record a conversion / revenue event","operationId":"recordEvent","tags":["Events"],"description":"From an app or website send `publishableKey`; from a server you may instead authenticate with the secret key as a Bearer token. `clickId` names the tap the conversion came from: every tap gets a unique id (a UUID, sent with the tap's redirect and passed to your app with the open), which the mobile SDKs remember from the attributed app open and attach for 7 days. It is kept only when it is a tap of this workspace; otherwise it is ignored and the event is still recorded.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["event"],"properties":{"publishableKey":{"type":"string","example":"st_pub_live_…"},"event":{"type":"string","example":"purchase"},"value":{"type":"number"},"currency":{"type":"string","example":"USD"},"linkId":{"type":"string","description":"Optional: with a known clickId it is filled in from that tap"},"platform":{"type":"string"},"clickId":{"type":"string","format":"uuid","description":"The id of the tap this conversion came from (the mobile SDKs fill it in)"}}},"example":{"publishableKey":"st_pub_live_…","event":"purchase","value":499,"currency":"INR","clickId":"9a1c7e52-4b3d-4f8e-a6d1-0c2b5e7f9a34"}}}},"responses":{"202":{"description":"Accepted. `clickId` echoes the tap id that was stored, or null when none was given or it was not a tap of this workspace.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"clickId":{"type":["string","null"],"format":"uuid"},"linkId":{"type":["string","null"],"description":"The linkId kept for this event: yours if it is one of your links, else the clickId tap's link; null when neither (unknown ids are dropped, the event is still recorded)"}}},"example":{"ok":true,"clickId":"9a1c7e52-4b3d-4f8e-a6d1-0c2b5e7f9a34","linkId":"lnk_5f0c2a8e-3b1d-4f6a-9c7e-2d8b1a0e4f63"}}}},"401":{"description":"Missing or invalid key"}}}},"/v1/tools/app-links":{"get":{"summary":"App Links / Universal Links checker (public tool)","operationId":"checkAppLinks","tags":["Tools"],"description":"Fetches https://<domain>/.well-known/assetlinks.json and /.well-known/apple-app-site-association (plus the legacy /apple-app-site-association) and explains each finding with a severity and a fix. No key needed. Rate-limited (10 per minute per IP); results cached 60 s per domain; CORS only for https://straitlink.in and http://localhost:<port>. Only public domains: IP addresses and names that resolve to private/reserved addresses are refused (400). https only, at most 5 redirects, 5 s, 256 KB. It checks the files on the domain, not how a particular phone behaves.","parameters":[{"name":"domain","in":"query","required":true,"description":"A domain (a full URL is accepted and reduced to its host)","schema":{"type":"string","example":"example.com"}}],"responses":{"200":{"description":"The report: { domain, checkedAt, android: { status, file, findings[] }, ios: { status, file, legacyFile, findings[] }, note }. Each finding is { id, severity: ok|warn|error, title, detail, fix }; each file is { url, finalUrl, status, contentType, size, redirects[], fetchError, parse, body (≤ 32 KB), bodyTruncated }.","content":{"application/json":{"schema":{"type":"object","properties":{"domain":{"type":"string"},"checkedAt":{"type":"string","format":"date-time"},"android":{"type":"object","properties":{"status":{"type":"string"},"file":{"type":"object"},"findings":{"type":"array","items":{"$ref":"#/components/schemas/AppLinksFinding"}}}},"ios":{"type":"object","properties":{"status":{"type":"string"},"file":{"type":"object"},"legacyFile":{"type":"object"},"findings":{"type":"array","items":{"$ref":"#/components/schemas/AppLinksFinding"}}}},"note":{"type":"string"}}},"example":{"domain":"acme.strait.link","checkedAt":"2026-10-01T10:00:00.000Z","android":{"status":"ok","file":{"url":"https://acme.strait.link/.well-known/assetlinks.json","finalUrl":"https://acme.strait.link/.well-known/assetlinks.json","status":200,"contentType":"application/json","size":312,"redirects":[],"fetchError":null,"parse":"ok","body":"[ … ]","bodyTruncated":false},"findings":[{"id":"android.valid","severity":"ok","title":"assetlinks.json is valid","detail":"com.acme.shop with 1 SHA-256 fingerprint.","fix":""}]},"ios":{"status":"ok","file":{"url":"https://acme.strait.link/.well-known/apple-app-site-association","finalUrl":"https://acme.strait.link/.well-known/apple-app-site-association","status":200,"contentType":"application/json","size":204,"redirects":[],"fetchError":null,"parse":"ok","body":"{ … }","bodyTruncated":false},"legacyFile":{"url":"https://acme.strait.link/apple-app-site-association","finalUrl":null,"status":404,"contentType":"text/html","size":0,"redirects":[],"fetchError":null,"parse":"not_parsed","body":null,"bodyTruncated":false},"findings":[{"id":"ios.valid","severity":"ok","title":"apple-app-site-association is valid","detail":"ABCDE12345.com.acme.shop handles every path.","fix":""}]},"note":"This checks the files served by the domain, not how a particular phone behaves."}}}},"400":{"description":"Not a domain, or refused (IP address, local name, private address)"},"422":{"description":"The domain is not in DNS"},"429":{"description":"Rate limited"}}}},"/v1/trace/{st}":{"get":{"summary":"Live Tap Trace (website demo)","operationId":"getTapTrace","tags":["Tools"],"description":"What the engine decided for one tap of the demo link (demo.strait.link/try?st=<token>), readable for 15 minutes. Public, rate-limited (30 burst, 1/s per IP); CORS only for https://straitlink.in and http://localhost:<port>. Never returns the IP, user agent or workspace. Only the demo workspace stores tokens.","parameters":[{"name":"st","in":"path","required":true,"description":"The session token the demo page generated: the `st` value on the demo link it shows","schema":{"type":"string","pattern":"^[a-z0-9]{20,32}$"}}],"responses":{"200":{"description":"The trace","content":{"application/json":{"schema":{"type":"object","properties":{"tappedAt":{"type":"string","format":"date-time"},"platform":{"type":"string","enum":["ios","android","desktop"]},"os":{"type":["string","null"],"example":"Android 14"},"deviceClass":{"type":["string","null"],"enum":["phone","tablet","desktop",null]},"inAppBrowser":{"type":["string","null"],"example":"instagram"},"channel":{"type":["string","null"],"example":"whatsapp"},"decision":{"type":"string","enum":["redirect","interstitial","escape"]},"sentTo":{"type":["string","null"],"enum":["app_or_store","app_or_website","store","website","app_store_page","in_app_browser_page","expired",null]},"clickId":{"type":"string","description":"first 8 characters of the tap id"},"decidedMs":{"type":["integer","null"]},"open":{"type":["object","null"],"properties":{"at":{"type":"string","format":"date-time"},"isNewUser":{"type":"boolean"},"appState":{"type":["string","null"],"enum":["closed","background","foreground",null]}}}}},"example":{"tappedAt":"2026-10-01T10:00:00.000Z","platform":"android","os":"Android 14","deviceClass":"phone","inAppBrowser":"whatsapp","channel":"whatsapp","decision":"escape","sentTo":"in_app_browser_page","clickId":"9a1c7e52","decidedMs":null,"open":{"at":"2026-10-01T10:00:04.000Z","isNewUser":false,"appState":"background"}}}}},"204":{"description":"No tap for this token yet (or it is older than 15 minutes): keep polling"},"404":{"description":"Malformed token"},"429":{"description":"Rate limited"}}}},"/v1/webhooks/{id}/test":{"post":{"summary":"Send a test event to a webhook","operationId":"testWebhook","tags":["Webhooks"],"description":"Delivers a `ping` event to the webhook right now (even if paused) and returns what the receiver answered. Not stored or retried. Every delivery is signed: `X-Strait-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of \"<t>.<raw body>\" with the webhook secret>` — recompute it and refuse timestamps more than 5 minutes old. It is the only signature header. Other headers: X-Strait-Event, X-Strait-Delivery (stable across retries — de-duplicate on it), X-Strait-Attempt. Webhook URLs must be public https endpoints: private, loopback, link-local and CGNAT addresses are refused when saved and again at every delivery (after DNS resolution).","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"The webhook id (Dashboard → Settings → Webhooks)","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The attempt (delivered or not)","content":{"application/json":{"schema":{"type":"object","properties":{"delivered":{"type":"boolean"},"status":{"type":["integer","null"],"description":"Receiver's HTTP status"},"error":{"type":["string","null"],"example":"HTTP 500 | timeout | unsafe_target"},"body":{"type":["string","null"],"description":"First 500 characters of the response"},"durationMs":{"type":"integer"},"deliveryId":{"type":"string"},"event":{"type":"string","enum":["ping"]}}},"example":{"delivered":true,"status":200,"error":null,"body":"ok","durationMs":182,"deliveryId":"test_4f1a9c2e","event":"ping"}}}},"401":{"description":"Missing or invalid secret key"},"404":{"description":"No such webhook in the key's workspace"}}}},"/v1/simulate":{"get":{"summary":"Dry-run a tap (playground)","operationId":"simulateTap","tags":["Tools"],"description":"What a tap on `url` with this user agent would do — the exact decision the link makes — without recording a tap, firing webhooks or counting toward your plan. Authenticate with your secret key (Bearer) or `publishableKey`; the link must belong to that workspace. Hand-off URLs carry a placeholder tap id (00000000-0000-4000-8000-000000000000).","security":[{"bearerAuth":[]}],"parameters":[{"name":"url","in":"query","required":true,"description":"The full short link, e.g. https://you.strait.link/summer","schema":{"type":"string","format":"uri"}},{"name":"ua","in":"query","required":true,"description":"A preset (android, iphone, ipad, desktop, instagram-ios, instagram-android, facebook-android, whatsapp-android, bot) or a raw User-Agent","schema":{"type":"string"}},{"name":"publishableKey","in":"query","required":false,"description":"Instead of a secret key: the workspace publishable key","schema":{"type":"string"}}],"responses":{"200":{"description":"The decision","content":{"application/json":{"schema":{"type":"object","properties":{"decision":{"type":"string","enum":["redirect","interstitial","escape","preview","expired","blocked"],"description":"preview = link-preview bots and requests without a browser user agent: the preview card, never a redirect, never billed"},"sentTo":{"type":["string","null"],"enum":["app_or_store","app_or_website","store","website","app_store_page","in_app_browser_page","expired",null]},"location":{"type":["string","null"],"description":"Redirect target, or the page's main button target (App Store / \"Open in app\" intent)"},"expiredTo":{"type":"string","enum":["page","store","url"],"description":"decision expired only: where the tap goes (page = 410 expired page; store / url = 302 to location)"},"expiredSetting":{"type":"object","description":"decision expired only: the setting that applied","properties":{"action":{"type":"string","enum":["page","store","url"]},"from":{"type":"string","enum":["link","workspace"]}}},"reason":{"type":"string","example":"App not on Google Play, Automatic chose website: opens the app if installed, otherwise the website"},"platform":{"type":"string","enum":["ios","android","desktop"]},"deviceClass":{"type":"string","enum":["phone","tablet","desktop"]},"inAppBrowser":{"type":["string","null"]},"isBot":{"type":"boolean","description":"true for link-preview bots and requests without a browser user agent (scripts, curl, no user agent): they get the preview page, counted as a preview, never billed"},"userAgent":{"type":"string"},"slug":{"type":"string"},"passwordProtected":{"type":"boolean"},"abTest":{"type":"boolean"}}},"example":{"decision":"redirect","sentTo":"app_or_store","location":"intent://sale/summer#Intent;scheme=https;package=com.acme.shop;end","reason":"App on Google Play: opens the app if installed, otherwise the Play Store","platform":"android","deviceClass":"phone","inAppBrowser":null,"isBot":false,"userAgent":"Mozilla/5.0 (Linux; Android 14; Pixel 8) …","slug":"summer","passwordProtected":false,"abTest":false}}}},"400":{"description":"Missing/invalid url or ua"},"401":{"description":"No valid key"},"404":{"description":"No such link in the key's workspace"}}}},"/v1/export":{"get":{"summary":"Export everything","operationId":"exportData","tags":["Export"],"description":"Everything your workspace needs to leave Strait, streamed as a download. Auth: a secret key (a publishable key gets 401), or `token` from POST /v1/export/token (one use, 60 seconds; this is how the dashboard downloads). `part=links`: NDJSON, one link per line (active and inactive). `part=taps`: CSV of every tap joined to its first app open and install, oldest first, no date cap. `part=events`: CSV of every conversion / revenue event, oldest first; channel, campaign and variant come from the tap the event is attributed to (when it has a click_id). `part=webhooks`: JSON of your webhook configuration (url, events, active, createdAt) — signing secrets are never exported. `part=worker`: a single-file Cloudflare Worker (`export default { fetch }`) that keeps your active links redirecting by device (Android / iPhone-iPad / desktop) without Strait.","security":[{"bearerAuth":[]}],"parameters":[{"name":"part","in":"query","required":true,"description":"What to export: `links` (NDJSON), `taps` (CSV), `events` (CSV), `webhooks` (JSON) or `worker` (JavaScript). Optional with `token` (the token carries it).","schema":{"type":"string","enum":["links","taps","events","webhooks","worker"]}},{"name":"from","in":"query","required":false,"description":"taps / events: inclusive start, ISO date or epoch ms","schema":{"type":"string"}},{"name":"to","in":"query","required":false,"description":"taps / events: exclusive end, ISO date or epoch ms","schema":{"type":"string"}},{"name":"token","in":"query","required":false,"description":"A one-time download token from POST /v1/export/token, instead of the Authorization header","schema":{"type":"string"}}],"responses":{"200":{"description":"The export, as a download (Content-Disposition: attachment)","content":{"application/x-ndjson":{"schema":{"type":"string"},"example":"{\"id\":\"lnk_5f0c2a8e-…\",\"slug\":\"summer\",\"longUrl\":\"https://shop.example.com/sale/summer\",\"active\":true}\n"},"text/csv":{"schema":{"type":"string","description":"taps columns: click_id, clicked_at, link_id, slug, source, platform, device_class, os_major, in_app_browser, channel, sent_to, campaign, variant, billable, opened_at, open_kind, open_route, app_state, is_new_user, opens, installed_at, install_matched, match_method. sent_to: app_or_store | app_or_website | store | website | app_store_page | in_app_browser_page | expired (a tap on an expired link: answered 410, billable=false). channel: the tap URL utm_source, else the destination's utm_source, else the in-app browser (whatsapp, instagram…), else the referrer. events columns: created_at, name, value, currency, platform, link_id, slug, click_id, channel, campaign, variant."}},"application/json":{"schema":{"type":"object","description":"part=webhooks","properties":{"exportedAt":{"type":"string","format":"date-time"},"webhooks":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"url":{"type":"string"},"events":{"type":"array","items":{"type":"string"}},"active":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"}}}}}}},"application/javascript":{"schema":{"type":"string"}}}},"400":{"description":"Unknown part, a bad from / to, or a token issued for another part"},"401":{"description":"Missing or invalid secret key, or an expired / used token"}}}},"/v1/export/token":{"post":{"summary":"Get a one-time export download link","operationId":"createExportToken","tags":["Export"],"description":"Issues a token for one download of one export part: GET /v1/export?part=…&token=… works once, within 60 seconds, with no Authorization header — so a browser can save a large export directly to disk. Only the token hash is stored. Used by the dashboard (a signed-in owner or admin); a secret key works too.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["part"],"properties":{"part":{"type":"string","enum":["links","taps","events","webhooks","worker"]},"from":{"type":"string","description":"taps / events: inclusive start"},"to":{"type":"string","description":"taps / events: exclusive end"}}},"example":{"part":"taps","from":"2026-09-01","to":"2026-10-01"}}}},"responses":{"201":{"description":"The token","content":{"application/json":{"schema":{"type":"object","properties":{"token":{"type":"string","example":"stx_…"},"part":{"type":"string"},"expiresAt":{"type":"string","format":"date-time"},"path":{"type":"string","example":"/v1/export?part=events&token=stx_…"}}},"example":{"token":"stx_…","part":"taps","expiresAt":"2026-10-01T10:01:00.000Z","path":"/v1/export?part=taps&token=stx_…"}}}},"400":{"description":"Unknown part or a bad from / to"},"401":{"description":"Missing or invalid secret key"}}}},"/v1/webhooks/{id}/deliveries":{"get":{"summary":"List a webhook's deliveries","operationId":"listWebhookDeliveries","tags":["Webhooks"],"description":"Delivery history, newest first: every event sent to this webhook with its status (`pending` = waiting for a retry, `delivered`, `failed` = gave up after 6 attempts), attempts, the last HTTP status or error, and when the next retry is due. Payloads are not returned. History is kept 30 days.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"The webhook id","schema":{"type":"string","format":"uuid"}},{"name":"limit","in":"query","required":false,"description":"1–100, default 20","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"nextCursor from the previous page","schema":{"type":"string"}}],"responses":{"200":{"description":"A page of deliveries","content":{"application/json":{"schema":{"type":"object","properties":{"deliveries":{"type":"array","items":{"$ref":"#/components/schemas/WebhookDelivery"}},"nextCursor":{"type":["string","null"]}}},"example":{"deliveries":[{"id":"3c9e1a7b-5d2f-4e8a-b6c1-9f0d2e4a7b58","webhookId":"7f3e2c1a-9b4d-4e8f-a6c5-2d1b0e9f8a73","event":"link.clicked","status":"failed","attempts":6,"lastStatus":500,"lastError":"HTTP 500","nextAttemptAt":null,"createdAt":"2026-10-01T10:00:00.000Z","deliveredAt":null}],"nextCursor":null}}}},"400":{"description":"Bad limit or cursor"},"401":{"description":"Missing or invalid secret key"},"404":{"description":"No such webhook in the key's workspace"}}}},"/v1/webhook-deliveries/{id}/redeliver":{"post":{"summary":"Redeliver a webhook delivery","operationId":"redeliverWebhookDelivery","tags":["Webhooks"],"description":"Re-queues one delivery (typically `failed`, after all retries) and sends it right away to the webhook's current URL, with the same X-Strait-Delivery id. Its attempt count starts over, so it gets the full retry schedule again. Refused (409) when it was already delivered, or while it is being sent (younger than 2 minutes, or claimed by a worker right now).","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"The delivery id (from the deliveries list)","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The delivery after the new attempt","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookDelivery"},"example":{"id":"3c9e1a7b-5d2f-4e8a-b6c1-9f0d2e4a7b58","webhookId":"7f3e2c1a-9b4d-4e8f-a6c5-2d1b0e9f8a73","event":"link.clicked","status":"delivered","attempts":1,"lastStatus":200,"lastError":null,"nextAttemptAt":null,"createdAt":"2026-10-01T10:00:00.000Z","deliveredAt":"2026-10-01T10:05:00.000Z"}}}},"401":{"description":"Missing or invalid secret key"},"404":{"description":"No such delivery in the key's workspace"},"409":{"description":"Already delivered, or being sent right now"}}}}}}