# Shortn

> Workspace link management, QR codes, campaigns, bio pages and analytics.

API access is subject to workspace rollout, key scopes and current role permissions.

## Documentation

- [Create links in bulk](https://shortn.at/docs/bulk-links.md)
- [Exports](https://shortn.at/docs/exports.md)
- [Getting started with the Shortn API](https://shortn.at/docs/getting-started.md)
- [CSV imports](https://shortn.at/docs/imports.md)
- [Shortn developer documentation](https://shortn.at/docs.md)
- [Link rules](https://shortn.at/docs/link-rules.md)
- [MCP setup and implementation status](https://shortn.at/docs/mcp.md)
- [QR exports](https://shortn.at/docs/qr-exports.md)
- [Outgoing webhooks](https://shortn.at/docs/webhooks.md)
- [Understanding analytics](https://shortn.at/docs/en/guides/analytics/understanding-analytics.md)
- [Upgrading plans](https://shortn.at/docs/en/guides/billing/upgrading-plans.md)
- [Creating bio pages](https://shortn.at/docs/en/guides/bio-pages/creating-bio-pages.md)
- [Customizing bio pages](https://shortn.at/docs/en/guides/bio-pages/customizing-bio-pages.md)
- [Organizing campaigns](https://shortn.at/docs/en/guides/campaigns/organizing-campaigns.md)
- [Getting started with Shortn](https://shortn.at/docs/en/guides/getting-started.md)
- [Shortn guides](https://shortn.at/docs/en/guides.md)
- [Creating links](https://shortn.at/docs/en/guides/links/creating-links.md)
- [Managing links](https://shortn.at/docs/en/guides/links/managing-links.md)
- [UTM parameters](https://shortn.at/docs/en/guides/links/utm-parameters.md)
- [Creating QR codes](https://shortn.at/docs/en/guides/qr-codes/creating-qr-codes.md)
- [Managing QR codes](https://shortn.at/docs/en/guides/qr-codes/managing-qr-codes.md)
- [Compreender as estatísticas](https://shortn.at/docs/pt/guides/analytics/understanding-analytics.md)
- [Mudar de plano](https://shortn.at/docs/pt/guides/billing/upgrading-plans.md)
- [Criar páginas bio](https://shortn.at/docs/pt/guides/bio-pages/creating-bio-pages.md)
- [Personalizar páginas bio](https://shortn.at/docs/pt/guides/bio-pages/customizing-bio-pages.md)
- [Organizar campanhas](https://shortn.at/docs/pt/guides/campaigns/organizing-campaigns.md)
- [Primeiros passos no Shortn](https://shortn.at/docs/pt/guides/getting-started.md)
- [Guias do Shortn](https://shortn.at/docs/pt/guides.md)
- [Criar links](https://shortn.at/docs/pt/guides/links/creating-links.md)
- [Gerir links](https://shortn.at/docs/pt/guides/links/managing-links.md)
- [Parâmetros UTM](https://shortn.at/docs/pt/guides/links/utm-parameters.md)
- [Criar códigos QR](https://shortn.at/docs/pt/guides/qr-codes/creating-qr-codes.md)
- [Gerir códigos QR](https://shortn.at/docs/pt/guides/qr-codes/managing-qr-codes.md)
- [Entender las estadísticas](https://shortn.at/docs/es/guides/analytics/understanding-analytics.md)
- [Cambiar de plan](https://shortn.at/docs/es/guides/billing/upgrading-plans.md)
- [Crear páginas bio](https://shortn.at/docs/es/guides/bio-pages/creating-bio-pages.md)
- [Personalizar páginas bio](https://shortn.at/docs/es/guides/bio-pages/customizing-bio-pages.md)
- [Organizar campañas](https://shortn.at/docs/es/guides/campaigns/organizing-campaigns.md)
- [Primeros pasos con Shortn](https://shortn.at/docs/es/guides/getting-started.md)
- [Guías de Shortn](https://shortn.at/docs/es/guides.md)
- [Crear enlaces](https://shortn.at/docs/es/guides/links/creating-links.md)
- [Gestionar enlaces](https://shortn.at/docs/es/guides/links/managing-links.md)
- [Parámetros UTM](https://shortn.at/docs/es/guides/links/utm-parameters.md)
- [Crear códigos QR](https://shortn.at/docs/es/guides/qr-codes/creating-qr-codes.md)
- [Gestionar códigos QR](https://shortn.at/docs/es/guides/qr-codes/managing-qr-codes.md)
- [Shortn API reference](https://shortn.at/docs/api.md)
- [Interactive API reference](https://shortn.at/docs/api)
- [OpenAPI specification](https://shortn.at/openapi.json)
- [Complete documentation](https://shortn.at/llms-full.txt)

---

# Create links in bulk

`POST /v1/links/bulk` accepts a JSON object with a `links` array of 1–1,000
link creation inputs. The entire request body must fit within 1 MiB. Use a key
with `links:write` whose creator can create resources in the workspace.
Each row supports the same destination, domain, key, title, description, tags and
password options, validation and plan gates as `POST /v1/links`.

```sh
curl https://api.shortn.at/v1/links/bulk \
  -H "Authorization: Bearer $SHORTN_API_KEY" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: campaign-launch-001' \
  --data '{"links":[{"url":"https://example.com/first","tags":["Launch"]},{"url":"https://example.com/second","tags":["Launch"]}]}'
```

## Results and atomicity

A successful request returns HTTP 201 with `count` and `items` in the input order.
Each item has the same shape as a newly created individual link. Destination
safety starts pending, with a durable background scan request committed alongside
each link. The worker scans new destinations and retries failures; configured
safety and password gates still apply at redirect time. If there are enabled
subscribers, each link produces its own `link.created` event.

The batch commits in one Mongo transaction. Invalid input, taken or duplicate
keys, a missing plan feature, insufficient quota, or a database/event write failure
creates no batch links or new tags. A workspace quota check covers the entire
batch and is serialized with other v2 link and QR backing-link creation requests.
Keys are checked again inside the transaction; database uniqueness handles racing
inserts. Foreign-workspace tags are never reused, including when legacy owner
fields happen to match.

Errors use `application/problem+json`. Validation or key errors may include a
`context.index` identifying the zero-based input row. HTTP 409 `limit_reached`
includes `context.limit`, `context.used` and `context.requested` so the integration
can show the exact remaining capacity. A validation failure in the request envelope
returns HTTP 400; an oversized body returns HTTP 413.

## Retries

Send a unique `Idempotency-Key` for each intended batch and retain it for retries.
The original status and response are retained for 24 hours. Reusing the key with a
different request returns HTTP 409 `idempotency_conflict`. An uncertain in-flight
operation returns `idempotency_pending`; it is not reissued automatically.
Feature flags, membership, role and scopes are checked before replaying a response.

This endpoint creates a bounded batch. CSV upload, mapping, dry runs, whole-import
quota reservation and resumable background imports remain separate work.


---

# Exports

The local backend supports incremental CSV/JSON encoding, the shared Links view
filters, Mongo snapshot cursors and durable private-object-store jobs. Private dashboard HTTP delivery and in-page review, progress and history are implemented locally. Analytics job encoding, workers and private HTTP delivery are also implemented locally; the analytics dashboard is connected locally; full-workspace archive jobs/private HTTP and ready-email outbox are implemented locally, with their dashboard and email sign-in return connected locally; this document does not claim complete P6 export coverage.

`EXPORTS_ENABLED` defaults to `0` in web, API and worker schemas. Enabled startup
requires `S3_ENDPOINT`, `S3_REGION`, `S3_BUCKET`, `S3_ACCESS_KEY_ID` and
`S3_SECRET_ACCESS_KEY`; production uses HTTPS. Deploy the `export_jobs` indexes
and private 24-hour object lifecycle before enabling. The worker pumps bounded
pages of due jobs into BullMQ and closes its own queue clients on shutdown.

Core validates filter shapes and reads current membership and plan entitlement.
`csvExport` gates CSV and JSON; a campaign-scoped export also requires
`campaignExport`. Workspace/legacy ownership, hidden and QR exclusions, trash,
search, tags, campaign, status and date behavior come from the same filter helper
as Links browsing. The exported fields are destination URL, key, title, tag
names, domain, expiry, created time and click total. Password hashes, safety
internals, user identifiers and signing keys are never export columns.

For fewer than 5,000 rows, `prepareLinkExport` returns a **server-only** selection
of at most 4,999 IDs. Pass it to `streamFilteredLinkExport`; never send the
selection to the browser or use a client-side library fetch. The bounded IDs
prevent concurrent inserts/filter changes growing the instant response past the
threshold. A Mongo snapshot cursor streams batches of 500, preserving a stable
view through concurrent updates. Authorization and plan reads run outside that
snapshot on each batch; abort closes the cursor/session.

At 5,000 rows or more, preparation creates a 24-hour export job. A request UUID
deduplicates retries for the same workspace/creator/format/filter; changing the
request under that UUID conflicts. The worker streams a complete CSV or JSON
file to private storage with a one-GiB transport bound, renews the fenced lease,
updates processed-row counts and retries transient transport failures. Recovery
restarts encoding into a fresh attempt object. No partial object is signed.
Only an unexpired completed job's own `exports/jobs/links/{workspace}/{job}/`
object can be delivered. Results expire from job creation, and signed delivery
never extends the remaining lifetime. Cancellation or a replaced lease prevents
the obsolete worker from publishing. Creators and current workspace admins/owners
may inspect jobs; membership/plan access is checked again at delivery.

CSV declares `shortn_format=shortn-csv-v1`. Formula-looking values are protected
with an apostrophe and literal apostrophes doubled; the importer reverses this
only for declared Shortn rows. Tags use JSON arrays within CSV cells so embedded
commas/semicolons remain lossless. The URL remains complete rather than splitting
UTM parameters, preserving duplicate parameters and fragments. Ordinary CSV
input keeps its previous behavior. Existing import file/row/field limits apply
to exported files being imported again.

Local integration evidence lives in `apps/worker/src/imports.integration.test.ts`:
actual worker round trip into a new workspace, 4,999/5,000 boundary, frozen instant
selection, snapshot consistency, abort/current membership and plan checks,
private signing/expiry, retry and idempotency, cancellation, stale-lease recovery,
and a real BullMQ queue. Parser/format tests cover formula safety and reversible
tags/quotes/apostrophes. All services in those tests are synthetic/loopback. Real
bucket CORS/signing/multipart/lifecycle and staging acceptance remain separate gates.

## Private dashboard HTTP delivery

These routes use the dashboard session cookie, not public API keys. All responses
are private/no-store with Cookie variance and nosniff. The independent web export
flag and current membership/plan access gate every operation.

| Route | Behavior |
| --- | --- |
| `POST /api/link-exports/{workspace}` | Metadata-only preparation; returns 201 with a download URL for <5,000 selected rows or 202 with safe job metadata and a status URL for larger views. |
| `GET /api/link-exports/{workspace}/download/{ref}` | Authenticated attachment stream for the creator-bound frozen selection; no browser library payload. |
| `GET /api/link-exports/{workspace}/jobs/{id}` | Authenticated safe progress/terminal metadata, with private signed delivery only for a valid completed job. |
| `POST /api/link-exports/{workspace}/jobs/{id}` | `{ "action": "cancel" }`; repeated cancellation is idempotent, and completed/failed jobs return 409. |

Preparation accepts JSON `{ format: "csv" | "json", filter?: { ... }, requestId?:
UUID }`. Filter names match core search/tags/campaign/status/createdAfter/
createdBefore/trash/sort; dates accept ISO date-time strings. JSON bodies are
bounded to 32 KiB even without Content-Length. Unknown fields/operators,
malformed JSON and inappropriate content types fail before preparation.
Preparation/cancellation require an exact Origin matching the configured dashboard
origin; missing or foreign origins return 403. Authentication returns 401, missing
workspaces/tokens/jobs 404, absent export entitlement 403, idempotency conflicts
409, oversized metadata 413 and wrong content type 415.

Instant selections remain in durable Redis under a workspace/creator/ref key for
15 minutes. Responses contain only a ref URL, format, count and expiry; the
4,999-or-fewer IDs and complete filter never reach the browser. Downloads validate
the current session, token lifetime, membership and export entitlement again.
The pull-based response preserves backpressure and closes/aborts the cursor on
request abort or response cancellation. Attachments use fixed safe filenames.
HEAD requests perform the same access/token checks without opening a data cursor.
These private endpoints do not extend the public REST contract listed in spec 10.

`apps/web/e2e/link-exports-http.local.ts` runs against the loopback P6 sandbox and
owns a synthetic private object store and real BullMQ worker. It verifies an actual
4,999-row Chromium attachment, reversible CSV bytes, concurrent insertion without
selection growth, a 5,000-row queued JSON artifact, private response headers,
origin/body-shape boundaries, a valid foreign workspace, creator-bound token expiry,
request deduplication, cancellation/retry/completed-state behavior and current plan
revocation. Evidence is saved in `.impeccable/review/p6-link-export-http/evidence.json`.
Transport unit tests verify body bounds, exact origin matching, pull backpressure,
request abort and iterator cleanup. No external provider writes are involved.


## Dashboard review, progress and history

The Links header opens `/{workspace}/links/export` with the current search, tags,
status, created-date, trash and sort scope. Pagination and row selection do not
narrow this whole-filter export. The review shows the total and 25 rows per page,
grouped by creation date, using the Links controls and divided list styling.
Tag suggestions are capped at 50 and search on the server; selected names remain
available when suggestions change. The main Links page uses the same bounded
suggestions. No complete link library is fetched into the browser.

Choose CSV or JSON and prepare the export once. Small exports show a download
link valid for 15 minutes. Background exports open a status page with processed
counts, cancellation and private download on completion. Polling pauses while
the page is hidden and preserves retryable failures; invalid access removes any
signed download. History shows unexpired background jobs, 25 per page, with
status and format filters. Members/viewers see their own jobs; current workspace
admins/owners can inspect workspace jobs. Expired jobs and plan/access loss have
in-page recovery actions. The pages are translated into English, Portuguese and
Spanish and use the existing light/dark themes.

`apps/web/e2e/link-exports-ui.local.ts` verifies a synthetic 5,000-link/103-tag
library, filter preservation, whole-filter 27-row CSV/JSON downloads across two
review pages, duplicate-submit prevention, remote tag search outside the first
50 suggestions, empty state, cancellation, actual BullMQ worker delivery,
30-job history pagination/filtering, expiry, plan loss and 390px bounds. Captures
and assertions are in `.impeccable/review/p6-link-export-ui/evidence.json`.
This is local dashboard evidence; analytics/workspace exports and external
storage/staging acceptance remain open.


## Analytics export jobs

Analytics exports always use durable jobs: aggregate CSV, or raw CSV/NDJSON.
`POST /api/analytics-exports/{workspace}` accepts only metadata:

```json
{
  "requestId": "00000000-0000-4000-8000-000000000001",
  "query": {
    "mode": "raw",
    "format": "ndjson",
    "scope": { "type": "workspace" },
    "from": "2026-10-01T00:00:00Z",
    "to": "2026-10-15T00:00:00Z",
    "timezone": "UTC",
    "interval": "day",
    "filters": {},
    "dimensions": []
  }
}
```

The response is 202 with safe job metadata and an API `statusUrl`.
`GET /api/analytics-exports/{workspace}/jobs/{id}` reads progress and completed
private delivery. POST to that job with `{ "action": "cancel" }` cancels a
pending/running job; cancellation retries succeed, while completed/failed jobs
return 409. GET the collection path for 25-row history, with `page`, `state`,
`format` and `mode` filters. These private dashboard routes use session cookies,
exact Origin checks on mutations, a 32 KiB metadata body bound and the same
private/no-store headers and current creator/admin visibility as link exports.
They are not public bearer-authenticated REST operations.

Owned link, QR, tag and campaign scopes and filter semantics share analytics
resolution. `csvExport` gates either format and `campaignExport` additionally
gates campaign scope. Preparation clamps the lower bound to the plan window and
the upper bound to request time. That effective window is retained during a plan
upgrade; current membership, scope and entitlement are rechecked outside the
snapshot during batches, worker heartbeats and delivery. A downgrade that narrows
the prepared window fails closed. Window age is anchored at request time, so
ordinary elapsed execution time does not continuously move the export's start.

Raw rows explicitly contain timestamp, event kind, key, country, city, continent,
device, browser, OS, referrer, language, branch and five UTM values. Event/tenant/
actor IDs, IP hashes, raw IP and user agents are excluded. NDJSON preserves the
original values; CSV protects spreadsheet formula prefixes. Raw cursors use
500-row batches. All sections of one aggregate file and raw traversal share a
Mongo snapshot session, including on the native time-series collection. Cursors
and sessions close on completion, failure and abort.

Aggregate CSV columns are `section,dimension,value,period,clicks,scans,uniques`.
It contains a total, populated time buckets in the requested timezone/interval,
and every group for each requested plan-allowed dimension. It does not inherit
the dashboard's top-row cap. Empty ranges include a zero total. Two-stage grouping
counts distinct visitors without building one unbounded visitor array; Mongo may
spill grouping to disk. Visitor hashes never appear in the file.

Analytics jobs use a separate kind and queue in the export collection; link
workers and endpoints explicitly exclude that kind. Due/history indexes include
kind. The fenced, renewable worker writes a unique attempt object below
`exports/jobs/analytics/{workspace}/{job}/`, with the existing one-GiB transport
bound, five transient retries and 24-hour lifecycle. Only a completed current
attempt is signed, for at most its remaining lifetime; cancellation or a replaced
lease prevents stale publication. Worker startup/pump/shutdown use the existing
independent `EXPORTS_ENABLED` flag, which remains off by default.

`apps/worker/src/analytics-exports.integration.test.ts` verifies native time-series
snapshot consistency, more than 1,000 aggregate groups, bounded raw traversal,
DTO redaction/formula safety, retention/plan changes, retries, cancellation,
replaced/expired leases, current roles, kind/tenant isolation, expiry, pagination
and a real BullMQ pump. `apps/web/e2e/analytics-exports-http.local.ts` verifies the
actual authenticated Next routes and 1,005-row private downloads in all three
mode/format combinations. Its synthetic evidence is in
`.impeccable/review/p6-analytics-export-backend/http-evidence.json`.
The analytics dashboard and breakdown pages now open in-page job review rather
than fetching an entire CSV response into browser memory. Full-workspace export, real storage/staging acceptance and
other P6 deliverables remain open.


## Analytics dashboard

Analytics and full-breakdown Export actions open `/{workspace}/analytics/export`,
preserving browser-local dates/timezone, interval, owned campaign scope, link/kind
and event filters. A breakdown entry retains that requested dimension. The page
shows the effective retained window, a current all-matching event count and
readable scope/filter values. It submits that bounded window, so a plan upgrade
before preparation cannot widen the reviewed range. The form offers aggregate
CSV and raw CSV/NDJSON; aggregate groups are disclosed only in aggregate mode.
Raw exports with no matching events are disabled; a valid empty aggregate view
can still export zero totals. Dates wholly outside retention have no prepare
action until the user changes the view.

Preparation opens in-page progress with written-row counts and indeterminate
activity, cancellation and a completed private download. Polling pauses while
hidden, uses one request at a time and removes delivery links on expired or lost
access. History filters mode/state/format on the server with 25-row pages.
The UI uses the shared Page, native selectors, divided rows, FilterBar and
Pagination with English, Portuguese and Spanish in both themes. It introduces
no library picker or complete browser library/event fetch. The older private
`/api/export/{workspace}` endpoint remains present for compatibility, but the
dashboard no longer calls it.

`apps/web/e2e/analytics-exports-ui.local.ts` exercises synthetic 5,000-link/101-tag
libraries with 1,005 matching events, all three mode/format downloads through a
real worker, breakdown entry, scope preservation, duplicate submits, cancellation,
30-job history pagination/filtering, empty/expired/plan states and 1440/390px
layouts. Evidence is in `.impeccable/review/p6-analytics-export-ui/evidence.json`.


## Full-workspace archive jobs

`POST /api/workspace-exports/{workspace}` accepts only an optional request UUID,
for example `{ "requestId": "00000000-0000-4000-8000-000000000001" }`.
It returns 202 safe job metadata and an API status URL. GET the collection for
25-row history (`page`, `state`); GET `jobs/{id}` for status and completed private
delivery; POST `jobs/{id}` with `{ "action": "cancel" }` to cancel. The routes use
session cookies, exact mutation Origin checks, a 32 KiB metadata limit and
private/no-store headers. Completed/failed cancellation returns 409, cancellation
retries succeed, unknown/expired jobs return 404 and current-role refusal is 403.
These are private dashboard routes rather than public REST API operations.

Authorization requires both the current workspace owner ID and a current owner
membership; a stale session role is insufficient. This portability export has no
paid `csvExport` or analytics-window gate. A workspace may have **one active
archive**; a different request while it is pending/running returns 409
`export_in_progress`. A partial unique index enforces that limit across requests.
Accepted UUID retries return the original job. Expired active rows release the
slot before eventual TTL deletion. Install the new active/mail indexes before
enabling the worker. The one-GiB transport bound applies to the complete ZIP;
size failure never exposes a partial download.

The ZIP contains 18 fixed-name entries:

- `workspace.json` and versioned `manifest.json` with record counts.
- JSON and CSV for links, QR codes, bio pages, retained bio-page versions,
  campaigns, tags and domains.
- `analytics.json` and `analytics.csv`, containing aggregate totals, populated
  daily UTC buckets and every dimension over all stored canonical workspace
  events. No visitor hash or raw event payload is exported.

Native and legacy personal-workspace ownership follow the same core predicates
as browsing. Hidden, QR-backing and trashed links are included. Link JSON retains
rules, social preview, UTM and reference metadata; link CSV uses the existing
reversible `shortn-csv-v1` format. QR JSON/CSV preserves stored design options,
including legacy gradients. Bio exports retain normalized draft/published content,
versions and complete owned legacy link/configuration data, so editor migration
limits do not truncate the archive. Foreign link references are omitted.
Complex CSV cells contain JSON and spreadsheet formula prefixes are protected.
Images remain referenced by URL; binary images/QR render files are not bundled.
Auth/session records, password hashes, signing credentials, provider verification
secrets and raw visitor data are excluded through explicit DTOs/projections.

A single Mongo snapshot covers workspace metadata and every resource/aggregate
file. Cursors use 500-row batches and close with their session on EOF, failure or
abort. ZIP output drains after each source chunk, preserving storage backpressure
instead of accumulating a library/archive in memory. Current ownership is checked
outside the snapshot between batches and on the renewable fenced worker lease.
Cancellation, ownership loss, replaced leases and expiry prevent publication.
Retries use a fresh attempt object under
`exports/jobs/workspace/{workspace}/{job}/`; results and signatures never outlive
the job's 24 hours from request. Startup/pump/shutdown use `EXPORTS_ENABLED`, still
off by default, with the existing private S3 configuration and job lifecycle.

Ready email has a separate leased outbox in the job. Storage completion remains
available if mail fails; transport retries do not regenerate the ZIP. Sending
requires the requester to remain the current owner and have a current verified
auth-account email. Revoked ownership suppresses delivery. Localized en/pt/es
HTML/text uses the shared mail layout and a stable provider idempotency key.
The email points to an authenticated workspace export page, so forwarding an email
does not give a download capability. Current-owner access and expiry are checked
again before signing the file. Configure `APP_URL`, `RESEND_API_KEY`, `MAIL_FROM`
and the intended `MAIL_ALLOWLIST`; absent/unavailable mail transport leaves a
retryable pending notification. The ready link opens the authenticated job page;
signing in preserves the exact validated workspace-export destination. Incoming
headers cannot supply an alternate return path.

`apps/worker/src/workspace-exports.integration.test.ts` verifies a 5,001-link ZIP,
a 2,501-link legacy bio, all content categories, one cross-file snapshot during
writes, secret/tenant isolation, all stored aggregates on a free plan, owner
changes, concurrent active requests/expiry, running cancellation, retries and
stale leases, separate mail retry/idempotency and a real BullMQ pump.
`apps/web/e2e/workspace-exports-http.local.ts` verifies actual session HTTP and an
18-entry private S3 ZIP, metadata bounds, free-owner access, current-role refusal,
kind/tenant isolation, UUIDs, cancellation/history/expiry. Evidence is in
`.impeccable/review/p6-workspace-export-backend/http-evidence.json`.

### Workspace dashboard

The current owner sees Export workspace in General settings. The in-page review
at `/{workspace}/settings/workspace/export` explains archive scope, exclusions,
the 1 GiB limit, one-active-job limit and 24-hour expiry. A live job replaces
Prepare with Continue export. Submission uses a stable request UUID and a local
duplicate guard; no library rows are fetched into the browser.

`/{workspace}/settings/workspace/exports/{id}` shows truthful phase/row progress,
cancellation, expiry, private ZIP download and separate pending/sent/failed email
state. Polling pauses in hidden tabs and continues at a lower frequency for a
completed archive whose email is pending. A completed download remains available
while email retries; expired or unauthorized reads remove it. Manual actions
invalidate stale poll responses. History at `.../exports` uses shared filters,
divided list rows and server-side 25-row pagination.

`apps/web/e2e/workspace-exports-ui.local.ts` verifies an isolated free workspace
with 5,001 links, 100 tags and 2,501 legacy bio references, real BullMQ/private
synthetic S3 generation and download of all 18 ZIP entries, duplicate submission,
active-job continuation, cancellation, history pages/state filtering, retry,
owner loss and expiry. A fake notifier verifies pending-to-sent polling and the
signed-out email → sign-in → exact job → ZIP flow without real provider mail.
Desktop/mobile en/pt/es light/dark evidence and design review live in
`.impeccable/review/p6-workspace-export-ui/`. Helper tests reject external,
traversing and malformed return paths. Real email/R2/staging acceptance and
remaining P6 requirements stay open.


---

# Getting started with the Shortn API

Shortn manages short links, QR codes, campaigns, tags, bio pages and domains in a workspace.
The API is being rolled out behind workspace feature flags. An API key does not enable access to a disabled feature.

## Authentication

Create a key in **Settings → API keys**. Only workspace owners and admins can manage keys.
Copy the secret when it is created; it is shown once. Give each integration the scopes it needs.
The key's permissions are intersected with its creator's current workspace role.
Revocation, membership removal and role changes apply to subsequent requests.

Send `Authorization: Bearer sk_live_…` over HTTPS to `https://api.shortn.at`.
Session cookies do not authenticate REST requests. Never put secrets in URLs or client-side code.

```sh
curl https://api.shortn.at/v1/workspace \
  -H "Authorization: Bearer $SHORTN_API_KEY"
```

## Create a link

Use a key with `links:write`. Keep the same `Idempotency-Key` when retrying a POST.
Use a new key for a different operation. A reused key with a different payload is rejected.

```sh
curl https://api.shortn.at/v1/links \
  -H "Authorization: Bearer $SHORTN_API_KEY" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: my-integration-create-001' \
  --data '{"url":"https://example.com","title":"Example"}'
```

## Pagination and filters

List endpoints return bounded pages. Use the returned `nextCursor` as the next request's
`cursor`, preserving the other filters. A missing next cursor ends the list.
The reference documents each endpoint's supported `limit`, search and filter parameters.
For a large link library, use tag and campaign filters alongside keyword search;
keep selections by stable link ID across pages. Do not download the whole library to populate a picker.

## Expand related metadata

Link lists and all link lookup routes accept `?expand=tags,campaign`.
Each link keeps its compact `tags` and `utm` fields. Optional full records appear
in `expanded.tags` and `expanded.campaign`. Both are arrays: a link can have
multiple tags and campaigns, and repeated references are returned once.

Requesting tags requires `tags:read`; campaigns requires `campaigns:read`, in
addition to `links:read`. A missing scope returns 403. Unknown expansion names
return 400. Omit `expand` to keep the compact response and avoid the related reads.
Expansion joins only the requested page (at most 200 links), with workspace
isolation and legacy reference support. Deleted, merged without a surviving
alias, or foreign-workspace references are omitted. Campaign link counts are
computed without returning the campaign's full link ID list.

```sh
curl 'https://api.shortn.at/v1/links?limit=50&expand=tags,campaign' \
  -H "Authorization: Bearer $SHORTN_API_KEY"
```

## Errors and retries

Errors use `application/problem+json` with a stable `code`. Dates are ISO strings and IDs are strings.
Check HTTP status and `code`; do not parse human-readable error text.
Rate limits apply per key and expose `RateLimit-*` headers. On a 429, respect `Retry-After`.
Retry transient failures with backoff, preserving the idempotency key for POST requests.
Feature, role and scope failures require configuration changes rather than repeated retries.

See the [API reference](/docs/api), its [markdown version](/docs/api.md),
and the [webhook guide](/docs/webhooks).


---

# CSV imports

The link and QR import backend and dashboard are implemented locally and disabled by default. A local filtered-CSV round trip through the actual import worker is verified; the in-page filtered export UI is also implemented locally; external acceptance remains open. There are no public import REST routes yet.

## Worker configuration

Set `IMPORTS_ENABLED=1` on the web app and worker only after provisioning private storage and
job indexes. Enabled startup requires `S3_ENDPOINT`, `S3_REGION`, `S3_BUCKET`,
`S3_ACCESS_KEY_ID` and `S3_SECRET_ACCESS_KEY`; production storage must use HTTPS.
Run `bun run db:indexes` and use the existing durable Redis configuration. The
worker discovers bounded pages of Mongo-backed jobs into BullMQ and recovers
expired leases after a restart. The flag defaults to `0`.

Input objects under `imports/{workspaceId}/{importId}.csv` must remain private.
Presigned PUT grants expire in 15 minutes and sign the exact length, CSV type,
private cache control and conditional no-overwrite header. The provider must
accept these headers through browser CORS. The real bucket's signing, conditional
PUT and lifecycle behavior still needs staging acceptance. Error CSV output under
`exports/jobs/imports/` requires a 24-hour lifecycle and private signed delivery.

## Pipeline

Core creates an actor/workspace-bound link or QR upload job with a 24-hour lifetime. Inspection
reads at most 10 MiB in the worker, parses at most 50,000 rows and snapshots source
rows in 500-row transactions. A SHA-256 source hash prevents mixed snapshots after
an upload changes during recovery. Poll views contain column names, row count and
only the first 20 rows, with preview cells bounded to 200 characters.

Mapping supports destination (`url`, `destination`, `long_url`), key (`key`, `slug`,
`code`), title, tags, UTM fields, expiry and domain. Ambiguous headers need an
explicit choice. A mapping change creates a new result generation; it cannot
change an import that is already committing.

Dry runs validate every row through the same URL, key, reserved-name, domain and
plan-feature checks as ordinary link creation. They detect duplicate custom keys
within the file, retain row-specific failures and stream a formula-safe error CSV.
No links, tags or quota reservations are created by a dry run. The ready summary
shows valid/error counts by code and advisory quota availability; commit rechecks
capacity before writing.

Commit reserves link or QR capacity for the whole valid set before any imported resource is
created. A refusal reports exact used, reserved, requested and available numbers.
Other v2 creates and imports respect this hold. Commits create at most 500 rows per
transaction through the shared creation pipeline. Link/tag writes, safety metadata,
mutation events, capacity consumption, row identities, receipt and job checkpoint
commit together. QR imports also insert the QR document and hidden backing link
in that transaction, with the standard default design. They charge QR quota and
preserve mapped keys, custom domains, destination/UTM, title, tags and expiry.
Retried chunks return stable resource IDs without duplicating either document.

Each committed receipt is invalidated again and warmed through the redirect
service's shared loader and generation-aware Redis cache, with at most 16 parallel
loads. A separate warming checkpoint resumes work after a post-commit cache
failure. Cache fills retain their normal fail-open behavior during outages;
required invalidation errors remain retryable. QR scan
identity is resolved by backing-link ID and legacy public-ID fallback, so identical
keys on different domains cannot select another QR's scan identity.

Lease checks occur inside the chunk transaction. Cancellation releases only
remaining capacity; an obsolete lease cannot write more rows. The worker checks
the creator's current membership and creation permission during every stage and
before each chunk. Terminal infrastructure failures release remaining holds.
Committed rows remain counted as usage. A commit whose rows are all created
finishes its completion event rather than accepting a late cancellation.

`import.completed` and the completed job state share a transaction. A failed
outbox write retries completion without recreating links. Only import identifiers
and counts enter the completion payload. Status and error delivery recheck current
workspace access, and signatures cannot outlive the job.

## Legacy coexistence

Deploy the updated legacy application and v2 writer code before enabling imports
while both applications accept creates. They share a stable legacy-identity lock
and the workspace mutation lock, including a legacy create that begins before a
personal workspace is provisioned. The legacy adapter uses its own Mongoose
connection/session; it does not mix BSON values or sessions from another driver.

Accepting an import sets an internal workspace marker that makes document-based
v2 quota authoritative for subsequent legacy link and QR creates. This continues
after the hold is consumed, released or expires, so legacy meter lag cannot admit
writes beyond the period limit. Legacy plan checks remain in place; accounts that
have never accepted an import retain their earlier behavior. Held capacity stays
separate for links and QRs, and refusals preserve the existing `plan-limit` result
with exact quota details.

The two legacy writes that create a QR/backing pair now share the quota transaction.
A failed QR write cannot leave an orphan backing link. Title/tag reads, password
hashing, cache invalidation and usage delivery stay outside the transaction.

## Verification and remaining gates

Local tests use real Mongo transactions, cache Redis and durable BullMQ with
synthetic private storage. They cover dry runs/errors, mapping generations,
whole-import quota refusal, cancellation, permission loss, changed input and
completion-event retries. A child worker is killed after 500 of 1,001 rows commit;
a replacement completes the job with exactly 1,001 rows and three receipts. The
same process-kill test covers QR imports and verifies 1,001 complete QR/backing
pairs, unique link references and QR-only usage. Rollback tests reject a QR insert
after backing rows are staged and verify that rows, tags, receipt, capacity and
checkpoint all roll back. Custom-domain tests cover view/design/export data,
cached scan identity, edit/delete and a same-key record on another domain.

A complete 50,000-link local run (inspection, dry run and commit) finished in
38.6 seconds with cache warming and synthetic private storage. This does not satisfy
the required staging acceptance. Real bucket CORS/conditional PUT/lifecycle and staging rollout remain
open. The full P6 goal is still active.

## Dashboard

Links and QR library headers open `/{workspace}/imports/new?kind=links|qr`.
The in-page form uploads CSV bytes directly to a private signed PUT; server
actions accept only byte count and resource kind. Pasted URLs are converted to
the same CSV format in the browser. Upload progress, cancellation and errors stay
in the form. Signing failures remove the unused upload job, and upload-completion
retries reuse the existing job.

`/{workspace}/imports/{id}` provides column mapping, the bounded 20-row preview,
full dry-run review, private error CSV download, exact quota numbers, explicit
creation of all valid rows, cancellation and background progress. After a dry
run, mapping and source preview are expandable so row results remain easy to
reach. Polling only runs during active phases while the tab is visible. An expiry
refresh removes the job and signed attachment after its lifetime.

History at `/{workspace}/imports` and row results both paginate on the server at
25 per page. Members/viewers see their own jobs; owners/admins may inspect other
workspace jobs. Only the creator with current create permission can upload,
remap or commit. Cancellation uses current resource-delete permission. Every
server action authenticates workspace access again. Row filters support literal
keyword search, server-searched tags, valid/invalid state, specific errors and
grouping by error. Tag suggestions are capped at 50; row labels and source column
names are bounded. Storage keys, worker leases, source cells and full result
sets are excluded from dashboard responses.

The web flag defaults to `0`, independently of `QR_EXPORTS_ENABLED`. Enabled web
startup requires all five private S3 settings and HTTPS in production. Deploy the
new indexes before enabling. English, Portuguese and Spanish use the existing
Page, FilterBar, list and Pagination controls; all flows are in-page.

Run the synthetic local browser harness against the P6 sandbox:

```sh
bun --no-env-file apps/web/e2e/imports.local.ts
```

It owns loopback private storage and a real BullMQ import worker, checks actual
conditional browser PUT headers, CSV and pasted URL input, mapping-error recovery,
dry runs without creation, error CSV download, row/history pagination, tags,
quota refusal, cancellation, and successful link/QR creation. Captures and
assertion evidence live in `.impeccable/review/p6-import-ui/`. A separate Mongo
integration check verifies bounded 25-row responses for a 50,000-row result set,
actor/workspace isolation, expiry, literal searches, tag caps and upload retries.
These checks do not establish real-provider or staging acceptance.

## Shortn export round trips

Shortn CSV exports declare `shortn_format=shortn-csv-v1` on every row. Values that
could become spreadsheet formulas receive a leading apostrophe; literal leading
apostrophes are doubled. The importer removes that protection only for declared
Shortn rows. Tags in this format are a JSON array inside the quoted CSV cell,
so commas, semicolons, quotes, Unicode and apostrophes inside tag names survive.
Unknown/blank declared versions and malformed tag arrays are rejected. Ordinary
third-party CSV keeps its existing delimiter-based tags and literal values.

Destinations retain their complete query and fragment, including repeated UTM
parameters; export does not split/rewrite them into separate UTM columns. A local
integration test exports a filtered source workspace, frees its global key, and
imports the CSV into a different workspace through inspection, dry run and commit.
It verifies destination, available key, tags, title, UTM and expiry. Existing
10 MiB/50,000-row import limits still apply to exported files being re-imported.


---

# Shortn developer documentation

Manage workspace links, QR codes, campaigns, tags, bio pages and domains through the Shortn API.
API access and outgoing webhooks are being rolled out behind workspace feature flags.

- [Getting started](/docs/getting-started): authentication, requests, pagination and errors.
- [Link schedule and query rules](/docs/link-rules): activation, expiry, fallback, forwarding and UTM.
- [Bulk link creation](/docs/bulk-links): atomic batches, quota errors and retries.
- [Interactive API reference](/docs/api): endpoint schemas and request examples.
- [API reference in markdown](/docs/api.md): all implemented endpoints and schemas.
- [Outgoing webhooks](/docs/webhooks): subscriptions, signatures, delivery and retries.
- [MCP integration status](/docs/mcp): availability and setup status.
- [OpenAPI specification](/openapi.json): machine-readable API contract.

Every documentation page also has a `.md` version. Use [llms.txt](/llms.txt)
for a compact index or [llms-full.txt](/llms-full.txt) for the complete documentation.


---

# Link rules

Link creation, bulk creation and editing accept a `rules` object. Link reads return
configured rules with UTC ISO timestamps. `PATCH /v1/links/{id}` replaces the whole
rules object when `rules` is supplied; send `rules: null` to clear it. Omit the
field to preserve existing rules. Link write scopes and current workspace roles apply.

```json
{
  "rules": {
    "activeFrom": "2026-11-01T08:00:00.000Z",
    "expiresAt": "2026-12-01T08:00:00.000Z",
    "expiredUrl": "https://example.com/campaign-ended",
    "maxClicks": 10000,
    "forwardQuery": true,
    "utm": { "source": "newsletter", "campaign": "November" },
    "permanent": false
  }
}
```

## Schedule and expiry

Activation is inclusive: the link starts redirecting at `activeFrom`. Expiry is
exclusive: at `expiresAt` it stops redirecting to its destination. When both are
present, activation must precede expiry. The redirect checks these timestamps
on every request, including a cache hit.

Rule mutations also persist versioned invalidation requests. The worker uses
BullMQ delayed jobs to evict canonical and alias cache entries at activation and
expiry boundaries. Edited or cleared rules supersede older jobs. Requests remain
in Mongo until invalidation succeeds, so queue publication and cache failures can
be retried. Timestamp checks remain authoritative during worker or Redis outages.

Safety and password checks run first. Without a fallback, scheduled links return
a branded 404 page and expired links return a branded 410 page in the visitor's
language. With `expiredUrl`, either time gate returns a 302 to that URL. Gate and
fallback responses do not count as destination clicks. Query forwarding, fixed
UTM and permanent redirect settings do not apply to the fallback.

Fallbacks must be public HTTP(S) destinations and pass the same structural
validation as a link destination. Safety scans include the fallback and keep the
highest risk verdict. Changing a fallback marks safety pending, preserves existing
blocks, and prevents a scan for the previous fallback from applying afterward.

## Approximate visit limit

`maxClicks` accepts an integer from zero through 9,007,199,254,740,991. It limits
lifetime non-bot destination visits, including QR scans and persisted visits from
before the limit was configured. Zero closes the link immediately. Safety, password and
time gates run before the count check; targeting and rotation run afterward.

The redirect reads a durable Redis hot counter seeded from persisted click and
scan totals. Tracked human visits increment it with the analytics stream write;
bots and gate responses do not increment it. Legacy QR links without an analytics
record still count allowed human visits. Counters have no idle expiry, including
when no analytics record exists to seed them again. After counter loss, tracked
links seed from their latest persisted totals. Untracked legacy QR visits from
before counting began, or lost counters without persisted history, cannot be
reconstructed.

This is an approximate limit: concurrent requests, queued analytics writes,
spool replay and persistence lag can overshoot it. Label it “about” in interfaces.
It does not count unique visitors. A missing counter service returns a retryable
503 for a limited destination rather than bypassing the limit. Existing safety,
password and time gates remain usable without that service.

At the limit, visitors receive a localized branded 410 page, or a 302 to
`expiredUrl` when configured. That fallback does not count, forward query
parameters, apply UTM settings or set a rotation cookie. Permanent redirects can
be cached by clients and bypass future server checks; use the default 302 for
links whose visit limit needs to be checked on returning visits.

## Query forwarding and UTM

`forwardQuery` defaults to false. When true, incoming parameters are appended to
the destination, with these rules:

- Existing destination parameters take precedence over incoming values.
- Repeated incoming values are preserved when the destination has no such key.
- `token`, `q`, `ref` and `block` are reserved for Shortn and are not forwarded.
- At most 50 parameters are appended, with keys capped at 100 characters and
  values at 500 characters.
- Fixed `utm.source`, `medium`, `campaign`, `term` and `content` overwrite both
  destination and incoming UTM values. Each configured value is capped at 200
  characters. Empty values do not add a parameter.

Destination fragments are preserved. Click events record the effective UTM
values applied to the redirect while retaining the existing attribution flow.

## App deep links

`deepLink` selects an app link for matching iOS or Android user agents, after
safety, password, time and visit-limit gates and before device/country targeting
or rotation:

```json
{
  "rules": {
    "deepLink": {
      "ios": "youtube://watch?v=123",
      "android": "https://www.youtube.com/watch?v=123",
      "fallback": "store",
      "store": {
        "ios": "https://apps.apple.com/app/youtube/id544007664",
        "android": "https://play.google.com/store/apps/details?id=com.google.android.youtube"
      }
    }
  }
}
```

Configure at least one OS target. App links accept public HTTPS URLs or these
app schemes: `youtube`, `spotify`, `whatsapp`, `tg`, `instagram`, `fb`, `twitter`,
`linkedin`, `slack`, `discord`, `zoomus`. Other handlers, executable schemes,
credentials, whitespace and control characters are rejected. App and store URLs
are capped at 2,048 characters. Public HTTPS app links and store URLs participate
in safety scans; custom app schemes cannot be inspected by HTTP safety providers.

`fallback: "web"` uses the link's main destination. `fallback: "store"` requires
an explicit HTTPS store URL for every configured OS, under `apps.apple.com` for
iOS and `play.google.com` for Android. It never guesses a store listing. Unknown
OS values and recognized bots continue through the ordinary targeting rules.

A matching visit returns a localized, private, non-cacheable HTML handoff with
an automatic app-open attempt and visible app/fallback links. If the page remains
visible, it navigates to the fallback after about 1.8 seconds. Becoming hidden,
losing focus or leaving the page cancels that timer; returning does not start it
again. The app button retries and starts a new timer. Without JavaScript both
links remain usable. Browsers may require a tap or confirmation before opening
an app; an installed app cannot be reliably detected from the web page. HTTPS
universal/app links navigate directly and use the OS/browser's handling.

Query forwarding and fixed UTM apply to the app and fallback URL. A handoff
records one click/scan with `m.branch` of `deep:ios` or `deep:android`; this measures
the handoff attempt, not confirmed app installation or launch. Browser fallback
and retry controls do not create extra Shortn events. Deep handoffs do not set a
rotation cookie or use a permanent redirect, regardless of `permanent`.

## Device and country targeting

Configure up to ten ordered `device` rules and ten ordered `geo` rules:

```json
{
  "rules": {
    "device": [
      { "os": "ios", "device": "mobile", "url": "https://example.com/ios" },
      { "device": "mobile", "url": "https://example.com/mobile" }
    ],
    "geo": [
      { "country": "PT", "url": "https://example.com/portugal" },
      { "country": "ES", "url": "https://example.com/spain" }
    ]
  }
}
```

Safety, password and time gates run before targeting. The first matching device
rule wins; if none matches, the first matching country rule wins. Otherwise the
link's main destination is used. An OS and device in the same rule must both
match. Each device rule must specify at least one of them. Supported OS values
are `ios`, `android`, `windows`, `macos`; devices are `mobile`, `tablet`, `desktop`.
Device/OS comes from the request user agent and country from the trusted
Cloudflare `CF-IPCountry` header. Country rules use uppercase two-letter codes.
Unknown OS/country values do not match those predicates.

Every target URL must pass public HTTP(S) structural validation. All configured
targets are included in safety scans, even when they are not selected for the
current visitor. Editing the destination set queues a new scan and supersedes
older scans. Query forwarding, fixed UTM and permanent redirect behavior apply
to the selected destination.

## Weighted rotation

A `rotation` list selects among one to ten public HTTP(S) destinations after
all gates and device/country rules:

```json
{
  "rules": {
    "rotation": [
      { "url": "https://example.com/a", "weight": 1 },
      { "url": "https://example.com/b", "weight": 3 }
    ]
  }
}
```

Weights are relative: this example assigns about one quarter of fresh selections
to A and three quarters to B. Each weight must be positive, finite and no greater
than 1,000,000. All rotation URLs are structurally validated and included in safety
scans. Changing only weights does not require rescanning the same destinations.

The first selection sets a host-only `_sr_{linkId}` cookie for one hour, with
`Secure`, `HttpOnly` and `SameSite=Lax`. It stores a configuration fingerprint and
option number rather than a visitor identifier. Subsequent visits reuse that
option without refreshing expiry. Changing URLs, option order or weights
invalidates the previous fingerprint and selects again. Malformed or out-of-range
cookie values are ignored. Without cookies, each request gets a fresh selection.

Device/country matches and safety/password/time gates do not create a rotation
cookie. Query forwarding, fixed UTM and permanent redirect settings apply to the
selected option. Use the default 302 when rotation changes should reach returning
visitors promptly; clients may remember permanent redirects.

## Branch analytics

Targeted destination clicks carry `m.branch`: `device:0`, `geo:0`, `rotation:0`, and so on,
or `destination` when no targeting rule matched. Numbers are zero-based rule
positions; editing or reordering rules changes what a position represents.
Links without targeting retain the existing event format, with no branch field.

The REST analytics endpoints accept `groupBy=branch` and `branch=geo:0` filters,
subject to the existing analytics-breakdown plan gate and workspace isolation.
The click pipeline preserves branch values during retries, and daily rollups
include `byBranch`. Safety/password/time gates produce no destination click.
The dashboard branch breakdown view remains part of the advanced management UI
work; the REST breakdown is available now.

## Permanent redirects

`permanent: true` uses HTTP 301 for the final destination hop. The default remains
HTTP 302. Destination clicks are still enqueued, and the server sends
`Cache-Control: private, max-age=0`. Permanent redirects can be remembered by
clients; prefer the default for links whose destination or rules may change.

## Social preview overrides

Link creation, bulk creation and editing accept a top-level `og` object:

```json
{
  "url": "https://example.com/article",
  "og": {
    "title": "The article title",
    "description": "A short description for a shared link.",
    "image": "https://images.example.com/article-card.png"
  }
}
```

Configure at least one field. Titles are capped at 200 characters, descriptions
at 500, and image URLs at 2,048. Empty fields and unknown keys are rejected. Images
must use public HTTPS URLs without credentials. Shortn does not fetch or upload
these images in the redirect path; social platforms fetch the configured URL.
Use a stable image URL accessible without authentication.

`PATCH /v1/links/{id}` replaces the whole `og` object when supplied. Send
`og: null` to clear it; omit it to preserve existing metadata. Reads return the
configured object. Creation and bulk creation share the same validation. Metadata
mutations invalidate canonical and historical-alias cache entries.

Recognized preview UAs include `facebookexternalhit`, `Facebot`, `Twitterbot`,
`LinkedInBot`, `Slackbot-LinkExpanding`, `Discordbot`, `TelegramBot`, `WhatsApp`,
`SkypeUriPreview`, `MicrosoftPreview`, `Pinterestbot` and `redditbot`. Matching is
case-insensitive and examines at most 2,000 characters. This is response selection,
not crawler authentication; user-agent strings can be copied. Search crawlers and
other automated clients retain normal redirects.

After safety, password, time and visit-limit gates, a preview bot gets private HTML
with escaped Open Graph/Twitter metadata and a meta refresh to the selected web
destination. The canonical `og:url` is the short URL without request query values.
An omitted preview title falls back to the link's title. An omitted image does
not invent an image URL. Device/country/rotation selection and query/UTM rules
still apply, but preview responses do not set rotation cookies or open native
apps. Humans keep normal redirect or deep-link behavior.

Recorded preview requests remain marked as bots in the click stream. They contribute
to bot volume rather than human click/scan analytics, click webhooks or the hot
visit count. Gate and fallback responses do not expose configured preview
metadata. Responses vary by user agent and are private with zero freshness;
platforms may keep their own preview cache and may require a fresh scrape after
an edit. The advanced in-page preview editor remains pending.

## Destination health

Link reads include read-only `health` results when a current check is scheduled.
`health.checks` contains individual destination results; `checkedAt` is the last
completed cycle and `nextCheckAt` is the next queued check time. An empty checks
list means the first check is pending. Queued checks may run after their due time
while a backlog drains.

The worker checks the main destination and configured web targets, including
fallback, device/country/rotation URLs, HTTPS app links and store URLs. Custom
app schemes are excluded. URLs differing only by fragment share a check. A cycle
runs weekly, with one target checked per job and bounded batches for large
libraries. Requests are limited per hostname across worker instances.

Checks use HEAD, falling back to GET with a one-byte Range request for HTTP 405
or 501. The connection closes after response headers, even if a server ignores
Range. Up to three redirects are followed, with public-address validation and
DNS pinning on every hop. Private, mixed public/private DNS and credential-bearing
targets are not requested. Redirect loops, DNS/network failures and timeouts
produce an unknown/unreachable result, not an assumed HTTP error.

Each check reports `status` (`ok`, `http_error`, `unreachable`, `blocked`), optional
HTTP status, check time, consecutive HTTP-error `streak`, and `failingSince`.
`health.flagged` becomes true after two consecutive 4xx/5xx checks of any target.
A healthy response clears that target's streak. Network/blocked outcomes do not
pretend that the page returned a 4xx/5xx status. Failure codes exclude remote
response bodies and credentials.

Health is informational: results never disable a link, change safety status or
modify its redirect. Websites may treat the checker differently from a browser.
The checker identifies itself as `Shortn-LinkHealth/1.0`, and Shortn treats its
requests as bots so checks of other Shortn links do not inflate human analytics.
Changing the destination set resets the check generation and hides old results;
late results cannot apply to an edited link. Metadata-only edits retain results.

The same workspace permissions and link-read API scopes apply. Health fields
cannot be written through link mutations. Active dashboard rows flag repeated
HTTP errors and open the link overview's Destination health section. It shows
per-target status and check dates, distinguishes blocked and unreachable results,
and hides stale results after a destination edit. Health remains informational.

Dashboard Links and QR selections persist across pages and filters, up to 500
items per action. Delete opens the existing permission-checked review page.
This dashboard limit is separate from the REST bulk-create limit of 1,000.

## Remaining rule features

Unsupported rule fields are rejected. The advanced
in-page rule editor also remains pending. Background safety scans for new or
changed link destinations are durable: the worker executes them through BullMQ,
retries failures with bounded exponential backoff, and rescans successful links
weekly. A destination edit supersedes any older queued or running request.
`WEB_RISK_API_KEY` must be configured in the worker for Google Web Risk checks;
without it the existing structural/domain-age checks still run. A configured
reputation-provider outage is retried rather than treated as a completed weekly scan.

Safety-provider results use shared Redis caching. RDAP registration timestamps
are cached per destination host for 24 hours; domain age is recalculated for
each scan. Web Risk threat matches remain specific to the exact URL and expire
at the provider's `expireTime`, following the
[Lookup API cache protocol](https://docs.cloud.google.com/web-risk/docs/caching).
Clean Lookup responses and provider failures are not cached. A clean path does
not establish trust for other URLs on that host. Cache outages fall through to
provider checks, and configured reputation outages retain durable retries.


---

# MCP setup and implementation status

Shortn has a locally verified MCP endpoint at `/mcp`, using the official
TypeScript SDK's Streamable HTTP transport with JSON responses. Deployment is
disabled by default. API-key access and workspace OAuth consent are implemented
and verified locally. Manual supported-client and staging acceptance remain
open. These examples apply to an explicitly enabled deployment.

## Authentication and rollout

Use a scoped workspace API key as `Authorization: Bearer sk_live_…`. Session
cookies are not accepted. Current membership, role, expiry, IP restrictions and
revocation are checked on every request. MCP and REST share the same per-key
rate budget and `RateLimit-*` headers. Core services enforce resource permissions.

Operators enable `PLATFORM_MCP_ENABLED=1` and configure `MCP_PUBLIC_ORIGIN`,
`API_KEY_PEPPER`, and both Redis URLs. Production requires HTTPS and separate
cache/durable Redis instances. `PLATFORM_MCP_ROLLOUT` starts at zero; the
workspace's `platformFeatures.mcp` override takes precedence. MCP's gate is
independent of REST. The request Host must match the configured public host;
cross-origin browser requests are not enabled by this API-key implementation.

## Workspace OAuth

Enable `MCP_OAUTH_ENABLED=1` on both web and API, with the same
`MCP_PUBLIC_ORIGIN`. On the API, set `MCP_OAUTH_ISSUER` to the web auth issuer,
for example `https://app.shortn.at/api/auth`, and enable the MCP mount as above.
Production requires HTTPS. Keep the web and API workspace rollout configuration
consistent. OAuth remains disabled unless explicitly configured.

The API publishes `/.well-known/oauth-protected-resource/mcp` and includes its
address in the unauthenticated `WWW-Authenticate` challenge. The web publishes
`/.well-known/oauth-authorization-server/api/auth`. Clients discover the
provider and register a public client, then request an authorization code for
the exact MCP resource (`https://api.shortn.at/mcp`) with PKCE S256. The browser
asks the user to sign in, select an eligible workspace and review permissions.
Email and username sign-in both resume the connection. Cancel creates no grant.

A grant is limited by the user's current workspace role and requested scopes.
Access tokens expire after ten minutes; refresh requires `offline_access`.
Client, consent, membership and the originating login session are checked live;
signing out of that session ends access. Workspace selection changes between
review and approval are rejected. OAuth mutation audit actors identify the
verified client. Refresh preserves the rate-limit identity.

Registered callbacks must match exactly. HTTPS, loopback HTTP and supported
native/private-use callback schemes are accepted; callbacks with credentials
or fragments are rejected. Native client launch and Claude Desktop acceptance
have not yet been verified. The local browser fixture verifies discovery,
consent, PKCE exchange and an authenticated MCP call against a synthetic app.

## Tools and scopes

| Tool              | Required scopes                                                                  | Behavior                                                                |
| ----------------- | -------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| `create_link`     | `links:write`                                                                    | URL, optional key/domain/title/tags/UTM/expiry; returns a short URL     |
| `update_link`     | `links:write`                                                                    | Updates fields and rules by ID                                          |
| `archive_link`    | `links:write`                                                                    | Moves authorized links to 30-day trash; annotated destructive           |
| `get_link`        | `links:read`                                                                     | Reads by ID or key; custom domains can be specified                     |
| `search_links`    | `links:read`; `campaigns:read` for a campaign filter                             | Query/tag/campaign filters, up to 200 results and a next cursor         |
| `get_analytics`   | `analytics:read`; `links:read` for a link key or `campaigns:read` for a campaign | One link, tag, campaign or workspace; bounded breakdown and summary     |
| `create_qr_code`  | `qr:write`, `links:read`                                                         | Attaches a QR code; signed export URL or asynchronous export job when enabled         |
| `get_qr_export` | `qr:read` | Reads export progress and the finished expiring private download URL |
| `list_campaigns`  | `campaigns:read`                                                                 | Bounded cursor pages without full campaign link libraries               |
| `create_campaign` | `campaigns:write`                                                                | Creates a campaign                                                      |
| `add_bio_link`    | `bio:write`, `links:write`                                                       | Creates and appends atomically; a full/missing page rolls back creation |
| `get_usage`       | `workspace:read`                                                                 | Current period consumption and plan limits                              |

Wildcard scopes such as `qr:*` and `bio:*` include their read/write scopes.
Scoped viewers cannot mutate resources; members can archive only their own
links. Analytics retain plan and retention limits. Search and campaign listings
use server-side pagination: follow `nextCursor` to browse the full library.
Tool errors have stable codes and `isError: true`.

`create_qr_code` returns a signed `exportUrl` when the enabled worker finishes
within its bounded wait, or an `exportJobId` to poll. The added
`get_qr_export` tool requires `qr:read` and returns the finished private URL.
See [QR exports](/docs/qr-exports.md) for flags, formats, limits and remaining UI
and staging gates. Disabled export configuration reports `exportStatus: "disabled"`.

Mutations record durable intent in `audit_log` before calling core, with actor
`mcp:api-key:{keyId}`, real user ID and tool name, followed by succeeded/failed
status. If intent recording fails, the mutation does not run. If completion
recording fails after success, intent remains for reconciliation and the tool
retains its successful response to avoid encouraging a duplicate mutation.
Audit records omit raw arguments, destination URLs and secrets.

## Resources and prompts

- `shortn://links/{key}`: default-domain link; requires `links:read`.
- `shortn://analytics/summary`: last seven days; requires `analytics:read`.
- `weekly_link_report`: prompt to summarize that resource.
- `create_campaign_links`: prompt taking `title` and `urls` strings.

Prompts describe work; fetching one does not mutate data. Custom-domain links
use `get_link` with `domain` instead of the default-domain resource.

## Client configuration

Replace the origin below with the enabled deployment's origin. Create a key
with the scopes needed for the workflow.

For [Claude Code](https://code.claude.com/docs/en/mcp), set `SHORTN_API_KEY` in
your shell and add the HTTP server:

```sh
claude mcp add --transport http shortn https://api.shortn.at/mcp \
  --header "Authorization: Bearer ${SHORTN_API_KEY}"
```

For [Cursor](https://cursor.com/docs/mcp), use `.cursor/mcp.json` and provide
`SHORTN_API_KEY` in the environment used to start Cursor:

```json
{
  "mcpServers": {
    "shortn": {
      "url": "https://api.shortn.at/mcp",
      "headers": { "Authorization": "Bearer ${env:SHORTN_API_KEY}" }
    }
  }
}
```

For [VS Code](https://code.visualstudio.com/docs/agents/reference/mcp-configuration),
use `.vscode/mcp.json` with a masked input:

```json
{
  "inputs": [
    {
      "type": "promptString",
      "id": "shortnApiKey",
      "description": "Shortn API key",
      "password": true
    }
  ],
  "servers": {
    "shortn": {
      "type": "http",
      "url": "https://api.shortn.at/mcp",
      "headers": { "Authorization": "Bearer ${input:shortnApiKey}" }
    }
  }
}
```

These follow the clients' documented HTTP/header formats. Local verification
uses the SDK client and MCP Inspector; manual client and staging acceptance
remain open. OAuth-capable clients can use the discovery flow above on an
explicitly enabled deployment; native client acceptance is still pending.


---

# QR exports

QR exports are implemented and verified locally. They are disabled by default;
external storage and staging acceptance remain open. The worker produces SVG,
PNG and print-sized PDF artifacts from an immutable design snapshot. Single and
selected ZIP dashboard exports include review, progress and private downloads.

## Enablement

Set `QR_EXPORTS_ENABLED=1` on the web dashboard, API and worker. Each requires
`S3_ENDPOINT`, `S3_REGION`, `S3_BUCKET`, `S3_ACCESS_KEY_ID` and
`S3_SECRET_ACCESS_KEY`, pointing at the same private S3/R2-compatible bucket.
The worker also requires `APP_URL`; `APP_URL` and optional `PUBLIC_ORIGIN`
define the allowed origins of stored upload logos. Production origins require
HTTPS. Provision the declared job indexes with `bun run db:indexes` and run the
worker with the existing durable Redis configuration. REST and MCP retain their
independent mount and workspace rollout gates.

The public assets route serves only validated `uploads/` keys. Export objects
under `exports/qr/` are delivered by signatures after authorization. Keep this
cache prefix private and available for every active job's delivery lifetime;
job access and signatures expire after 24 hours. ZIP output under
`exports/jobs/qr/` needs a 24-hour lifecycle; it must also remain private.

## REST

Use a bearer API key with `qr:read`. Viewers can export codes they can read.
Exporting does not edit the QR code or require `qr:write`.

```sh
curl --max-redirs 0 -i \
  -H "Authorization: Bearer ${SHORTN_API_KEY}" \
  'https://api.shortn.at/v1/qr-codes/QR_ID/export?format=png&size=1024'
```

`format` is `svg`, `png` or `pdf` (default `svg`); `size` is an integer from
128 through 4096 pixels (default 1024). `202 Accepted` returns an export job and
an authenticated polling path in `Location`, with `Retry-After: 1`:

```json
{
  "id": "JOB_ID",
  "state": "pending",
  "format": "png",
  "size": 1024,
  "createdAt": "2026-10-15T10:00:00.000Z",
  "expiresAt": "2026-10-16T10:00:00.000Z"
}
```

Poll `GET /v1/qr-exports/JOB_ID` using the same workspace credential. The status
is `pending`, `running`, `completed` or `failed`. Completed jobs include
`downloadUrl`, `downloadExpiresAt` and print guidance. The download URL is a
private attachment signature valid only for the remaining job lifetime. A
repeated export request redirects with `303 See Other` when ready; use `curl -L`
to follow that redirect and save the artifact. Cross-origin clients can read
`Location`, `Retry-After` and rate headers. Status responses use
`Cache-Control: private, no-store` and omit the stored design and object key.

Equivalent requests for the same creator/design/format/size in the same UTC day
reuse the job. The worker retries temporary storage failures and resumes expired
leases. New requests after a design change produce a new snapshot. A failed
print design returns a stable failure code; repeated export requests return
`409 export_failed` with the job ID and reason. Temporary signing failure returns
`503 storage_unavailable`. Expired, foreign or inaccessible jobs return `404`;
revoked credentials return `401`.

## Dashboard

From a QR code, choose **Export for print**, then SVG, PNG or PDF and a size
from 128 to 4096 px. The preview uses corrected print options, and the page shows
minimum print guidance. JPEG remains available separately for screen use. With
the flag off, existing browser downloads remain available.

In the QR library, select codes across pages and filters and choose
**Export selected**. Viewers can export readable codes. The review supports the
library’s searchable tag, date and keyword filters, date grouping and 25-row
server pages. Filters change the review list; the ZIP includes the entire
selection shown in the count, up to 500 codes and 512 MiB. A short, private
selection reference is bound to the current workspace and user for 24 hours.

The status page polls while visible, shows preparation counts, and stops polling
when the job completes or fails. It provides a private attachment download and
its expiry, or a clear error and a link to review a failing design. Current
membership and resource access are rechecked on each request. A selection that
loses an item before submission is rejected without creating a job.

## Selected QR ZIP

`POST /v1/qr-codes/export` queues a ZIP job with `qr:read`, including for viewers.
Send `ids` (1–500 selected QR IDs), `format` (`svg`, `png` or `pdf`) and optional
`size` (128–4096, default 1024). Duplicate IDs are collapsed; every selected code
must belong to the workspace. A mixed foreign/missing selection creates no job.
Use `Idempotency-Key` for request replay. Equivalent selections, independent of
order, reuse the same creator/day/design job.

The 202 response includes the job ID, `total`, `done`, the 500-code `limit`, and
`maximumBytes: 536870912` (512 MiB). Poll the authenticated `Location` at
`GET /v1/qr-exports/zip/JOB_ID`. `phase` is `rendering` or `assembling`; `done`
counts checkpointed artifacts. The worker persists one immutable item snapshot
per selected code, processes cursor batches, and resumes completed items after
retry or lease recovery. ZIP names are safe, unique flat filenames. A print
failure identifies `failedQrId` and its stable reason.

Archives stream to private multipart storage rather than accumulating the
entire ZIP in worker memory. Output above 512 MiB fails as `archive_too_large`
and incomplete multipart uploads are aborted. A completed status includes the
expiring attachment URL. Current membership and every selected resource are
checked before signing; foreign, deleted or expired selections cannot be
retrieved. Configure a 24-hour lifecycle for `exports/jobs/qr/`, separately from
the reusable design cache under `exports/qr/`.

## MCP

`create_qr_code` accepts optional `format` and `size` alongside `linkKey`,
`domain` and `style`. It requires `qr:write` and `links:read`. With exports enabled,
it waits up to five seconds for the worker and returns the signed `exportUrl`
when ready. If still processing, it returns `exportJobId`, `exportStatus` and a
null `exportUrl`. Poll `get_qr_export {id: exportJobId}` with `qr:read` to obtain
the finished download URL. This is the twelfth tool, added to the eleven planned
workspace tools for asynchronous delivery.

When export configuration is disabled, QR creation still succeeds with
`exportStatus: "disabled"`. If export fails after QR creation, the result retains
the created QR ID and available job ID; it reports the export failure without
presenting the QR mutation as failed. Polling is read-only; QR creation retains
its mutation audit actor and record.

## Print output

Print plans enforce a quiet zone of at least four modules, error correction Q
minimum (H with a logo), opaque colors and at least 3:1 module/background contrast,
including finder colors and gradients. Six dot/finder styles, linear/radial
paints and stored PNG logos are supported. Sizes, payloads and logo bytes are
bounded. The worker reads logos directly from validated stored upload keys;
it does not fetch arbitrary image URLs.

PDF page dimensions preserve 300 dpi. `print.minimumMm` recommends a physical
size using four printer pixels per module at 300 dpi. Circular legacy designs currently fail explicitly. Frame/caption treatment
remains unimplemented. The local parity fixture verifies corrected
print previews against server artifacts at 1024 px.


---

# Outgoing webhooks

Outgoing webhooks are implemented behind default-off flags. Main link CRUD,
trash/restore, managed bio publication and domain verification produce automatic
events. Campaign UTM changes and QR backing/attachment mutations also produce link
events. Tag rename/deletion, domain assignment/removal/recovery, safety scans and
report-triggered blocking also emit link updates. Opted-in clicks and scans are
batched. Import completion remains pending.
Explicit test requests produce `webhook.test` deliveries.

## Configure an endpoint

Use an API key with `webhooks:write` whose creator is a workspace owner or admin.
Creation, changes, tests and redelivery require `PLATFORM_WEBHOOKS_ENABLED=1`, a
configured `WEBHOOK_SECRET_KEY`, and an enabled workspace or rollout cohort.
Metadata reads require `webhooks:read` and an owner/admin creator. Reads and
removal remain available when outgoing provisioning is disabled.

`POST /v1/webhooks` accepts `name`, an HTTPS `url`, `events`, and optional
`enabled` (default true). Supported subscription names are `link.created`,
`link.updated`, `link.deleted`, `link.clicked`, `qr.scanned`, `bio.published`,
`domain.verified` and `import.completed`.

The creation response includes a `whsec_…` signing secret. Retain it securely;
subsequent metadata reads omit it. Replaying the original POST with the same
`Idempotency-Key` returns the original creation response. Signing secrets are
encrypted in MongoDB and creation-response idempotency records. Keep the same
encryption key across API and worker processes. Changing it requires a separate
re-encryption procedure; replacing it directly prevents existing secrets from
being decrypted.

Endpoints must use HTTPS without URL credentials or fragments. The delivery
worker rejects private, loopback, link-local, reserved and other special IP
ranges, including mixed public/private DNS answers. It pins a validated DNS
address while preserving TLS hostname verification, applies DNS and request
deadlines, and treats redirects as unsuccessful deliveries.

## Test, inspect and redeliver

`POST /v1/webhooks/{id}/test` returns HTTP 202 with the persisted delivery ID and
event ID. No destination request runs in the API handler. The worker sends:

```json
{
  "id": "event UUID",
  "type": "webhook.test",
  "createdAt": "2026-10-13T12:00:00.000Z",
  "workspaceId": "workspace ID",
  "data": {}
}
```

`GET /v1/webhooks/{id}/deliveries` returns a cursor-paginated log retained for 30
days. It includes status, attempt count, timestamps and bounded error codes,
without receiver response bodies or signing secrets. Use
`POST /v1/webhooks/{id}/deliveries/{deliveryId}/redeliver` to create a new delivery
for the same event. An expired log record cannot be redelivered.

Endpoint metadata uses `GET /v1/webhooks`, `GET/PATCH/DELETE /v1/webhooks/{id}`.
Lists accept `cursor`, `limit` (1–200, default 50) and literal `search` on name or
URL. Disabling or deleting an endpoint prevents queued deliveries from starting
another destination request; an already-started request may finish.

## Verify deliveries

Headers include `Shortn-Delivery-Id`, `Shortn-Event-Id` and
`Shortn-Signature: t={unix seconds},v1={hex HMAC-SHA256}`. The signed bytes are the
timestamp, a period, and the exact raw UTF-8 request body. Use the complete
`whsec_…` string as the HMAC key, without decoding or removing its prefix. Check
timestamp freshness and compare signatures with a constant-time comparison.

Delivery is at least once. Persist the event ID when processing an event so an
uncertain network response or manual redelivery cannot repeat an already-applied
operation. All attempts for a delivery retain its delivery and event IDs;
manual redelivery has a new delivery ID and the original event ID. Return a 2xx
status once the event has been durably accepted by the receiver.

MongoDB is the durable delivery source. BullMQ executes attempts on the durable
Redis instance. Failed attempts back off exponentially from one second, capped
at one hour, within a 24-hour retry window. Expired leases can be reclaimed after
a worker interruption. Endpoints with an unresolved failure period of three days
are disabled and a notification record is persisted atomically. When email is
configured, the worker emails the workspace owner in their preferred language,
retrying failed sends every five minutes with a stable provider idempotency key.
Each new disablement has a separate notification identity. The existing mail
allowlist applies. Workspace owners and admins can manage endpoints and delivery logs in Settings → Webhooks.

## Automatic mutation events

The mutation and source event commit together in a MongoDB transaction. Failed
mutations produce no event. No enabled subscriber means no source event is stored.
The worker fans out in bounded recipient pages, advancing its cursor atomically
with delivery creation. Concurrent pumps and restarts reuse automatic delivery
identities. Subscribers created after the event are excluded; subscriptions and
workspace feature gates are checked when fanout runs and before sending.

Main link creation emits `link.created`; edits and restoring a trashed link emit
`link.updated`; moving an active link to trash emits `link.deleted`. Repeating
delete/restore does not emit another transition event. Link `data` contains `id`,
`domain`, `key`, `url`, `title` and `deletedAt`, with no password material, safety
reports or visitor information. Managed publication emits `bio.published` with
`id`, `handle` and `revision`; republishing that revision is a no-op. A domain's
transition to active emits `domain.verified` with `id`, `hostname`, `kind` and
`status`; periodic verification does not repeat the event.

## Click and scan batches

Explicitly subscribe to `link.clicked` or `qr.scanned` to receive these events.
They are collected separately per workspace and event type, then sent when 100
receipts accumulate or the oldest receipt reaches 10 seconds. The worker checks
every second; queue or database backlog can delay delivery. One pump processes
at most 20 batches of 100 receipts, sharing capacity between due workspaces.

Batch `data` has `count` and an `events` array. Each entry has `id` (the original
stream ID), `linkId`, `timestamp`, `domain`, `key` and, for scans, `qrId`. Domain
and key strings are bounded. Raw IPs, user agents, referrers, query parameters
and visitor hashes are excluded. Bots are excluded. Legacy personal-workspace
links and legacy QR backing links are supported; QR ownership and backing-link
identity are checked before recording a scan.

Receipts commit with majority write concern before Redis acknowledgement. If
receipt persistence fails after analytics was saved, the pending entry replays
without duplicating analytics or webhook receipts. Receipt consumption and source
event creation share a transaction, so an interrupted batch can be retried.
Completed receipts retain their stream IDs for 30 days to deduplicate replays;
unprocessed receipts have no TTL. Automatic fanout uses the batch's first receipt
time to exclude endpoints created after collection started. Workspace and endpoint
disablement still apply when the batch is flushed and delivered.

Campaign UTM replacement, rename and deletion emit `link.updated` for links whose
variants change. Their events commit with campaign membership and canonical link
fields. References are workspace scoped, including legacy title-only references;
other campaign IDs with the same title are preserved. Repeating identical variants
emits no update. Stale expected-state edits return a conflict before creating a
campaign or recording an event.

Creating a QR emits `link.created` for its hidden backing link and `link.updated`
when attaching it to an existing link. QR editing emits updates for changed
backing/attached links, including tags when explicitly applied to the attachment.
Deleting a QR emits `link.deleted` for its removed backing link and `link.updated`
for detachment. QR, backing-link, attachment and source-event mutations share a
transaction; creation quota checks are serialized, and deletion archives legacy
scans in 1,000-row pages inside that transaction. Stored link IDs keep attachment
updates and detachment working after a linked resource is renamed. Legacy records
without those IDs retain their key-based lookup.

Tag rename and deletion emit `link.updated` for linked resources, including
trashed links and canonical tag-ID references. Repeating the current tag name is
a no-op. Tag/reference updates and source events share the existing transaction.
Workspace ownership checks exclude contaminated foreign references.

Domain assignment, removal and recovery emit updates for changed links. Assignment
checks hostname/TLS readiness and add-on status in its transaction, protects all
previous keys against collisions, and rejects trashed links. Domain operations
invalidate canonical and alias caches on affected hosts and queue related bio
page invalidations with the mutation. Recovery retains conflicting links on their
original host.

Safety scans emit updates when risk, status or blocking fields change. Periodic
rescans with identical state emit no event. A scan result for a changed destination
is discarded; fields derive from the current transactional document, preserving
concurrent password edits and report-triggered blocking. The third distinct abuse
report blocks the link and emits an update in the same transaction. Reports below
the threshold and duplicate reporters do not emit update events. Report details,
reporter identities and visitor data are excluded from event payloads.

## Workspace settings

Settings → Webhooks provides in-page create/edit forms, explicit event selection,
pause/enable controls, test requests and confirmed deletion. The signing secret
is shown once after creation; copy it and acknowledge saving it before creating
another endpoint. Refreshing or leaving the page removes the secret from the UI.
Endpoint lists support literal name/URL search and 25-row cursor pages.

Open an endpoint's Delivery log to view its 30-day history in 25-row pages.
Rows show event identity, state, attempts, HTTP status and bounded error codes.
Redeliver creates a new delivery for the same event, and Refresh updates the log.
Paused endpoints must be enabled before requesting deliveries. When outgoing
webhooks are unavailable, reads and deletion remain available; provisioning and
delivery requests are disabled. Server actions recheck the current workspace role
and shared service gates for every operation. Interface copy is available in
English, Portuguese and Spanish.


---

# Understanding analytics

Shortn counts every visit to your short links and QR codes. This guide explains what is counted, how to narrow the numbers down with ranges, breakdowns and filters, how to export them, and how visitor data is protected.

## Clicks and scans

Shortn records two kinds of visit:

- a **click** is a visit through a short link, whether typed, tapped or opened from another site;
- a **scan** is a visit through one of your QR codes.

The **Type** filter switches between **Clicks and scans**, **Clicks** and **QR scans**. The summary at the top also shows **Unique visitors** and the **Top country**.

## Where to find analytics

- **Analytics** in the sidebar covers the whole workspace.
- A link's **Analytics** tab covers that link.
- A QR code's **Scans** tab covers that code.
- A campaign's **Analytics** tab covers the links in that campaign.
- A bio page's **Analytics** tab shows views, clicks and the click-through rate of the page.

All of them except bio pages share the same controls.

## Choose a date range

1. Open **Date range** and pick **Last 7 days**, **Last 30 days**, **Last 90 days** or **Last 12 months**, or set a **Custom range** with **From** and **To** dates.
2. Choose an **Interval** to group the chart by hour, day, week or month.

How far back you can go depends on your plan's analytics history. When a range starts earlier, the page tells you the date before which data is outside your plan's analytics window and shows what it can. See [Pricing](/en/pricing) for the history each plan includes.

## Breakdowns

Below the chart, breakdowns group visits by:

- **Links**
- **Countries** and **Cities**
- **Referrers**: the site a visit came from. Direct means no referring site, such as a typed address, an app or a QR scan.
- **Devices**, **Browsers** and **Operating systems**
- **UTM sources**, **UTM mediums** and **UTM campaigns**
- **Routing branches**: which redirect rule picked the destination, for links with country, device or rotation rules.

Each breakdown shows the top rows; select **See all** for the full list. Breakdowns other than links, and filtering by them, depend on your plan.

## Filter the view

Select a value in a breakdown to filter the whole page by it, or open the filters to combine several, for example one link, one country and mobile devices. Select **Clear all** to remove them.

Every filter is part of the page address, so you can bookmark a view or share it with a colleague who has access to the workspace.

## Export analytics

1. Set the range and filters you want.
2. Select **Export analytics**.
3. Under **Data**, choose **Aggregates** for totals per time bucket and per group, or **Raw events** for one row per visit.
4. Choose the format (CSV, or NDJSON for raw events) and select **Prepare export**.

Exports run in the background, so you can leave the page. Find finished files under **Export history**. Files and history expire 24 hours after preparation. The export keeps the retained window you reviewed, even if your plan changes before it runs. CSV export depends on your plan, and campaign export has its own plan requirement.

## Privacy

- Visitor IP addresses are hashed.
- Raw IP addresses are deleted after 90 days.
- Raw-event exports exclude IP addresses, visitor hashes and user agents.
- Embedded content on bio pages loads only after the visitor chooses to load it.

## Reading the numbers

- Compare the same range across links, rather than totals across ranges of different length.
- Use UTM breakdowns to see which channel worked, and routing branches to compare the destinations of one link.

## Next steps

- [UTM parameters](/docs/en/guides/links/utm-parameters)
- [Organizing campaigns](/docs/en/guides/campaigns/organizing-campaigns)
- [Upgrading plans](/docs/en/guides/billing/upgrading-plans)


---

# Upgrading plans

Each workspace has its own plan. This guide covers where to see your plan and usage, how to upgrade, downgrade or cancel, how payment works, and what happens to your links when a plan changes. Only the workspace owner can change the plan or see invoices.

## See your plan and usage

Open **Settings** and select **Billing**. The page has three parts:

- **Plan**: your current plan and when it renews or ends. On Free, it shows when usage resets.
- **Usage**: links and QR codes created this period, and bio pages in total, against your plan's limits.
- **Invoices**: your payment history, with a **Download** link for each invoice.

Link and QR code limits reset every billing period. When you reach a limit, the page tells you, and you can upgrade to keep creating.

## Compare plans

Select **Compare plans** to see every plan side by side: limits, analytics history, members, workspaces and features such as custom keys, campaigns, analytics breakdowns, CSV export and QR logos. The same information is on the [Pricing](/en/pricing) page.

## Prices and currency

Plans are monthly and billed in US dollars. Shortn shows a euro estimate next to each price, based on European Central Bank reference rates. Checkout charges the dollar amount; the euro figure is only an estimate.

## Upgrade

1. Open **Compare plans**.
2. Select **Upgrade to …** on the plan you want.
3. On Free, you go to checkout. Enter your payment details there and confirm.
4. Back in Shortn, the billing page shows your new plan within a few seconds, once the payment provider confirms it. Select **Refresh** if it hasn't changed yet.

If you already pay for a plan, an upgrade opens a confirmation page instead of checkout. Select **Switch to …** to confirm. The new plan starts as soon as the billing provider confirms, and you are charged the prorated difference for the current period.

## Downgrade

1. Open **Compare plans** and select **Switch to …** on a lower plan.
2. Read the confirmation page, then select **Switch to …**.

You keep your current plan until the end of the period. The lower plan's limits and features apply from then on. To stay on your current plan, select **Keep …** instead.

## Cancel

1. On the billing page, select **Cancel plan**.
2. Confirm on the next page.

You keep the plan until the end of the current period; then the workspace moves to Free. Until that date you can select **Keep …** to resume the plan as usual.

## Your links keep working

Plan limits only control what you can create. When you downgrade or cancel, existing links, QR codes and bio pages keep redirecting. Features your new plan doesn't include stop being available for new changes. For example, if your plan no longer includes QR logos, you can keep an existing logo or remove it, but not add a new one.

## Payment methods and failed payments

Select **Open billing portal** to update your billing details or payment method. If a payment fails, the billing page shows a notice with **Update payment method**, so you can keep your plan.

## Custom-domain add-on

Custom domains are sold as an add-on, separate from the plan. Each add-on includes one domain. Buy one from **Settings** under **Domains** with **Buy domain add-on**, then connect the domain there. See [Customizing bio pages](/docs/en/guides/bio-pages/customizing-bio-pages).

## Enterprise

Enterprise plans have terms agreed with Shortn. To change one, contact Shortn.

## Next steps

- [Pricing](/en/pricing)
- [Getting started](/docs/en/guides/getting-started)


---

# Creating bio pages

A bio page gathers your links on one public page, the kind you put in a social media profile. Visitors see your profile, your links and any other sections you add, and every click is counted. This guide covers creating a page and publishing its first version.

## Before you start

How many bio pages a workspace can have depends on your plan; the Bio pages list shows how many you've used. The links on a bio page are ordinary Shortn links, so they show up in **Links** and in analytics like any other link.

## Create a page

1. Open **Bio pages** in the sidebar and select **New bio page**.
2. Under **Address**, enter the address people will visit. Use 3 to 63 lowercase letters, numbers and hyphens. Shortn checks whether it's free as you type.
3. Optionally enter a **Title**, shown at the top of the page. If you leave it empty, the address is used.
4. Optionally select **Choose a link** under **First link** to put one of your links on the page straight away.
5. Select **Create page**.

The page's overview opens, with **Copy URL** and **Open page** for its public address.

## Build the page

From the overview, select **Edit bio page**. The editor shows a live preview of your page; switch between **Phone preview** and **Desktop preview** to check both.

1. Select **Add section** and choose what to add. The section types are **Profile**, **Link**, **Social links**, **Section heading**, **Text**, **Image**, **Embed**, **QR code** and **Links**.
2. Select a section in the preview to edit it. Drag its handle to reorder sections.
3. To add several of your links at once, add a **Links** section and select **Manage links**. You can filter by tags, search, and select links across pages before adding them.
4. To add a URL that isn't a short link yet, type it into a link section and select **Shorten and add link**. This creates a new link in your workspace.

Changes save as a draft while you work. Visitors don't see them until you publish.

## Publish

Select **Publish** when the draft is ready. The page is live at its address straight away. Each publish creates a version you can go back to later; see [Customizing bio pages](/docs/en/guides/bio-pages/customizing-bio-pages).

## Share the page

Select **Copy URL** on the overview and paste the address into your social profiles, email signature or printed material. To change the page's name in its address, open **Page settings** in the editor and use **Change handle**. Previous addresses keep working after a change.

## Bio page analytics

The **Analytics** tab of a bio page shows the last 30 days: **Views**, **Clicks**, **Click-through rate**, **Clicks by block** and the top referrers. The links on the page are also counted in the workspace **Analytics** page.

## Delete a page

On the overview, select **Delete bio page** and confirm. The public address stops working right away and becomes free for anyone to claim. The links that were on the page keep working.

## Tips

- Put the link that matters most first, or highlight it.
- Keep button text short so it fits on a phone.
- Check the phone preview before you publish, since bio pages are usually opened from a social app on a phone.

## Next steps

- [Customizing bio pages](/docs/en/guides/bio-pages/customizing-bio-pages)
- [Managing links](/docs/en/guides/links/managing-links)


---

# Customizing bio pages

The bio page editor works on a draft: you change sections, theme and settings, check the live preview, and publish when you're ready. This guide covers each part of the editor, scheduling, versions and connecting your own domain.

Open a bio page and select **Edit bio page**. The toolbar has **Add section**, **Sections**, **Theme**, **Page settings** and **History**.

## Sections

Select **Add section** to add one of these:

- **Profile**: avatar, title, description and alignment. You can choose the avatar shape.
- **Link**: one button for a link. Turn on **Highlight this link** to make it stand out, and add a thumbnail.
- **Links**: a group of links. Select **Manage links** to filter by tags, search, select links across pages and add them in one step. Under **Display links**, choose a layout such as a vertical list, paginated cards or image grids.
- **Social links**: icons that link to your profiles on other platforms.
- **Section heading** and **Text**: headings and short paragraphs between your links.
- **Image**: an image with a description and an optional link.
- **Embed**: content from YouTube, Spotify, SoundCloud, Vimeo or Apple Music. Visitors see a button to load it, with a note that loading the content shares their IP address with the provider.
- **QR code**: a QR code for one of your links.

Select a section in the preview to edit it, and drag its handle to reorder. **Sections** lists every section in order.

## Show sections on a schedule

Each section has a **Visible** switch and optional **Show from (UTC)** and **Show until (UTC)** dates. Use them to show a ticket link only during an event, or hide a section without deleting it. Scheduling is available on paid plans.

## Theme

Open **Theme** to change how the page looks:

1. Pick a **Preset theme** as a starting point, or choose **Custom**.
2. Adjust the colours: **Background**, **Surface**, **Text** and **Accent**.
3. Choose a **Font**, a **Button style**, a **Corner radius** and the **Spacing**.
4. Optionally add a **Cover image (optional)** behind the profile.

Themes are constrained so pages stay readable. If the text contrast is too low, the editor tells you so; select **Use accessible color** to fix it.

## Page settings

**Page settings** holds the settings that apply to the whole page:

- **Handle**: the name in the page's address. Select **Change handle** to rename it; previous addresses keep working.
- **Page language**: the language the page is written in.
- **Hide Shortn footer**: removes the "Made with Shortn" footer. Available on paid plans.
- **Search appearance**: turn on **Hide from search engines** to keep the page out of search results.

## Drafts and publishing

Your changes save as a draft a moment after you stop typing, and you can save at any time with **Save draft**. The preview shows the draft; visitors keep seeing the published page until you select **Publish**.

If the page was changed in another session, the editor warns you before saving, so you don't overwrite someone else's work.

## Versions

Every publish creates a version. Open **History** to see **Published versions (last 20)**. To go back, select **Restore version … to draft** on the one you want. The version is copied into your draft; select **Publish** to make it live again.

## Use your own domain

You can serve a bio page from your own domain, such as `links.example.com`, with the custom-domain add-on. Each add-on includes one domain.

1. Open **Settings** and select **Domains**.
2. If you don't have a free domain allowance, select **Buy domain add-on**.
3. Under **Add domain**, enter the **Hostname**, choose what to **Use for**, pick the **Bio page**, and select **Add domain**.
4. Follow the **DNS instructions**: create the CNAME record shown at your DNS provider. For an apex domain, use CNAME flattening.
5. Wait for the status to change to **Active**. If verification fails, check the record and select **Retry verification**.

Only people who can manage billing can add domains.

## Next steps

- [Creating bio pages](/docs/en/guides/bio-pages/creating-bio-pages)
- [Upgrading plans](/docs/en/guides/billing/upgrading-plans)


---

# Organizing campaigns

A campaign groups the UTM-tagged variants of your links so you can read their clicks together. Its title is the `utm_campaign` value, it stores suggested values for the other UTM parameters, and its UTM builder tags many links in one step. Campaigns depend on your plan; if yours doesn't include them, the Campaigns page says which plan does.

## Create a campaign

1. Open **Campaigns** in the sidebar and select **New campaign**.
2. Enter a **Title**. It is used as `utm_campaign` in the URLs you tag, so write it the way it should read in your analytics, for example `spring-sale`.
3. Optionally add a **Description**.
4. Select **Create campaign**.

The campaign opens with the tabs **Overview**, **Analytics**, **UTM builder** and **Settings**.

## Set UTM defaults

Open **Settings** and fill in the **UTM defaults**: lists of suggested sources, mediums, terms and contents. Type a value and press Enter to add it. Values are saved in lowercase. The UTM builder and the link variant form suggest them, which keeps spelling consistent across your team.

Renaming the campaign switches saved variants to the new title. URLs you have already shared keep the old title, and their clicks stop counting in the campaign.

## Add links with the UTM builder

1. Open **UTM builder**, or select **Build a tagged URL** on the overview.
2. Under **Links**, select **Browse links**. Filter by tags, search, or group results by tag. Tick links on as many pages as you need, or use **Select all results**, then select **Use … selected links**.
3. Fill in **Source**, **Medium** and, optionally, **Term** and **Content**. `utm_campaign` is always the campaign's title.
4. Check the **Tagged URL** preview and select **Copy URL** if you need it straight away.
5. Select **Save as variant**. With several links selected, the button saves the same variant on every one.

A link joins the campaign when one of its UTM variants uses the campaign. Until you save the variant, its clicks don't count in the campaign.

You can also add variants one link at a time from the link's **UTM variants** tab; see [UTM parameters](/docs/en/guides/links/utm-parameters).

## Move existing links into a campaign

From **Links**, select the links you want, across pages and filters, and choose **Move to campaign**. Pick the **Destination campaign** and select **Move selected links**. Each UTM variant keeps its source, medium, term and content, and the campaign's defaults don't overwrite them.

## Read campaign results

The **Overview** shows the last 30 days of clicks, scans and visitors, and **Links and variants** lists each link with its tagged URLs and clicks. Select **Copy utm_campaign** to copy the exact value.

The **Analytics** tab opens the full analytics filtered to the campaign, with the same date ranges, breakdowns and filters as the workspace **Analytics** page. Exporting a campaign's analytics as CSV depends on your plan. See [Understanding analytics](/docs/en/guides/analytics/understanding-analytics).

## Find campaigns

The Campaigns list has a **Search** field for titles and descriptions, and **Sort by** options for newest, oldest, most links or title.

## Delete a campaign

On the overview, select **Delete campaign** and confirm. Its UTM variants are removed from the links, and its UTM defaults are deleted. The links themselves keep working. URLs already shared with its `utm_campaign` keep their clicks, but no campaign page shows them any more.

## Tips

- Agree on a naming scheme for titles, such as `season-year-product`, before the team starts tagging.
- Use `utm_content` to compare placements of the same link inside one channel.

## Next steps

- [UTM parameters](/docs/en/guides/links/utm-parameters)
- [Understanding analytics](/docs/en/guides/analytics/understanding-analytics)


---

# Getting started with Shortn

Shortn turns long URLs into short links you can share, print as QR codes or collect on a bio page, and it counts every click and scan. This guide covers creating an account, finding your way around the dashboard and creating your first short link.

## Create your account

1. Go to [app.shortn.at/register](https://app.shortn.at/register).
2. Enter your full name, a username, your email and a password, then select **Create account**. If the page offers **Continue with Google** or **Continue with GitHub**, you can use one of those instead.
3. Open the verification email and follow its link. The link works for one hour. If it has expired, or the email didn't arrive, request a new one with **Resend email**.
4. Sign in at [app.shortn.at/login](https://app.shortn.at/login) with your email or username.

Your account starts with a personal workspace on the Free plan.

## Find your way around

The sidebar holds the main sections of the dashboard:

- **Links**: every short link in the workspace, with search, tag filters, archive and trash.
- **Analytics**: clicks and QR scans across the workspace.
- **QR codes**: codes you design, download and print.
- **Bio pages**: link-in-bio pages with their own public address.
- **Campaigns**: groups of UTM-tagged links that you read together.
- **Settings**: workspace details, members, billing, domains, API keys and your account.

The **Usage** panel shows how much of your plan's monthly allowance you have used, for example **Links this month**.

A few keyboard shortcuts speed things up outside text fields: Ctrl+K (Cmd+K on a Mac) opens the **Command palette**, C starts a new link, and ? lists every shortcut under **Keyboard shortcuts**.

You can change the dashboard language and theme from the account menu.

## Create your first short link

1. Open **Links** in the sidebar.
2. Select **New link**, or paste a URL anywhere on the Links page outside a text field. Either way, the new-link form opens.
3. Enter the **Destination URL**. Shortn looks up the page and can suggest a title and key; select **Use page title** or **Use suggested key** to accept them.
4. Select **Create link**.

The link is created and copied to your clipboard, and its overview page opens. From there you can copy it again with **Copy short link**, open its **Analytics** tab or create a QR code for it. [Creating links](/docs/en/guides/links/creating-links) covers every field on the form.

## Work with a team

Each workspace has its own links, members and billing. To add a team workspace, open **Settings**, go to **General** and select **Create workspace** under **Team workspaces**. You become its owner and can invite people under **Members**. Members have one of four roles: owner, admin, member or viewer. Viewers can see links but cannot create or change them.

How many workspaces and members you can have depends on your plan.

## Plans

Free includes a monthly allowance of links and QR codes and a bio page. Paid plans raise the limits and add features such as custom keys, campaigns, analytics breakdowns and CSV export. Limits reset every billing period, and existing links keep redirecting on any plan. See [Pricing](/en/pricing) for the current plans, or [Upgrading plans](/docs/en/guides/billing/upgrading-plans) to change yours.

## Next steps

- [Managing links](/docs/en/guides/links/managing-links)
- [Creating QR codes](/docs/en/guides/qr-codes/creating-qr-codes)
- [Creating bio pages](/docs/en/guides/bio-pages/creating-bio-pages)


---

# Shortn guides

These guides walk through the Shortn dashboard task by task: creating and organising short links, designing QR codes for print, publishing bio pages, grouping links into campaigns, reading analytics and managing your plan. Each guide uses the labels you see in the app, so you can follow along with the dashboard open.

## Start here

- [Getting started](/docs/en/guides/getting-started)

## Links

- [Creating links](/docs/en/guides/links/creating-links)
- [Managing links](/docs/en/guides/links/managing-links)
- [UTM parameters](/docs/en/guides/links/utm-parameters)

## QR codes

- [Creating QR codes](/docs/en/guides/qr-codes/creating-qr-codes)
- [Managing QR codes](/docs/en/guides/qr-codes/managing-qr-codes)

## Bio pages

- [Creating bio pages](/docs/en/guides/bio-pages/creating-bio-pages)
- [Customizing bio pages](/docs/en/guides/bio-pages/customizing-bio-pages)

## Campaigns

- [Organizing campaigns](/docs/en/guides/campaigns/organizing-campaigns)

## Analytics

- [Understanding analytics](/docs/en/guides/analytics/understanding-analytics)

## Billing

- [Upgrading plans](/docs/en/guides/billing/upgrading-plans)

## More resources

- [API documentation](/docs): the REST API, webhooks and MCP server, in English.
- [Pricing](/en/pricing): what each plan includes and its current limits.


---

# Creating links

A short link sends visitors to a destination URL and counts each visit. This guide covers the new-link form, from the destination to the advanced redirect rules, and importing links in bulk.

## Create a link

1. Open **Links** in the sidebar and select **New link**. You can also paste a URL anywhere on the Links page outside a text field, or press C.
2. Enter the **Destination URL**. This is the only required field.
3. Fill in any optional fields described below.
4. Select **Create link**.

Shortn copies the new short link to your clipboard and opens its overview. Each link you create counts towards your plan's monthly link limit, shown under **Usage** in the sidebar.

## Title and key suggestions

When you paste a destination, Shortn looks up the page. Once the details arrive, select **Use page title** to fill in the **Title**, or **Use suggested key** to take a readable key. Your own edits are never overwritten. If the page can't be read, enter a title yourself or leave it empty.

## Custom key

The key is the part after the domain, for example `example` in `shortn.at/example`. Leave **Custom key** empty for a random key, or type your own using letters, digits, `-` and `_`. Custom keys depend on your plan; if yours doesn't include them, the form says which plan does.

If you later rename a key, the old key keeps redirecting to the same link.

## Title, description and tags

**Title** and **Description** help you find the link in lists and analytics. Visitors don't see them.

Under **Tags**, search for an existing tag or type a new one to create it. A link can carry up to 50 tags. Tags power the filters on the Links page, in the campaign builder and in the bio page link picker.

## Password

Under **Password**, set a password that visitors must enter before they reach the destination, plus an optional **Hint** shown on the password page. Password protection depends on your plan.

## Advanced options

Expand **Advanced options** for redirect rules. Each group shows a one-line summary of its current setting.

- **Schedule and expiry**: set **Available from (UTC)** and **Expires at (UTC)**, an **Approximate lifetime visit limit**, and a **Fallback URL when unavailable** for visitors who arrive outside that window. The visit limit includes QR scans and is approximate, because simultaneous visits can exceed it.
- **Country targeting**: send visitors from specific countries to other URLs. Use two-letter country codes; the first matching rule wins.
- **Device targeting**: route by operating system, device type, or both.
- **Weighted rotation**: split traffic between up to 10 destinations by relative weight. Returning visitors keep the same destination.
- **App links**: open an iOS or Android app, with a fallback to the web destination or the app store.
- **Redirect and query parameters**: forward incoming query parameters, use a permanent HTTP 301 redirect, and set fixed UTM values. See [UTM parameters](/docs/en/guides/links/utm-parameters).
- **Social preview**: set the title, description and image that link-preview bots show when the link is shared.

## Import links from a CSV file

To create many links at once, select **Import** on the Links page. Upload a UTF-8 CSV file with a header row, up to 50,000 rows, then map its columns, review the validation results and create the valid links. Imported links count towards your plan's monthly limit.

## After you create a link

The link's page has tabs for **Overview**, **Analytics**, **QR code**, **UTM variants** and **Edit**. From the **QR code** tab you can select **Create a QR code** to make a printable code that uses this link.

Changes you save on **Edit** apply from the next click.

## Next steps

- [Managing links](/docs/en/guides/links/managing-links)
- [UTM parameters](/docs/en/guides/links/utm-parameters)
- [Creating QR codes](/docs/en/guides/qr-codes/creating-qr-codes)


---

# Managing links

The Links page is built for large libraries. You can search and filter on the server, select links across pages and apply one change to all of them, archive links you no longer need in view, and restore deleted links within 30 days.

## Find links

Open **Links** in the sidebar. The list shows 25 links per page, with **Previous** and **Next** to move between pages. Use the filters above the list:

- **Search**: matches the key, destination or title.
- **Tag**: pick one or more tags. The tag list searches every tag in the workspace, so it works even with hundreds of tags.
- **Status**: show only links that are **Password-protected**, that **Shows safety page**, or that are **Disabled**.
- **Created**: limit the list to a recent period or a custom date range.
- **Sort by**: **Newest first**, **Oldest first** or **Most clicks**.

Filters are part of the page address, so you can bookmark or share a filtered view.

A **Destination error** flag on a row means weekly checks of that destination returned errors. The checks are informational; the link keeps redirecting.

## Edit a link

1. Select the link to open its overview.
2. Open the **Edit** tab.
3. Change the destination, key, title, description, tags, password or advanced options.
4. Select **Save changes**.

Changes apply from the next click. If you change the key, the old key keeps redirecting to the same link.

## Select links across pages

Tick the checkbox on each row you want, or use **Select this page**. Your selection stays as you move between pages and change filters, so you can build it from several searches. The selection bar shows how many links are selected and the maximum for one action. Use **Clear selection** to start again.

## Bulk actions

With links selected, choose an action. Each one opens its own page where you can review the selected links, filter that review, and confirm. Filters on the review page only change what you see; the action applies to the whole selection.

- **Edit tags**: choose an **Operation**, either **Add tags**, **Remove tags** or **Replace all tags**, pick the tags, then select **Apply tag changes**. **Remove all tags** clears every tag from the selection.
- **Move to campaign**: choose a **Destination campaign** and select **Move selected links**. Each link keeps its UTM source, medium, term and content. This needs a plan with campaigns.
- **Archive**: moves the links out of the active list. See below.
- **Delete selected**: moves the links to Trash.

Members can delete only the links they created. If your selection includes other people's links, the delete page tells you how many it can delete.

## Archive

Archiving tidies the active list without breaking anything. Archived links keep redirecting, and they stay in their QR codes, campaigns and bio pages. Their analytics remain.

To see them, select **Archived** above the list. Select links there and choose **Unarchive** to return them to the active list.

## Delete and restore

Deleting a link stops its short URL immediately, including on printed QR codes. For 30 days:

- the link stays in **Trash**, and you can restore it with its key, settings, campaigns and bio page references;
- its key stays reserved, so nobody else can take it.

After 30 days the link is permanently removed. Aggregate analytics remain.

To restore links:

1. Select **Trash** above the list. The **Restore until** column shows each link's deadline.
2. Select the links you want, across pages if needed.
3. Select **Restore selected**.

In Trash and Archived you can also choose **Group this page** and **By first tag** to group the current page by tag.

## Export

Select **Export** on the Links page to prepare a file of every link that matches your current filters, across all pages. A CSV export can be imported back into Shortn later.

## Next steps

- [UTM parameters](/docs/en/guides/links/utm-parameters)
- [Organizing campaigns](/docs/en/guides/campaigns/organizing-campaigns)
- [Understanding analytics](/docs/en/guides/analytics/understanding-analytics)


---

# UTM parameters

UTM parameters are query tags such as `utm_source=newsletter` that analytics tools read to tell where a visit came from. Shortn handles them in three places: UTM variants on a link, the UTM builder in a campaign, and the redirect settings that decide which parameters reach the destination.

## The five parameters

- `utm_source`: where the traffic comes from, such as `newsletter` or `facebook`.
- `utm_medium`: the type of channel, such as `email` or `social`.
- `utm_campaign`: the campaign the visit belongs to.
- `utm_term`: a paid search keyword. Optional.
- `utm_content`: tells similar links apart, such as `header` and `footer`. Optional.

## Add UTM variants to a link

A variant is a tagged version of one short link for one channel. Visits through it are counted per source, medium and campaign.

1. Open the link and select the **UTM variants** tab.
2. Select **Add variant**.
3. Fill in **Source**, **Medium** and **Campaign**, and optionally **Term** and **Content**. Choosing a saved campaign shows its suggested values.
4. Select **Save variant**.

The variant list shows each **Tagged URL**: your short link with the parameters appended, for example `shortn.at/spring?utm_source=newsletter&utm_medium=email&utm_campaign=spring-sale`. Select **Copy** to copy it.

The **Campaign** value links the variant to the campaign with the same name, which adds the link to that campaign. UTM variants and campaigns depend on your plan.

To change a variant, select **Edit**. To delete it, select **Remove** and confirm with **Remove variant**. URLs you have already shared keep redirecting, and their analytics remain. If it was the link's last variant in a campaign, the link leaves that campaign.

## Tag many links at once

To tag several links for the same channel, use the **UTM builder** tab of a campaign. It always sets `utm_campaign` to the campaign's title, suggests your saved values, and saves the same variant on every link you select. See [Organizing campaigns](/docs/en/guides/campaigns/organizing-campaigns).

## Control what reaches the destination

The tagged URL carries the parameters to Shortn, which records them with each visit. Whether they are passed on to the destination depends on the link's redirect settings.

1. Open the link's **Edit** tab and expand **Advanced options**.
2. Open **Redirect and query parameters**.
3. Choose your settings:
   - **Forward incoming query parameters** passes the visitor's query parameters, including UTM tags, on to the destination.
   - The `utm_source`, `utm_medium`, `utm_campaign`, `utm_term` and `utm_content` fields set fixed values that are always added to the destination.
4. Select **Save changes**.

When the same parameter appears in more than one place, these rules apply:

- a fixed UTM value from the redirect settings always wins;
- otherwise a parameter already in the destination URL is kept over an incoming one;
- incoming parameters are forwarded only when forwarding is on.

## Read UTM results

On the **Analytics** page, the **UTM sources**, **UTM mediums** and **UTM campaigns** breakdowns group visits by tag, and you can filter by any value. Breakdowns depend on your plan. See [Understanding analytics](/docs/en/guides/analytics/understanding-analytics).

## Good practice

- Write values in lowercase and keep one spelling per channel, so results for the same channel aren't split across several values.
- Use `utm_content` to compare placements of the same link, such as a header and a footer.
- Don't add UTM tags to links that only point inside your own site; they overwrite the original source of the visit.

## Next steps

- [Organizing campaigns](/docs/en/guides/campaigns/organizing-campaigns)
- [Managing links](/docs/en/guides/links/managing-links)


---

# Creating QR codes

Every QR code in Shortn opens a short link. The pattern printed on paper never changes, so you can change where the code goes after printing and still count every scan. This guide covers creating a code and getting a first file out.

## Before you start

Each QR code you create counts towards your plan's monthly QR code limit, shown under **Usage** in the sidebar. A code can point at:

- **a URL**: Shortn creates a short link for the code;
- **one of your links**: the code is attached to that link and opens it.

## Create a QR code

1. Open **QR codes** in the sidebar and select **New QR code**.
2. Under **Destination URL**, enter where a scan should take people. To use an existing link instead, select **Choose a link** under **Or use one of your links:** and pick it from the list. **Use a URL instead** switches back.
3. Optionally add a **Title** to help you find the code later, and **Tags**.
4. Select **Create QR code**.

The code's overview opens. The preview on the form uses a sample short link; your code gets its own short link when you create it.

You can also start from a link: open the link, select its **QR code** tab and select **Create a QR code**. The code is attached to that link, and you'll find it on that tab afterwards. If the link already has a code, the new one replaces it as the link's code, and the old code keeps scanning.

## Design the code

Select **Edit design** on the overview, or open the **Design** tab. You can change the colours, pattern, corner styles, error correction, logo, frame and caption. The scan check on the same page warns you about low contrast or a logo that is too large. Select **Save design** when you're done.

Design changes never change the link inside the code. [Managing QR codes](/docs/en/guides/qr-codes/managing-qr-codes) explains each option.

## Download the code

On the overview, under **Download**, select **Export for print**:

1. Choose a **Format**: SVG, PNG or PDF. SVG is a vector format and scales to any print size.
2. Choose a **Size** in pixels.
3. Select **Prepare download**, then **Download file** when it is ready.

The export page shows the recommended minimum print size for the code. For screens and email, open **JPEG for screens** on the overview.

## Test before you print

- Scan the downloaded file with two or three different phones.
- Keep clear space around the code; frames and captions are placed outside it.
- Keep strong contrast between the code and its background, with a dark code on a light background.
- Don't print the code smaller than the recommended minimum size.
- Add a short call to action near the code so people know what they'll get.

## Change the destination later

Open the **Details** tab, change the **Destination URL** and select **Save details**. Printed copies follow the new destination from the next scan. If the code is attached to a link, select **Also update the link** to copy the new destination or title to that link too.

## Next steps

- [Managing QR codes](/docs/en/guides/qr-codes/managing-qr-codes)
- [Understanding analytics](/docs/en/guides/analytics/understanding-analytics)


---

# Managing QR codes

This guide covers the design options for a QR code, exporting files for print, exporting many codes at once as a ZIP file, and deleting codes.

## Find your codes

Open **QR codes** in the sidebar. Use **Search** to match the title, destination or code, filter by **Tag** and **Created**, and switch the **Layout** between **Grid** and **List**. Select a code to open its overview, where the **Overview**, **Design**, **Scans** and **Details** tabs live.

## Design options

Open the **Design** tab. The preview updates as you change options and notes when it includes unsaved changes. Select **Save design** to apply them.

- **Colours**: pick a preset or set the **Code** and **Background** colours. Under **Corners**, keep the code colour or choose a separate corner colour.
- **Pattern**: choose the shape of the **Dots**, **Corner squares** and **Corner dots**. Shapes change the look only; the link inside stays the same.
- **Error correction**: choose a **Level**. Higher levels let a code survive scuffs and a logo, at the cost of a denser pattern.
- **Logo**: upload an image to draw in the middle of the code, set the **Logo size**, and optionally **Hide the pattern behind the logo**. Keep the logo at 50% of the error correction or less. Logos depend on your plan.
- **Shape and caption**: choose a square or circular **Shape**, a **Frame** (none, outline or solid) and an optional **Caption** of up to 64 characters. Frames and captions stay outside the clear space around the code, and both appear in downloads.

The **Scan check** panel rates the contrast between code and background and warns about inverted colours, a transparent background and logo size. Treat its warnings as a prompt to test with real phones before printing.

## Export for print

1. On the code's overview, under **Download**, select **Export for print**.
2. Choose a **Format** (SVG, PNG or PDF) and a **Size**.
3. Read the print guidance on the page: four quiet modules around the code, error correction Q (H with a logo), at least 3:1 contrast, and the recommended minimum print size at 300 dpi.
4. Select **Prepare download**, then **Download file**.

Downloads expire 24 hours after you start the export. For screen use, **JPEG for screens** on the overview gives you a JPEG straight away.

## Export several codes as a ZIP

1. On the QR codes list, tick the codes you want, or use **Select this page**. Your selection stays as you change pages and filters.
2. Select **Export selected**.
3. Review the selection. Filters on this page only change the list you see; the ZIP includes every selected code.
4. Choose a **Format** and **Size**, then select **Create ZIP**.
5. When the export is ready, select **Download ZIP**.

The export page shows how many codes fit in one ZIP file. You can leave the page while the export runs; it continues in the background.

## Edit details and destination

The **Details** tab holds the **Destination URL**, **Title** and **Tags**. Changing the destination doesn't change the printed pattern; printed copies follow the new destination from the next scan. Select **Save details** to apply.

## Scans

The overview summarises the last 30 days: scans, visitors, all-time scans and the date of the last scan. The **Scans** tab opens the full analytics for the code. Scans also appear in the workspace **Analytics** page, separate from link clicks. See [Understanding analytics](/docs/en/guides/analytics/understanding-analytics).

## Delete QR codes

Select **Delete QR code** on the overview, or select codes in the list and choose **Delete selected**. The confirmation page lists what happens:

- the code stops scanning right away, because its short link is deleted with it;
- links the code was attached to keep working and only lose their QR code;
- scan history is archived and no longer shown.

Deleting a QR code can't be undone, and printed copies can't be brought back. Members can delete only the codes they created.

## Next steps

- [Creating QR codes](/docs/en/guides/qr-codes/creating-qr-codes)
- [Creating bio pages](/docs/en/guides/bio-pages/creating-bio-pages)


---

# Compreender as estatísticas

O Shortn conta cada visita aos seus links curtos e códigos QR. Este guia explica o que é contado, como restringir os números com intervalos, análises e filtros, como os exportar e como os dados dos visitantes são protegidos.

## Cliques e leituras

O Shortn regista dois tipos de visita:

- um **clique** é uma visita através de um link curto, seja escrito, tocado ou aberto a partir de outro site;
- uma **leitura** é uma visita através de um dos seus códigos QR.

O filtro **Tipo** alterna entre **Cliques e leituras**, **Cliques** e **Leituras QR**. O resumo no topo mostra também os **Visitantes únicos** e o **País principal**.

## Onde encontrar as estatísticas

- **Estatísticas**, na barra lateral, abrange todo o espaço de trabalho.
- O separador **Estatísticas** de um link abrange esse link.
- O separador **Leituras** de um código QR abrange esse código.
- O separador **Estatísticas** de uma campanha abrange os links dessa campanha.
- O separador **Estatísticas** de uma página bio mostra as visualizações, os cliques e a taxa de cliques da página.

Todos, exceto o das páginas bio, partilham os mesmos controlos.

## Escolher um intervalo de datas

1. Abra **Intervalo de datas** e escolha **Últimos 7 dias**, **Últimos 30 dias**, **Últimos 90 dias** ou **Últimos 12 meses**, ou defina um **Intervalo personalizado** com as datas **De** e **Até**.
2. Escolha o **Intervalo** para agrupar o gráfico por hora, dia, semana ou mês.

Até onde pode recuar depende do histórico de estatísticas do plano. Quando um intervalo começa mais cedo, a página indica a data a partir da qual os dados ficam fora do período do plano e mostra o que estiver disponível. Consulte os [Preços](/pt/pricing) para ver o histórico de cada plano.

## Análises

Por baixo do gráfico, as análises agrupam as visitas por:

- **Links**
- **Países** e **Cidades**
- **Origens**: o site de onde veio a visita. Direto significa que não houve site de origem, como num endereço escrito, numa aplicação ou numa leitura de código QR.
- **Dispositivos**, **Navegadores** e **Sistemas operativos**
- **Origens UTM**, **Meios UTM** e **Campanhas UTM**
- **Regras de destino**: a regra de redirecionamento que escolheu o destino, nos links com regras por país, por dispositivo ou de rotação.

Cada análise mostra as linhas principais; selecione **Ver tudo** para a lista completa. As análises além dos links, e a filtragem por elas, dependem do plano.

## Filtrar a vista

Selecione um valor numa análise para filtrar a página inteira, ou abra os filtros para combinar vários, por exemplo um link, um país e dispositivos móveis. Selecione **Limpar tudo** para os retirar.

Todos os filtros ficam no endereço da página, por isso pode guardar uma vista nos favoritos ou partilhá-la com um colega que tenha acesso ao espaço de trabalho.

## Exportar estatísticas

1. Defina o intervalo e os filtros pretendidos.
2. Selecione **Exportar estatísticas**.
3. Em **Dados**, escolha **Agregados** para totais por período e por grupo, ou **Eventos individuais** para uma linha por visita.
4. Escolha o formato (CSV, ou NDJSON para eventos individuais) e selecione **Preparar exportação**.

As exportações correm em segundo plano, por isso pode sair da página. Os ficheiros prontos ficam no **Histórico de exportações**. Ficheiros e histórico expiram 24 horas após a preparação. A exportação mantém o período revisto, mesmo que o plano mude antes de ser executada. A exportação CSV depende do plano, e a exportação de campanhas tem um requisito de plano próprio.

## Privacidade

- Os endereços IP dos visitantes são guardados em forma de hash.
- Os endereços IP originais são apagados ao fim de 90 dias.
- As exportações de eventos individuais excluem endereços IP, hashes de visitantes e agentes do utilizador.
- O conteúdo incorporado nas páginas bio só é carregado depois de o visitante o pedir.

## Ler os números

- Compare o mesmo intervalo entre links, em vez de totais de intervalos com durações diferentes.
- Use as análises UTM para ver que canal resultou, e as regras de destino para comparar os destinos de um mesmo link.

## Próximos passos

- [Parâmetros UTM](/docs/pt/guides/links/utm-parameters)
- [Organizar campanhas](/docs/pt/guides/campaigns/organizing-campaigns)
- [Mudar de plano](/docs/pt/guides/billing/upgrading-plans)


---

# Mudar de plano

Cada espaço de trabalho tem o seu próprio plano. Este guia explica onde consultar o plano e a utilização, como subir, descer ou cancelar o plano, como funciona o pagamento e o que acontece aos links quando o plano muda. Só o proprietário do espaço de trabalho pode alterar o plano ou ver as faturas.

## Consultar o plano e a utilização

Abra **Definições** e selecione **Faturação**. A página tem três partes:

- **Plano**: o plano atual e a data em que é renovado ou termina. No plano Free, mostra quando a utilização é reposta.
- **Utilização**: links e códigos QR criados neste período e o total de páginas bio, face aos limites do plano.
- **Faturas**: o histórico de pagamentos, com a opção **Descarregar** em cada fatura.

Os limites de links e de códigos QR são repostos em cada período de faturação. Quando atinge um limite, a página avisa, e pode mudar de plano para continuar a criar.

## Comparar planos

Selecione **Comparar planos** para ver todos os planos lado a lado: limites, histórico de estatísticas, membros, espaços de trabalho e funcionalidades como chaves personalizadas, campanhas, análises detalhadas, exportação CSV e logótipos em códigos QR. A mesma informação está na página de [Preços](/pt/pricing).

## Preços e moeda

Os planos são mensais e cobrados em dólares americanos. O Shortn mostra uma estimativa em euros junto de cada preço, calculada com as taxas de referência do Banco Central Europeu. O pagamento cobra o valor em dólares; o valor em euros é apenas uma estimativa.

## Subir de plano

1. Abra **Comparar planos**.
2. Selecione **Mudar para …** no plano pretendido.
3. A partir do plano Free, segue para o pagamento. Indique aí os dados de pagamento e confirme.
4. De volta ao Shortn, a página de faturação mostra o novo plano em poucos segundos, assim que o fornecedor de pagamentos o confirmar. Selecione **Atualizar** se ainda não tiver mudado.

Se já paga um plano, a subida abre uma página de confirmação em vez do pagamento. Selecione **Mudar para o …** para confirmar. O novo plano começa assim que o fornecedor de faturação confirmar, e é cobrada a diferença proporcional do período atual.

## Descer de plano

1. Abra **Comparar planos** e selecione **Mudar para …** num plano inferior.
2. Leia a página de confirmação e selecione **Mudar para o …**.

Mantém o plano atual até ao fim do período. A partir daí aplicam-se os limites e as funcionalidades do plano inferior. Para ficar no plano atual, selecione **Manter o …**.

## Cancelar

1. Na página de faturação, selecione **Cancelar plano**.
2. Confirme na página seguinte.

Mantém o plano até ao fim do período atual; depois, o espaço de trabalho passa para o plano Free. Até essa data, pode selecionar **Manter o …** para retomar o plano normalmente.

## Os links continuam a funcionar

Os limites do plano só controlam o que pode ser criado. Ao descer de plano ou cancelar, os links, códigos QR e páginas bio existentes continuam a redirecionar. As funcionalidades que o novo plano não inclui deixam de estar disponíveis para novas alterações. Por exemplo, se o plano deixar de incluir logótipos em códigos QR, pode manter um logótipo existente ou removê-lo, mas não acrescentar um novo.

## Métodos de pagamento e pagamentos falhados

Selecione **Abrir portal de faturação** para atualizar os dados de faturação ou o método de pagamento. Se um pagamento falhar, a página de faturação mostra um aviso com **Atualizar método de pagamento**, para que possa manter o plano.

## Extra de domínio personalizado

Os domínios personalizados são vendidos como extra, à parte do plano. Cada extra inclui um domínio. Compre-o em **Definições**, na secção **Domínios**, com **Comprar extra de domínio**, e ligue lá o domínio. Consulte [Personalizar páginas bio](/docs/pt/guides/bio-pages/customizing-bio-pages).

## Enterprise

Os planos Enterprise têm condições acordadas com o Shortn. Para alterar um plano Enterprise, contacte o Shortn.

## Próximos passos

- [Preços](/pt/pricing)
- [Primeiros passos](/docs/pt/guides/getting-started)


---

# Criar páginas bio

Uma página bio reúne os seus links numa única página pública, do tipo que se coloca no perfil de uma rede social. Os visitantes veem o perfil, os links e as restantes secções que acrescentar, e cada clique é contado. Este guia explica como criar uma página e publicar a primeira versão.

## Antes de começar

O número de páginas bio por espaço de trabalho depende do plano; a lista de páginas bio mostra quantas já estão em uso. Os links de uma página bio são links normais do Shortn, por isso aparecem em **Links** e nas estatísticas como os restantes.

## Criar uma página

1. Abra **Páginas bio** na barra lateral e selecione **Nova página de bio**.
2. Em **Endereço**, indique o endereço que as pessoas vão visitar: de 3 a 63 letras minúsculas, números e hífenes. O Shortn verifica se está livre enquanto escreve.
3. Indique, se quiser, um **Título**, mostrado no topo da página. Se o deixar vazio, é usado o endereço.
4. Em **Primeiro link**, pode selecionar **Escolher um link** para colocar já um dos seus links na página.
5. Selecione **Criar página**.

Abre-se o resumo da página, com **Copiar URL** e **Abrir página** para o endereço público.

## Construir a página

No resumo, selecione **Editar página bio**. O editor mostra uma pré-visualização em direto; alterne entre **Pré-visualização móvel** e **Pré-visualização de computador** para verificar as duas.

1. Selecione **Adicionar secção** e escolha o que acrescentar. Os tipos de secção são **Perfil**, **Link**, **Redes sociais**, **Título de secção**, **Texto**, **Imagem**, **Conteúdo incorporado**, **Código QR** e **Links**.
2. Selecione uma secção na pré-visualização para a editar. Arraste a pega para mudar a ordem.
3. Para acrescentar vários links de uma vez, adicione uma secção **Links** e selecione **Gerir links**. Pode filtrar por etiquetas, pesquisar e selecionar links em várias páginas antes de os acrescentar.
4. Para acrescentar um URL que ainda não é um link curto, escreva-o numa secção de link e selecione **Encurtar e adicionar link**. Isto cria um novo link no espaço de trabalho.

As alterações são guardadas num rascunho enquanto trabalha. Os visitantes só as veem depois de publicar.

## Publicar

Selecione **Publicar** quando o rascunho estiver pronto. A página fica disponível no endereço de imediato. Cada publicação cria uma versão a que pode regressar mais tarde; consulte [Personalizar páginas bio](/docs/pt/guides/bio-pages/customizing-bio-pages).

## Partilhar a página

Selecione **Copiar URL** no resumo e cole o endereço nos perfis das redes sociais, na assinatura de e-mail ou em material impresso. Para mudar o nome da página no endereço, abra **Definições da página** no editor e use **Alterar identificador**. Os endereços anteriores continuam a funcionar depois da alteração.

## Estatísticas da página bio

O separador **Estatísticas** de uma página bio mostra os últimos 30 dias: **Visualizações**, **Cliques**, **Taxa de cliques**, **Cliques por bloco** e as principais origens. Os links da página também são contados na página **Estatísticas** do espaço de trabalho.

## Eliminar uma página

No resumo, selecione **Eliminar página de bio** e confirme. O endereço público deixa de funcionar de imediato e fica livre para qualquer pessoa. Os links que estavam na página continuam a funcionar.

## Sugestões

- Coloque primeiro o link mais importante, ou destaque-o.
- Use textos curtos nos botões, para que caibam num ecrã de telemóvel.
- Verifique a pré-visualização móvel antes de publicar, já que as páginas bio costumam ser abertas a partir de uma aplicação social no telemóvel.

## Próximos passos

- [Personalizar páginas bio](/docs/pt/guides/bio-pages/customizing-bio-pages)
- [Gerir links](/docs/pt/guides/links/managing-links)


---

# Personalizar páginas bio

O editor de páginas bio trabalha sobre um rascunho: altera as secções, o tema e as definições, confirma na pré-visualização em direto e publica quando estiver pronto. Este guia descreve cada parte do editor, o agendamento, as versões e a ligação de um domínio próprio.

Abra uma página bio e selecione **Editar página bio**. A barra de ferramentas tem **Adicionar secção**, **Secções**, **Tema**, **Definições da página** e **Histórico**.

## Secções

Selecione **Adicionar secção** para acrescentar uma destas:

- **Perfil**: avatar, título, descrição e alinhamento. É possível escolher a forma do avatar.
- **Link**: um botão para um link. Ative **Destacar este link** para o fazer sobressair e acrescente uma miniatura.
- **Links**: um grupo de links. Selecione **Gerir links** para filtrar por etiquetas, pesquisar, selecionar links em várias páginas e acrescentá-los de uma vez. Em **Apresentação dos links**, escolha um esquema como lista vertical, cartões paginados ou grelhas de imagens.
- **Redes sociais**: ícones com ligação para os seus perfis noutras plataformas.
- **Título de secção** e **Texto**: títulos e parágrafos curtos entre os links.
- **Imagem**: uma imagem com descrição e ligação opcional.
- **Conteúdo incorporado**: conteúdo do YouTube, Spotify, SoundCloud, Vimeo ou Apple Music. Os visitantes veem um botão para o carregar, com a indicação de que isso partilha o endereço IP com o fornecedor.
- **Código QR**: um código QR para um dos seus links.

Selecione uma secção na pré-visualização para a editar e arraste a pega para mudar a ordem. **Secções** lista todas as secções pela ordem atual.

## Mostrar secções de forma agendada

Cada secção tem o interruptor **Visível** e as datas opcionais **Mostrar a partir de (UTC)** e **Mostrar até (UTC)**. Servem, por exemplo, para mostrar um link de bilhetes só durante um evento, ou para esconder uma secção sem a apagar. O agendamento está disponível nos planos pagos.

## Tema

Abra **Tema** para mudar o aspeto da página:

1. Escolha um **Tema predefinido** como ponto de partida, ou **Personalizado**.
2. Ajuste as cores: **Fundo**, **Superfície**, **Texto** e **Destaque**.
3. Escolha o **Tipo de letra**, o **Estilo dos botões**, o **Raio dos cantos** e o **Espaçamento**.
4. Acrescente, se quiser, uma **Imagem de capa (opcional)** por trás do perfil.

Os temas têm limites para que as páginas se mantenham legíveis. Se o contraste do texto for insuficiente, o editor avisa; selecione **Usar cor acessível** para o corrigir.

## Definições da página

Em **Definições da página** estão as opções que valem para a página inteira:

- **Identificador**: o nome da página no endereço. Selecione **Alterar identificador** para o mudar; os endereços anteriores continuam a funcionar.
- **Idioma da página**: o idioma em que a página está escrita.
- **Ocultar rodapé Shortn**: retira o rodapé "Criado com Shortn". Disponível nos planos pagos.
- **Aspeto na pesquisa**: ative **Ocultar dos motores de pesquisa** para manter a página fora dos resultados de pesquisa.

## Rascunhos e publicação

As alterações são guardadas no rascunho pouco depois de parar de escrever, e é possível guardar a qualquer momento com **Guardar rascunho**. A pré-visualização mostra o rascunho; os visitantes continuam a ver a página publicada até selecionar **Publicar**.

## Versões

Cada publicação cria uma versão. Abra **Histórico** para ver as **Versões publicadas (últimas 20)**. Para voltar atrás, selecione **Restaurar versão … como rascunho** na versão pretendida. A versão é copiada para o rascunho; selecione **Publicar** para a tornar pública de novo.

## Usar um domínio próprio

Uma página bio pode ser servida a partir de um domínio próprio, como `links.exemplo.pt`, com o extra de domínio personalizado. Cada extra inclui um domínio.

1. Abra **Definições** e selecione **Domínios**.
2. Se não tiver domínios disponíveis, selecione **Comprar extra de domínio**.
3. Em **Adicionar domínio**, indique o **Nome do domínio**, escolha a opção em **Usar para**, escolha a **Página bio** e selecione **Adicionar domínio**.
4. Siga as **Instruções DNS**: crie no fornecedor de DNS o registo CNAME indicado. Para um domínio de raiz, use CNAME flattening.
5. Aguarde até o estado mudar para **Ativo**. Se a verificação falhar, confirme o registo e selecione **Repetir verificação**.

Só quem pode gerir a faturação pode adicionar domínios.

## Próximos passos

- [Criar páginas bio](/docs/pt/guides/bio-pages/creating-bio-pages)
- [Mudar de plano](/docs/pt/guides/billing/upgrading-plans)


---

# Organizar campanhas

Uma campanha agrupa as variantes UTM dos seus links para que os cliques possam ser lidos em conjunto. O título da campanha é o valor de `utm_campaign`, a campanha guarda valores sugeridos para os restantes parâmetros UTM e o construtor de UTM etiqueta muitos links de uma vez. As campanhas dependem do plano; se o seu não as incluir, a página de campanhas indica o plano que as inclui.

## Criar uma campanha

1. Abra **Campanhas** na barra lateral e selecione **Nova campanha**.
2. Indique um **Título**. É usado como `utm_campaign` nos URL que etiquetar, por isso escreva-o como deve aparecer nas estatísticas, por exemplo `saldos-primavera`.
3. Acrescente, se quiser, uma **Descrição**.
4. Selecione **Criar campanha**.

A campanha abre com os separadores **Resumo**, **Estatísticas**, **Construtor de UTM** e **Definições**.

## Definir valores UTM sugeridos

Abra **Definições** e preencha as **Predefinições UTM**: listas de origens, meios, termos e conteúdos sugeridos. Escreva um valor e prima Enter para o acrescentar. Os valores são guardados em minúsculas. O construtor de UTM e o formulário de variantes do link sugerem-nos, o que mantém a mesma grafia em toda a equipa.

Mudar o título da campanha atualiza as variantes guardadas. Os URL já partilhados mantêm o título antigo, e os respetivos cliques deixam de contar na campanha.

## Acrescentar links com o construtor de UTM

1. Abra **Construtor de UTM**, ou selecione **Construir um URL etiquetado** no resumo.
2. Em **Links**, selecione **Explorar ligações**. Filtre por etiquetas, pesquise ou agrupe os resultados por etiqueta. Marque links em quantas páginas precisar, ou use **Selecionar todos os resultados**, e depois selecione **Usar … ligações selecionadas**.
3. Preencha **Origem**, **Meio** e, se quiser, **Termo** e **Conteúdo**. O `utm_campaign` é sempre o título da campanha.
4. Verifique a pré-visualização do **URL etiquetado** e selecione **Copiar URL** se precisar dele já.
5. Selecione **Guardar como variante**. Com vários links selecionados, o botão grava a mesma variante em todos.

Um link entra na campanha quando uma das suas variantes UTM usa essa campanha. Enquanto a variante não for guardada, os cliques do link não contam na campanha.

Também é possível acrescentar variantes link a link, no separador **Variantes UTM** de cada link; consulte [Parâmetros UTM](/docs/pt/guides/links/utm-parameters).

## Mover links existentes para uma campanha

Em **Links**, selecione os links pretendidos, em várias páginas e filtros, e escolha **Mover para campanha**. Escolha a **Campanha de destino** e selecione **Mover ligações selecionadas**. Cada variante UTM mantém a origem, o meio, o termo e o conteúdo, e as predefinições da campanha não os substituem.

## Ler os resultados da campanha

O **Resumo** mostra os cliques, as leituras e os visitantes dos últimos 30 dias, e **Links e variantes** lista cada link com os seus URL etiquetados e cliques. Selecione **Copiar utm_campaign** para copiar o valor exato.

O separador **Estatísticas** abre as estatísticas completas filtradas pela campanha, com os mesmos intervalos de datas, análises e filtros da página **Estatísticas** do espaço de trabalho. A exportação CSV das estatísticas de uma campanha depende do plano. Consulte [Compreender as estatísticas](/docs/pt/guides/analytics/understanding-analytics).

## Encontrar campanhas

A lista de campanhas tem o campo **Pesquisar**, para títulos e descrições, e **Ordenar por**, com as opções mais recentes, mais antigas, mais links ou título.

## Eliminar uma campanha

No resumo, selecione **Eliminar campanha** e confirme. As variantes UTM são retiradas dos links e as predefinições UTM são apagadas. Os links continuam a funcionar. Os URL já partilhados com esse `utm_campaign` mantêm os cliques, mas nenhuma página de campanha os mostra.

## Sugestões

- Combine com a equipa um esquema de nomes para os títulos, como `estacao-ano-produto`, antes de começar a etiquetar.
- Use `utm_content` para comparar posições do mesmo link dentro de um canal.

## Próximos passos

- [Parâmetros UTM](/docs/pt/guides/links/utm-parameters)
- [Compreender as estatísticas](/docs/pt/guides/analytics/understanding-analytics)


---

# Primeiros passos no Shortn

O Shortn transforma URL longos em links curtos que pode partilhar, imprimir como códigos QR ou reunir numa página bio, e conta cada clique e cada leitura. Este guia explica como criar uma conta, como se orientar no painel e como criar o primeiro link curto.

## Criar a conta

1. Aceda a [app.shortn.at/register](https://app.shortn.at/register).
2. Indique o nome completo, um nome de utilizador, o e-mail e uma palavra-passe e selecione **Criar conta**. Se a página mostrar **Continuar com Google** ou **Continuar com GitHub**, também pode usar uma dessas opções.
3. Abra o e-mail de verificação e siga a ligação. A ligação é válida durante uma hora. Se tiver expirado, ou se o e-mail não chegar, peça outra com **Reenviar e-mail**.
4. Inicie sessão em [app.shortn.at/login](https://app.shortn.at/login) com o e-mail ou o nome de utilizador.

A conta começa com um espaço de trabalho pessoal no plano Free.

## Orientar-se no painel

A barra lateral reúne as secções principais:

- **Links**: todos os links curtos do espaço de trabalho, com pesquisa, filtros por etiqueta, arquivo e lixo.
- **Estatísticas**: cliques e leituras de códigos QR em todo o espaço de trabalho.
- **Códigos QR**: os códigos que desenha, transfere e imprime.
- **Páginas bio**: páginas de links com endereço público próprio.
- **Campanhas**: grupos de links com etiquetas UTM, para ler em conjunto.
- **Definições**: dados do espaço de trabalho, membros, faturação, domínios, chaves de API e a sua conta.

O painel **Utilização** mostra quanto já foi gasto do limite mensal do plano, por exemplo em **Links este mês**.

Fora dos campos de texto há alguns atalhos de teclado: Ctrl+K (Cmd+K no Mac) abre a **Paleta de comandos**, C inicia um novo link e ? mostra todos os atalhos em **Atalhos de teclado**.

O idioma e o tema do painel mudam-se no menu da conta.

## Criar o primeiro link curto

1. Abra **Links** na barra lateral.
2. Selecione **Novo link** ou cole um URL em qualquer ponto da página de links, fora de um campo de texto. Em ambos os casos abre-se o formulário de criação.
3. Indique o **URL de destino**. O Shortn consulta a página e pode sugerir um título e uma chave; para os aceitar, selecione **Usar título da página** ou **Usar chave sugerida**.
4. Selecione **Criar link**.

O link é criado e copiado para a área de transferência, e abre-se a respetiva página de resumo. Aí pode copiá-lo de novo com **Copiar link curto**, abrir o separador **Estatísticas** ou criar um código QR. O guia [Criar links](/docs/pt/guides/links/creating-links) descreve todos os campos do formulário.

## Trabalhar em equipa

Cada espaço de trabalho tem os seus próprios links, membros e faturação. Para criar um espaço de trabalho de equipa, abra **Definições**, entre em **Geral** e selecione **Criar espaço de trabalho** em **Espaços de trabalho de equipa**. Fica como proprietário e pode convidar pessoas em **Membros**. Cada membro tem uma de quatro funções: proprietário, administrador, membro ou leitor. Os leitores veem os links, mas não os podem criar nem alterar.

O número de espaços de trabalho e de membros depende do plano.

## Planos

O plano Free inclui um limite mensal de links e de códigos QR e uma página bio. Os planos pagos aumentam os limites e acrescentam funcionalidades como chaves personalizadas, campanhas, análises detalhadas e exportação CSV. Os limites são repostos em cada período de faturação, e os links existentes continuam a redirecionar em qualquer plano. Consulte os [Preços](/pt/pricing) para ver os planos atuais, ou [Mudar de plano](/docs/pt/guides/billing/upgrading-plans) para alterar o seu.

## Próximos passos

- [Gerir links](/docs/pt/guides/links/managing-links)
- [Criar códigos QR](/docs/pt/guides/qr-codes/creating-qr-codes)
- [Criar páginas bio](/docs/pt/guides/bio-pages/creating-bio-pages)


---

# Guias do Shortn

Estes guias explicam o painel do Shortn tarefa a tarefa: criar e organizar links curtos, preparar códigos QR para impressão, publicar páginas bio, agrupar links em campanhas, ler as estatísticas e gerir o plano. Cada guia usa os nomes que aparecem na aplicação, para que possa acompanhar com o painel aberto.

## Para começar

- [Primeiros passos](/docs/pt/guides/getting-started)

## Links

- [Criar links](/docs/pt/guides/links/creating-links)
- [Gerir links](/docs/pt/guides/links/managing-links)
- [Parâmetros UTM](/docs/pt/guides/links/utm-parameters)

## Códigos QR

- [Criar códigos QR](/docs/pt/guides/qr-codes/creating-qr-codes)
- [Gerir códigos QR](/docs/pt/guides/qr-codes/managing-qr-codes)

## Páginas bio

- [Criar páginas bio](/docs/pt/guides/bio-pages/creating-bio-pages)
- [Personalizar páginas bio](/docs/pt/guides/bio-pages/customizing-bio-pages)

## Campanhas

- [Organizar campanhas](/docs/pt/guides/campaigns/organizing-campaigns)

## Estatísticas

- [Compreender as estatísticas](/docs/pt/guides/analytics/understanding-analytics)

## Faturação

- [Mudar de plano](/docs/pt/guides/billing/upgrading-plans)

## Mais recursos

- [Documentação da API](/docs): a API REST, os webhooks e o servidor MCP, em inglês.
- [Preços](/pt/pricing): o que cada plano inclui e os limites atuais.


---

# Criar links

Um link curto envia os visitantes para um URL de destino e conta cada visita. Este guia percorre o formulário de criação, do destino às regras avançadas de redirecionamento, e explica como importar links em massa.

## Criar um link

1. Abra **Links** na barra lateral e selecione **Novo link**. Também pode colar um URL em qualquer ponto da página de links, fora de um campo de texto, ou premir C.
2. Indique o **URL de destino**. É o único campo obrigatório.
3. Preencha, se quiser, os campos opcionais descritos abaixo.
4. Selecione **Criar link**.

O Shortn copia o novo link curto para a área de transferência e abre o respetivo resumo. Cada link criado conta para o limite mensal de links do plano, visível em **Utilização** na barra lateral.

## Sugestões de título e de chave

Ao colar um destino, o Shortn consulta a página. Quando os dados chegam, selecione **Usar título da página** para preencher o **Título**, ou **Usar chave sugerida** para adotar uma chave legível. O que tiver escrito nunca é substituído. Se a página não puder ser lida, escreva um título ou deixe o campo vazio.

## Chave personalizada

A chave é a parte que vem depois do domínio, por exemplo `exemplo` em `shortn.at/exemplo`. Deixe **Chave personalizada** vazia para obter uma chave aleatória, ou escreva uma com letras, algarismos, `-` e `_`. As chaves personalizadas dependem do plano; se o seu não as incluir, o formulário indica o plano que as inclui.

Se mais tarde mudar a chave, a chave antiga continua a redirecionar para o mesmo link.

## Título, descrição e etiquetas

O **Título** e a **Descrição** ajudam a encontrar o link nas listas e nas estatísticas. Os visitantes não os veem.

Em **Etiquetas**, pesquise uma etiqueta existente ou escreva uma nova para a criar. Cada link pode ter até 50 etiquetas. As etiquetas alimentam os filtros da página de links, do construtor de campanhas e do seletor de links das páginas bio.

## Palavra-passe

Em **Palavra-passe**, defina uma palavra-passe que os visitantes têm de introduzir antes de chegar ao destino e, opcionalmente, uma **Dica** mostrada na página de acesso. A proteção por palavra-passe depende do plano.

## Opções avançadas

Abra **Opções avançadas** para configurar regras de redirecionamento. Cada grupo mostra um resumo de uma linha com a configuração atual.

- **Agendamento e expiração**: defina **Disponível a partir de (UTC)** e **Expira em (UTC)**, um **Limite aproximado de visitas acumuladas** e um **URL alternativa quando indisponível** para quem chega fora desse período. O limite inclui leituras de códigos QR e é aproximado, porque visitas simultâneas podem ultrapassá-lo.
- **Destino por país**: envia visitantes de determinados países para outros URL. Use códigos de país de duas letras; aplica-se a primeira regra que corresponder.
- **Destino por dispositivo**: encaminha por sistema operativo, por tipo de dispositivo ou por ambos.
- **Rotação ponderada**: divide o tráfego entre até 10 destinos segundo pesos relativos. Quem regressa mantém o mesmo destino.
- **Links de aplicações**: abre uma aplicação iOS ou Android, com alternativa para o destino web ou para a loja de aplicações.
- **Redirecionamento e parâmetros**: encaminha os parâmetros recebidos, usa um redirecionamento permanente HTTP 301 e fixa valores UTM. Consulte [Parâmetros UTM](/docs/pt/guides/links/utm-parameters).
- **Pré-visualização social**: define o título, a descrição e a imagem que os robôs de pré-visualização mostram quando o link é partilhado.

## Importar links de um ficheiro CSV

Para criar muitos links de uma vez, selecione **Importar** na página de links. Carregue um ficheiro CSV em UTF-8 com linha de cabeçalho e até 50 000 linhas, associe as colunas, reveja o resultado da validação e crie os links válidos. Os links importados contam para o limite mensal do plano.

## Depois de criar o link

A página do link tem os separadores **Resumo**, **Estatísticas**, **Código QR**, **Variantes UTM** e **Editar**. No separador **Código QR**, selecione **Criar um código QR** para gerar um código imprimível que usa este link.

As alterações guardadas em **Editar** aplicam-se a partir do clique seguinte.

## Próximos passos

- [Gerir links](/docs/pt/guides/links/managing-links)
- [Parâmetros UTM](/docs/pt/guides/links/utm-parameters)
- [Criar códigos QR](/docs/pt/guides/qr-codes/creating-qr-codes)


---

# Gerir links

A página de links foi pensada para bibliotecas grandes. É possível pesquisar e filtrar no servidor, selecionar links em várias páginas e aplicar a mesma alteração a todos, arquivar o que já não precisa de ver e restaurar links eliminados durante 30 dias.

## Encontrar links

Abra **Links** na barra lateral. A lista mostra 25 links por página, com **Anterior** e **Seguinte** para navegar. Use os filtros por cima da lista:

- **Pesquisar**: procura na chave, no destino ou no título.
- **Etiqueta**: escolha uma ou mais etiquetas. A lista pesquisa todas as etiquetas do espaço de trabalho, por isso funciona mesmo com centenas delas.
- **Estado**: mostra apenas os links **Protegido por palavra-passe**, os que **Mostra página de segurança** ou os **Desativado**.
- **Criado**: limita a lista a um período recente ou a um intervalo de datas personalizado.
- **Ordenar por**: **Mais recentes**, **Mais antigos** ou **Mais cliques**.

Os filtros ficam no endereço da página, por isso pode guardar ou partilhar uma vista filtrada.

A indicação **Erro no destino** numa linha significa que as verificações semanais desse destino devolveram erros. As verificações são apenas informativas; o link continua a redirecionar.

## Editar um link

1. Selecione o link para abrir o resumo.
2. Abra o separador **Editar**.
3. Altere o destino, a chave, o título, a descrição, as etiquetas, a palavra-passe ou as opções avançadas.
4. Selecione **Guardar alterações**.

As alterações aplicam-se a partir do clique seguinte. Se mudar a chave, a antiga continua a redirecionar para o mesmo link.

## Selecionar links em várias páginas

Marque a caixa de cada linha pretendida ou use **Selecionar esta página**. A seleção mantém-se quando muda de página ou de filtros, por isso pode construí-la a partir de várias pesquisas. A barra de seleção mostra quantos links estão selecionados e o máximo permitido por ação. Use **Limpar seleção** para recomeçar.

## Ações em massa

Com links selecionados, escolha uma ação. Cada uma abre uma página própria onde pode rever os links selecionados, filtrar essa revisão e confirmar. Os filtros da página de revisão só mudam o que vê; a ação aplica-se à seleção completa.

- **Editar etiquetas**: escolha a **Operação** (**Adicionar etiquetas**, **Remover etiquetas** ou **Substituir todas as etiquetas**), escolha as etiquetas e selecione **Aplicar alterações**. **Remover todas as etiquetas** retira todas as etiquetas da seleção.
- **Mover para campanha**: escolha a **Campanha de destino** e selecione **Mover ligações selecionadas**. Cada link mantém a origem, o meio, o termo e o conteúdo UTM. Requer um plano com campanhas.
- **Arquivar**: retira os links da lista ativa. Ver abaixo.
- **Eliminar selecionados**: envia os links para o lixo.

Os membros só podem eliminar os links que criaram. Se a seleção incluir links de outras pessoas, a página de eliminação indica quantos podem ser eliminados.

## Arquivo

Arquivar arruma a lista ativa sem estragar nada. Os links arquivados continuam a redirecionar e mantêm-se nos códigos QR, nas campanhas e nas páginas bio. As estatísticas também se mantêm.

Para os ver, selecione **Arquivados** por cima da lista. Selecione links nessa vista e escolha **Desarquivar** para os devolver à lista ativa.

## Eliminar e restaurar

Eliminar um link desativa o URL curto de imediato, incluindo em códigos QR impressos. Durante 30 dias:

- o link fica no **Lixo** e pode ser restaurado com a chave, as definições, as campanhas e as referências em páginas bio;
- a chave fica reservada e mais ninguém a pode usar.

Ao fim de 30 dias o link é removido definitivamente. As estatísticas agregadas mantêm-se.

Para restaurar links:

1. Selecione **Lixo** por cima da lista. A coluna **Restaurar até** mostra o prazo de cada link.
2. Selecione os links pretendidos, em várias páginas se necessário.
3. Selecione **Restaurar selecionados**.

No lixo e nos arquivados também pode usar **Agrupar esta página** com **Por primeira etiqueta** para agrupar a página atual por etiqueta.

## Exportar

Selecione **Exportar** na página de links para preparar um ficheiro com todos os links que correspondem aos filtros atuais, em todas as páginas. Uma exportação em CSV pode voltar a ser importada no Shortn.

## Próximos passos

- [Parâmetros UTM](/docs/pt/guides/links/utm-parameters)
- [Organizar campanhas](/docs/pt/guides/campaigns/organizing-campaigns)
- [Compreender as estatísticas](/docs/pt/guides/analytics/understanding-analytics)


---

# Parâmetros UTM

Os parâmetros UTM são etiquetas no URL, como `utm_source=newsletter`, que as ferramentas de estatísticas leem para saber de onde veio uma visita. No Shortn surgem em três sítios: nas variantes UTM de um link, no construtor de UTM de uma campanha e nas definições de redirecionamento, que decidem que parâmetros chegam ao destino.

## Os cinco parâmetros

- `utm_source`: de onde vem o tráfego, por exemplo `newsletter` ou `facebook`.
- `utm_medium`: o tipo de canal, por exemplo `email` ou `social`.
- `utm_campaign`: a campanha a que a visita pertence.
- `utm_term`: a palavra-chave de pesquisa paga. Opcional.
- `utm_content`: distingue links semelhantes, como `cabecalho` e `rodape`. Opcional.

## Acrescentar variantes UTM a um link

Uma variante é uma versão etiquetada de um link curto para um canal. As visitas através dela são contadas por origem, meio e campanha.

1. Abra o link e selecione o separador **Variantes UTM**.
2. Selecione **Adicionar variante**.
3. Preencha **Origem**, **Meio** e **Campanha** e, se quiser, **Termo** e **Conteúdo**. Ao escolher uma campanha guardada, aparecem os valores sugeridos dessa campanha.
4. Selecione **Guardar variante**.

A lista de variantes mostra cada **URL etiquetado**: o link curto com os parâmetros acrescentados, por exemplo `shortn.at/primavera?utm_source=newsletter&utm_medium=email&utm_campaign=saldos-primavera`. Selecione **Copiar** para o copiar.

O valor de **Campanha** liga a variante à campanha com o mesmo nome, o que junta o link a essa campanha. As variantes UTM e as campanhas dependem do plano.

Para alterar uma variante, selecione **Editar**. Para a apagar, selecione **Remover** e confirme com **Remover variante**. Os URL já partilhados continuam a redirecionar e as estatísticas mantêm-se. Se era a última variante do link numa campanha, o link sai dessa campanha.

## Etiquetar muitos links de uma vez

Para etiquetar vários links para o mesmo canal, use o separador **Construtor de UTM** de uma campanha. Define sempre `utm_campaign` com o título da campanha, sugere os valores guardados e grava a mesma variante em todos os links selecionados. Consulte [Organizar campanhas](/docs/pt/guides/campaigns/organizing-campaigns).

## Controlar o que chega ao destino

O URL etiquetado leva os parâmetros até ao Shortn, que os regista com cada visita. Se são ou não passados ao destino depende das definições de redirecionamento do link.

1. Abra o separador **Editar** do link e expanda **Opções avançadas**.
2. Abra **Redirecionamento e parâmetros**.
3. Escolha as definições:
   - **Encaminhar parâmetros recebidos** passa ao destino os parâmetros do visitante, incluindo as etiquetas UTM.
   - Os campos `utm_source`, `utm_medium`, `utm_campaign`, `utm_term` e `utm_content` fixam valores que são sempre acrescentados ao destino.
4. Selecione **Guardar alterações**.

Quando o mesmo parâmetro aparece em mais de um sítio, aplicam-se estas regras:

- um valor UTM fixado nas definições de redirecionamento prevalece sempre;
- caso contrário, um parâmetro que já exista no URL de destino prevalece sobre o recebido;
- os parâmetros recebidos só são encaminhados com o encaminhamento ativo.

## Ler os resultados UTM

Na página **Estatísticas**, as análises **Origens UTM**, **Meios UTM** e **Campanhas UTM** agrupam as visitas por etiqueta, e é possível filtrar por qualquer valor. Estas análises dependem do plano. Consulte [Compreender as estatísticas](/docs/pt/guides/analytics/understanding-analytics).

## Boas práticas

- Escreva os valores em minúsculas e mantenha uma só grafia por canal, para que os resultados do mesmo canal não fiquem divididos por vários valores.
- Use `utm_content` para comparar posições do mesmo link, como o cabeçalho e o rodapé.
- Não acrescente etiquetas UTM a links que apontam para o seu próprio site; substituem a origem original da visita.

## Próximos passos

- [Organizar campanhas](/docs/pt/guides/campaigns/organizing-campaigns)
- [Gerir links](/docs/pt/guides/links/managing-links)


---

# Criar códigos QR

Cada código QR do Shortn abre um link curto. O padrão impresso nunca muda, por isso é possível alterar o destino depois de imprimir e continuar a contar cada leitura. Este guia explica como criar um código e obter o primeiro ficheiro.

## Antes de começar

Cada código QR criado conta para o limite mensal de códigos QR do plano, visível em **Utilização** na barra lateral. Um código pode apontar para:

- **um URL**: o Shortn cria um link curto para o código;
- **um dos seus links**: o código fica associado a esse link e abre-o.

## Criar um código QR

1. Abra **Códigos QR** na barra lateral e selecione **Novo código QR**.
2. Em **URL de destino**, indique para onde a leitura deve levar. Para usar um link existente, selecione **Escolher um link** em **Ou use um dos seus links:** e escolha-o na lista. **Usar um URL** volta à opção anterior.
3. Acrescente, se quiser, um **Título** para encontrar o código mais tarde, e **Etiquetas**.
4. Selecione **Criar código QR**.

Abre-se o resumo do código. A pré-visualização do formulário usa um link curto de exemplo; o código recebe o seu próprio link curto quando é criado.

Também é possível partir de um link: abra o link, selecione o separador **Código QR** e depois **Criar um código QR**. O código fica associado a esse link e passa a aparecer nesse separador. Se o link já tiver um código, o novo passa a ser o código do link, e o antigo continua a funcionar.

## Desenhar o código

Selecione **Editar design** no resumo ou abra o separador **Design**. Pode alterar as cores, o padrão, os cantos, a correção de erros, o logótipo, a moldura e a legenda. A verificação de leitura na mesma página avisa quando o contraste é baixo ou o logótipo é demasiado grande. No fim, selecione **Guardar design**.

As alterações de design nunca mudam o link dentro do código. O guia [Gerir códigos QR](/docs/pt/guides/qr-codes/managing-qr-codes) explica cada opção.

## Transferir o código

No resumo, em **Transferir**, selecione **Exportar para impressão**:

1. Escolha o **Formato**: SVG, PNG ou PDF. O SVG é vetorial e adapta-se a qualquer tamanho de impressão.
2. Escolha o **Tamanho** em píxeis.
3. Selecione **Preparar transferência** e, quando estiver pronto, **Transferir ficheiro**.

A página de exportação indica o tamanho mínimo de impressão recomendado para o código. Para ecrãs e e-mail, abra **JPEG para ecrãs** no resumo.

## Testar antes de imprimir

- Leia o ficheiro transferido com dois ou três telemóveis diferentes.
- Mantenha espaço livre à volta do código; as molduras e as legendas ficam fora desse espaço.
- Mantenha um contraste forte entre o código e o fundo, com código escuro sobre fundo claro.
- Não imprima o código abaixo do tamanho mínimo recomendado.
- Junte ao código uma frase curta que diga o que se obtém ao lê-lo.

## Mudar o destino mais tarde

Abra o separador **Detalhes**, altere o **URL de destino** e selecione **Guardar detalhes**. As cópias impressas seguem o novo destino a partir da leitura seguinte. Se o código estiver associado a um link, selecione **Atualizar também o link** para copiar o novo destino ou título também para esse link.

## Próximos passos

- [Gerir códigos QR](/docs/pt/guides/qr-codes/managing-qr-codes)
- [Compreender as estatísticas](/docs/pt/guides/analytics/understanding-analytics)


---

# Gerir códigos QR

Este guia descreve as opções de design de um código QR, a exportação de ficheiros para impressão, a exportação de vários códigos num ficheiro ZIP e a eliminação de códigos.

## Encontrar os códigos

Abra **Códigos QR** na barra lateral. Use **Pesquisar** para procurar por título, destino ou código, filtre por **Etiqueta** e por **Criado** e alterne a **Disposição** entre **Grelha** e **Lista**. Selecione um código para abrir o resumo, onde estão os separadores **Resumo**, **Design**, **Leituras** e **Detalhes**.

## Opções de design

Abra o separador **Design**. A pré-visualização acompanha as alterações e indica quando inclui alterações por guardar. Selecione **Guardar design** para as aplicar.

- **Cores**: escolha uma predefinição ou defina as cores do **Código** e do **Fundo**. Em **Cantos**, mantenha a cor do código ou escolha outra cor para os cantos.
- **Padrão**: escolha a forma dos **Pontos**, dos **Quadrados dos cantos** e dos **Pontos dos cantos**. As formas só mudam o aspeto; o link mantém-se.
- **Correção de erros**: escolha um **Nível**. Níveis mais altos permitem que o código resista a riscos e a um logótipo, à custa de um padrão mais denso.
- **Logótipo**: carregue uma imagem para o centro do código, ajuste o **Tamanho do logótipo** e, se quiser, ative **Esconder o padrão atrás do logótipo**. Mantenha o logótipo em 50% da correção de erros ou menos. Os logótipos dependem do plano.
- **Forma e legenda**: escolha uma **Forma** quadrada ou circular, uma **Moldura** (nenhuma, contorno ou sólida) e uma **Legenda** opcional até 64 caracteres. Molduras e legendas ficam fora do espaço livre à volta do código e aparecem nas transferências.

O painel **Verificação de leitura** avalia o contraste entre o código e o fundo e avisa sobre cores invertidas, fundo transparente e tamanho do logótipo. Use os avisos como motivo para testar com telemóveis reais antes de imprimir.

## Exportar para impressão

1. No resumo do código, em **Transferir**, selecione **Exportar para impressão**.
2. Escolha o **Formato** (SVG, PNG ou PDF) e o **Tamanho**.
3. Leia as indicações de impressão na página: quatro módulos de margem à volta do código, correção Q (H com logótipo), contraste mínimo de 3:1 e o tamanho mínimo recomendado a 300 dpi.
4. Selecione **Preparar transferência** e depois **Transferir ficheiro**.

As transferências expiram 24 horas após iniciar a exportação. Para uso em ecrã, **JPEG para ecrãs** no resumo gera um JPEG de imediato.

## Exportar vários códigos num ZIP

1. Na lista de códigos QR, marque os códigos pretendidos ou use **Selecionar esta página**. A seleção mantém-se ao mudar de página e de filtros.
2. Selecione **Exportar seleção**.
3. Reveja a seleção. Os filtros desta página só mudam a lista apresentada; o ZIP inclui todos os códigos selecionados.
4. Escolha o **Formato** e o **Tamanho** e selecione **Criar ZIP**.
5. Quando a exportação estiver pronta, selecione **Transferir ZIP**.

A página de exportação indica quantos códigos cabem num ZIP. Pode sair da página durante a exportação; o processo continua em segundo plano.

## Editar detalhes e destino

O separador **Detalhes** reúne o **URL de destino**, o **Título** e as **Etiquetas**. Mudar o destino não altera o padrão impresso; as cópias impressas seguem o novo destino a partir da leitura seguinte. Selecione **Guardar detalhes** para aplicar.

## Leituras

O resumo mostra os últimos 30 dias: leituras, visitantes, leituras acumuladas e a data da última leitura. O separador **Leituras** abre as estatísticas completas do código. As leituras também aparecem na página **Estatísticas** do espaço de trabalho, separadas dos cliques nos links. Consulte [Compreender as estatísticas](/docs/pt/guides/analytics/understanding-analytics).

## Eliminar códigos QR

Selecione **Eliminar código QR** no resumo, ou selecione códigos na lista e escolha **Eliminar selecionados**. A página de confirmação explica o que acontece:

- o código deixa de funcionar de imediato, porque o seu link curto é eliminado com ele;
- os links a que o código estava associado continuam a funcionar e apenas perdem o código QR;
- o histórico de leituras é arquivado e deixa de ser mostrado.

A eliminação de um código QR não pode ser anulada e as cópias impressas não podem ser recuperadas. Os membros só podem eliminar os códigos que criaram.

## Próximos passos

- [Criar códigos QR](/docs/pt/guides/qr-codes/creating-qr-codes)
- [Criar páginas bio](/docs/pt/guides/bio-pages/creating-bio-pages)


---

# Entender las estadísticas

Shortn cuenta cada visita a tus enlaces cortos y códigos QR. Esta guía explica qué se cuenta, cómo acotar los datos con intervalos, desgloses y filtros, cómo exportarlos y cómo se protegen los datos de los visitantes.

## Clics y escaneos

Shortn registra dos tipos de visita:

- un **clic** es una visita a través de un enlace corto, ya sea escrito, pulsado o abierto desde otro sitio;
- un **escaneo** es una visita a través de uno de tus códigos QR.

El filtro **Tipo** cambia entre **Clics y escaneos**, **Clics** y **Escaneos QR**. El resumen superior muestra también los **Visitantes únicos** y el **País principal**.

## Dónde están las estadísticas

- **Estadísticas**, en la barra lateral, cubre todo el espacio de trabajo.
- La pestaña **Estadísticas** de un enlace cubre ese enlace.
- La pestaña **Escaneos** de un código QR cubre ese código.
- La pestaña **Analíticas** de una campaña cubre los enlaces de esa campaña.
- La pestaña **Estadísticas** de una página bio muestra las vistas, los clics y la tasa de clics de la página.

Todas, salvo la de las páginas bio, comparten los mismos controles.

## Elige un intervalo de fechas

1. Abre **Intervalo de fechas** y elige **Últimos 7 días**, **Últimos 30 días**, **Últimos 90 días** o **Últimos 12 meses**, o define un **Intervalo personalizado** con las fechas **Desde** y **Hasta**.
2. Elige el **Intervalo** para agrupar el gráfico por hora, día, semana o mes.

Hasta dónde puedes retroceder depende del historial de estadísticas de tu plan. Si un intervalo empieza antes, la página te indica la fecha a partir de la cual los datos quedan fuera del período de tu plan y muestra lo que esté disponible. Consulta los [Precios](/es/pricing) para ver el historial de cada plan.

## Desgloses

Debajo del gráfico, los desgloses agrupan las visitas por:

- **Enlaces**
- **Países** y **Ciudades**
- **Orígenes**: el sitio del que llegó la visita. Directo significa que no hubo sitio de origen, como una dirección escrita, una aplicación o un escaneo QR.
- **Dispositivos**, **Navegadores** y **Sistemas operativos**
- **Fuentes UTM**, **Medios UTM** y **Campañas UTM**
- **Reglas de destino**: la regla de redirección que eligió el destino, en enlaces con reglas por país, por dispositivo o de rotación.

Cada desglose muestra las filas principales; selecciona **Ver todo** para la lista completa. Los desgloses distintos del de enlaces, y el filtrado por ellos, dependen de tu plan.

## Filtra la vista

Selecciona un valor de un desglose para filtrar toda la página por él, o abre los filtros para combinar varios, por ejemplo un enlace, un país y los dispositivos móviles. Selecciona **Quitar todo** para eliminarlos.

Todos los filtros forman parte de la dirección de la página, así que puedes guardar una vista en marcadores o compartirla con alguien que tenga acceso al espacio de trabajo.

## Exporta las estadísticas

1. Define el intervalo y los filtros que quieras.
2. Selecciona **Exportar estadísticas**.
3. En **Datos**, elige **Agregados** para totales por periodo y por grupo, o **Eventos individuales** para una fila por visita.
4. Elige el formato (CSV, o NDJSON para eventos individuales) y selecciona **Preparar exportación**.

Las exportaciones se ejecutan en segundo plano, así que puedes salir de la página. Los archivos terminados están en **Historial de exportaciones**. Archivos e historial caducan 24 horas después de prepararse. La exportación conserva el período que revisaste, aunque tu plan cambie antes de que se ejecute. La exportación CSV depende de tu plan, y la de campañas tiene su propio requisito de plan.

## Privacidad

- Las direcciones IP de los visitantes se guardan como hash.
- Las direcciones IP originales se borran a los 90 días.
- Las exportaciones de eventos individuales excluyen direcciones IP, hashes de visitantes y agentes de usuario.
- El contenido integrado en las páginas bio solo se carga cuando el visitante lo pide.

## Cómo leer los datos

- Usa los desgloses UTM para ver qué canal ha funcionado, y las reglas de destino para comparar los destinos de un mismo enlace.

## Siguientes pasos

- [Parámetros UTM](/docs/es/guides/links/utm-parameters)
- [Organizar campañas](/docs/es/guides/campaigns/organizing-campaigns)
- [Cambiar de plan](/docs/es/guides/billing/upgrading-plans)


---

# Cambiar de plan

Cada espacio de trabajo tiene su propio plan. Esta guía explica dónde ver tu plan y tu uso, cómo mejorar, bajar o cancelar el plan, cómo funciona el pago y qué pasa con tus enlaces cuando cambia el plan. Solo el propietario del espacio de trabajo puede cambiar el plan o ver las facturas.

## Consulta tu plan y tu uso

Abre **Configuración** y selecciona **Facturación**. La página tiene tres partes:

- **Plan**: tu plan actual y cuándo se renueva o termina. En el plan Free, muestra cuándo se restablece el uso.
- **Uso**: los enlaces y códigos QR creados en este periodo y el total de páginas bio, frente a los límites de tu plan.
- **Facturas**: tu historial de pagos, con la opción **Descargar** en cada factura.

Los límites de enlaces y códigos QR se restablecen cada periodo de facturación. Cuando llegas a un límite, la página te avisa, y puedes mejorar el plan para seguir creando.

## Compara planes

Selecciona **Comparar planes** para ver todos los planes uno al lado del otro: límites, historial de estadísticas, miembros, espacios de trabajo y funciones como claves personalizadas, campañas, desgloses de estadísticas, exportación CSV y logotipos en códigos QR. La misma información está en la página de [Precios](/es/pricing).

## Precios y moneda

Los planes son mensuales y se facturan en dólares estadounidenses. Shortn muestra una estimación en euros junto a cada precio, calculada con los tipos de referencia del Banco Central Europeo. El pago cobra el importe en dólares; la cifra en euros es solo una estimación.

## Mejora tu plan

1. Abre **Comparar planes**.
2. Selecciona **Mejorar a …** en el plan que quieras.
3. Desde el plan Free, pasas al pago. Introduce ahí tus datos de pago y confirma.
4. De vuelta en Shortn, la página de facturación muestra tu nuevo plan en pocos segundos, en cuanto el proveedor de pagos lo confirma. Selecciona **Actualizar** si todavía no ha cambiado.

Si ya pagas un plan, la mejora abre una página de confirmación en lugar del pago. Selecciona **Cambiar a …** para confirmar. El nuevo plan empieza en cuanto lo confirma el proveedor de facturación, y se te cobra la diferencia proporcional del periodo actual.

## Baja de plan

1. Abre **Comparar planes** y selecciona **Cambiar a …** en un plan inferior.
2. Lee la página de confirmación y selecciona **Cambiar a …**.

Conservas tu plan actual hasta el final del periodo. A partir de entonces se aplican los límites y las funciones del plan inferior. Para quedarte en tu plan actual, selecciona **Mantener …**.

## Cancela

1. En la página de facturación, selecciona **Cancelar plan**.
2. Confirma en la página siguiente.

Conservas el plan hasta el final del periodo actual; después, el espacio de trabajo pasa al plan Free. Hasta esa fecha puedes seleccionar **Mantener …** para que el plan se renueve con normalidad.

## Tus enlaces siguen funcionando

Los límites del plan solo controlan lo que puedes crear. Si bajas de plan o cancelas, los enlaces, códigos QR y páginas bio que ya tienes siguen redirigiendo. Las funciones que tu nuevo plan no incluye dejan de estar disponibles para cambios nuevos. Por ejemplo, si tu plan deja de incluir logotipos en códigos QR, puedes conservar un logotipo existente o quitarlo, pero no añadir uno nuevo.

## Métodos de pago y pagos fallidos

Selecciona **Abrir portal de facturación** para actualizar tus datos de facturación o tu método de pago. Si un pago falla, la página de facturación muestra un aviso con **Actualizar método de pago**, para que puedas conservar tu plan.

## Complemento de dominio personalizado

Los dominios personalizados se venden como complemento, aparte del plan. Cada complemento incluye un dominio. Cómpralo en **Configuración**, en **Dominios**, con **Comprar complemento de dominio**, y conecta ahí el dominio. Consulta [Personalizar páginas bio](/docs/es/guides/bio-pages/customizing-bio-pages).

## Enterprise

Los planes Enterprise tienen condiciones acordadas con Shortn. Para cambiar uno, ponte en contacto con Shortn.

## Siguientes pasos

- [Precios](/es/pricing)
- [Primeros pasos](/docs/es/guides/getting-started)


---

# Crear páginas bio

Una página bio reúne tus enlaces en una sola página pública, de las que se ponen en el perfil de una red social. Los visitantes ven tu perfil, tus enlaces y las demás secciones que añadas, y cada clic se cuenta. Esta guía explica cómo crear una página y publicar su primera versión.

## Antes de empezar

Cuántas páginas bio puede tener un espacio de trabajo depende de tu plan; la lista de páginas bio muestra cuántas has usado. Los enlaces de una página bio son enlaces normales de Shortn, así que aparecen en **Enlaces** y en las estadísticas como cualquier otro.

## Crea una página

1. Abre **Páginas bio** en la barra lateral y selecciona **Nueva página bio**.
2. En **Dirección**, escribe la dirección que visitará la gente: de 3 a 63 letras minúsculas, números y guiones. Shortn comprueba si está libre mientras escribes.
3. Si quieres, escribe un **Título**, que aparece en la parte superior de la página. Si lo dejas vacío, se usa la dirección.
4. En **Primer enlace**, puedes seleccionar **Elegir un enlace** para poner ya uno de tus enlaces en la página.
5. Selecciona **Crear página**.

Se abre el resumen de la página, con **Copiar URL** y **Abrir página** para su dirección pública.

## Construye la página

Desde el resumen, selecciona **Editar página bio**. El editor muestra una vista previa en directo; cambia entre **Vista móvil** y **Vista de escritorio** para revisar las dos.

1. Selecciona **Añadir sección** y elige qué añadir. Los tipos de sección son **Perfil**, **Enlace**, **Redes sociales**, **Título de sección**, **Texto**, **Imagen**, **Contenido integrado**, **Código QR** y **Enlaces**.
2. Selecciona una sección en la vista previa para editarla. Arrastra su asa para cambiar el orden.
3. Para añadir varios enlaces a la vez, añade una sección **Enlaces** y selecciona **Gestionar enlaces**. Puedes filtrar por etiquetas, buscar y seleccionar enlaces de varias páginas antes de añadirlos.
4. Para añadir una URL que todavía no es un enlace corto, escríbela en una sección de enlace y selecciona **Acortar y añadir enlace**. Así se crea un enlace nuevo en tu espacio de trabajo.

Los cambios se guardan como borrador mientras trabajas. Los visitantes no los ven hasta que publicas.

## Publica

Selecciona **Publicar** cuando el borrador esté listo. La página queda disponible en su dirección al instante. Cada publicación crea una versión a la que puedes volver más adelante; consulta [Personalizar páginas bio](/docs/es/guides/bio-pages/customizing-bio-pages).

## Comparte la página

Selecciona **Copiar URL** en el resumen y pega la dirección en tus perfiles sociales, en la firma de correo o en material impreso. Para cambiar el nombre de la página en su dirección, abre **Ajustes de la página** en el editor y usa **Cambiar identificador**. Las direcciones anteriores siguen funcionando después del cambio.

## Estadísticas de la página bio

La pestaña **Estadísticas** de una página bio muestra los últimos 30 días: **Vistas**, **Clics**, **Tasa de clics**, **Clics por bloque** y los principales orígenes. Los enlaces de la página también se cuentan en la página **Estadísticas** del espacio de trabajo.

## Elimina una página

En el resumen, selecciona **Eliminar página bio** y confirma. La dirección pública deja de funcionar al instante y queda libre para cualquiera. Los enlaces que había en la página siguen funcionando.

## Consejos

- Pon primero el enlace que más importa, o destácalo.
- Usa textos cortos en los botones para que quepan en la pantalla de un móvil.
- Revisa la vista móvil antes de publicar, porque las páginas bio se suelen abrir desde una aplicación social en el móvil.

## Siguientes pasos

- [Personalizar páginas bio](/docs/es/guides/bio-pages/customizing-bio-pages)
- [Gestionar enlaces](/docs/es/guides/links/managing-links)


---

# Personalizar páginas bio

El editor de páginas bio trabaja sobre un borrador: cambias las secciones, el tema y los ajustes, lo compruebas en la vista previa en directo y publicas cuando quieras. Esta guía describe cada parte del editor, la programación, las versiones y cómo conectar tu propio dominio.

Abre una página bio y selecciona **Editar página bio**. La barra de herramientas tiene **Añadir sección**, **Secciones**, **Tema**, **Ajustes de la página** e **Historial**.

## Secciones

Selecciona **Añadir sección** para añadir una de estas:

- **Perfil**: avatar, título, descripción y alineación. Puedes elegir la forma del avatar.
- **Enlace**: un botón para un enlace. Activa **Destacar este enlace** para que resalte y añade una miniatura.
- **Enlaces**: un grupo de enlaces. Selecciona **Gestionar enlaces** para filtrar por etiquetas, buscar, seleccionar enlaces de varias páginas y añadirlos de una vez. En **Presentación de enlaces**, elige un diseño como lista vertical, tarjetas paginadas o cuadrículas de imágenes.
- **Redes sociales**: iconos que enlazan a tus perfiles en otras plataformas.
- **Título de sección** y **Texto**: títulos y párrafos cortos entre tus enlaces.
- **Imagen**: una imagen con descripción y un enlace opcional.
- **Contenido integrado**: contenido de YouTube, Spotify, SoundCloud, Vimeo o Apple Music. Los visitantes ven un botón para cargarlo, con un aviso de que al hacerlo se comparte su dirección IP con el proveedor.
- **Código QR**: un código QR para uno de tus enlaces.

Selecciona una sección en la vista previa para editarla y arrastra su asa para cambiar el orden. **Secciones** muestra todas las secciones en orden.

## Muestra secciones con programación

Cada sección tiene el interruptor **Visible** y las fechas opcionales **Mostrar desde (UTC)** y **Mostrar hasta (UTC)**. Sirven para mostrar un enlace solo durante un evento. La programación está disponible en los planes de pago.

## Tema

Abre **Tema** para cambiar el aspecto de la página:

1. Elige un **Tema predefinido** como punto de partida, o **Personalizado**.
2. Ajusta los colores: **Fondo**, **Superficie**, **Texto** y **Acento**.
3. Elige la **Fuente**, el **Estilo de botones**, el **Radio de esquinas** y el **Espaciado**.
4. Si quieres, añade una **Imagen de portada (opcional)** detrás del perfil.

Los temas tienen límites para que las páginas sigan siendo legibles. Si el contraste del texto es insuficiente, el editor te avisa; selecciona **Usar color accesible** para corregirlo.

## Ajustes de la página

En **Ajustes de la página** están las opciones que afectan a toda la página:

- **Identificador**: el nombre de la página en su dirección. Selecciona **Cambiar identificador** para cambiarlo; las direcciones anteriores siguen funcionando.
- **Idioma de la página**: el idioma en que está escrita la página.
- **Ocultar pie de Shortn**: quita el pie "Creado con Shortn". Disponible en los planes de pago.
- **Apariencia en búsquedas**: activa **Ocultar de los buscadores** para que la página no salga en los resultados de búsqueda.

## Borradores y publicación

Tus cambios se guardan en el borrador poco después de que dejes de escribir, y puedes guardar cuando quieras con **Guardar borrador**. La vista previa muestra el borrador; los visitantes siguen viendo la página publicada hasta que seleccionas **Publicar**.

## Versiones

Cada publicación crea una versión. Abre **Historial** para ver las **Versiones publicadas (últimas 20)**. Para volver atrás, selecciona **Restaurar versión … como borrador** en la que quieras. La versión se copia en tu borrador; selecciona **Publicar** para que vuelva a estar visible.

## Usa tu propio dominio

Puedes servir una página bio desde tu propio dominio, como `enlaces.ejemplo.es`, con el complemento de dominio personalizado. Cada complemento incluye un dominio.

1. Abre **Configuración** y selecciona **Dominios**.
2. Si no te quedan dominios disponibles, selecciona **Comprar complemento de dominio**.
3. En **Añadir dominio**, escribe el **Nombre del dominio**, elige una opción en **Usar para**, elige la **Página bio** y selecciona **Añadir dominio**.
4. Sigue las **Instrucciones DNS**: crea en tu proveedor de DNS el registro CNAME que se indica. Para un dominio raíz, usa CNAME flattening.
5. Espera a que el estado cambie a **Activo**. Si la verificación falla, revisa el registro y selecciona **Reintentar verificación**.

Solo quien puede gestionar la facturación puede añadir dominios.

## Siguientes pasos

- [Crear páginas bio](/docs/es/guides/bio-pages/creating-bio-pages)
- [Cambiar de plan](/docs/es/guides/billing/upgrading-plans)


---

# Organizar campañas

Una campaña agrupa las variantes UTM de tus enlaces para que puedas leer sus clics en conjunto. Su título es el valor de `utm_campaign`, guarda valores sugeridos para los demás parámetros UTM y su generador de UTM etiqueta muchos enlaces de una vez. Las campañas dependen de tu plan; si el tuyo no las incluye, la página de campañas te dice qué plan las incluye.

## Crea una campaña

1. Abre **Campañas** en la barra lateral y selecciona **Nueva campaña**.
2. Escribe un **Título**. Se usa como `utm_campaign` en las URL que etiquetes, así que escríbelo tal como quieres verlo en tus estadísticas, por ejemplo `rebajas-primavera`.
3. Si quieres, añade una **Descripción**.
4. Selecciona **Crear campaña**.

La campaña se abre con las pestañas **Resumen**, **Analíticas**, **Generador de UTM** y **Ajustes**.

## Define los valores UTM sugeridos

Abre **Ajustes** y rellena los **Valores UTM predeterminados**: listas de fuentes, medios, términos y contenidos sugeridos. Escribe un valor y pulsa Intro para añadirlo. Los valores se guardan en minúsculas. El generador de UTM y el formulario de variantes del enlace te los sugieren, lo que mantiene la misma grafía en todo el equipo.

Si cambias el título de la campaña, las variantes guardadas pasan al título nuevo. Las URL que ya hayas compartido mantienen el antiguo, y sus clics dejan de contar en la campaña.

## Añade enlaces con el generador de UTM

1. Abre **Generador de UTM**, o selecciona **Crear una URL etiquetada** en el resumen.
2. En **Enlaces**, selecciona **Explorar enlaces**. Filtra por etiquetas, busca o agrupa los resultados por etiqueta. Marca enlaces en todas las páginas que necesites, o usa **Seleccionar todos los resultados**, y después selecciona **Usar … enlaces seleccionados**.
3. Rellena **Fuente**, **Medio** y, si quieres, **Término** y **Contenido**. `utm_campaign` es siempre el título de la campaña.
4. Revisa la vista previa de la **URL etiquetada** y selecciona **Copiar URL** si la necesitas ya.
5. Selecciona **Guardar como variante**. Con varios enlaces seleccionados, el botón guarda la misma variante en todos.

Un enlace entra en la campaña cuando una de sus variantes UTM usa esa campaña. Hasta que guardes la variante, sus clics no cuentan en la campaña.

También puedes añadir variantes enlace a enlace desde la pestaña **Variantes UTM** de cada enlace; consulta [Parámetros UTM](/docs/es/guides/links/utm-parameters).

## Mueve enlaces existentes a una campaña

En **Enlaces**, selecciona los enlaces que quieras, de varias páginas y filtros, y elige **Mover a campaña**. Elige la **Campaña de destino** y selecciona **Mover enlaces seleccionados**. Cada variante UTM conserva su fuente, medio, término y contenido, y los valores predeterminados de la campaña no los sobrescriben.

## Lee los resultados de la campaña

El **Resumen** muestra los clics, escaneos y visitantes de los últimos 30 días, y **Enlaces y variantes** lista cada enlace con sus URL etiquetadas y sus clics. Selecciona **Copiar utm_campaign** para copiar el valor exacto.

La pestaña **Analíticas** abre las estadísticas completas filtradas por la campaña, con los mismos intervalos de fechas, desgloses y filtros que la página **Estadísticas** del espacio de trabajo. Exportar en CSV las estadísticas de una campaña depende de tu plan. Consulta [Entender las estadísticas](/docs/es/guides/analytics/understanding-analytics).

## Encuentra campañas

La lista de campañas tiene el campo **Buscar**, para títulos y descripciones, y **Ordenar por**, con las opciones más recientes, más antiguas, más enlaces o título.

## Elimina una campaña

En el resumen, selecciona **Eliminar campaña** y confirma. Sus variantes UTM se quitan de los enlaces y sus valores predeterminados se borran. Los enlaces siguen funcionando. Las URL ya compartidas con su `utm_campaign` conservan sus clics, pero ninguna página de campaña los muestra.

## Consejos

- Acordad en el equipo un esquema de nombres para los títulos, como `temporada-año-producto`, antes de empezar a etiquetar.
- Usa `utm_content` para comparar ubicaciones del mismo enlace dentro de un canal.

## Siguientes pasos

- [Parámetros UTM](/docs/es/guides/links/utm-parameters)
- [Entender las estadísticas](/docs/es/guides/analytics/understanding-analytics)


---

# Primeros pasos con Shortn

Shortn convierte URL largas en enlaces cortos que puedes compartir, imprimir como códigos QR o reunir en una página bio, y cuenta cada clic y cada escaneo. Esta guía explica cómo crear una cuenta, cómo moverte por el panel y cómo crear tu primer enlace corto.

## Crea tu cuenta

1. Entra en [app.shortn.at/register](https://app.shortn.at/register).
2. Escribe tu nombre completo, un nombre de usuario, tu correo y una contraseña, y selecciona **Crear cuenta**. Si la página ofrece **Continuar con Google** o **Continuar con GitHub**, también puedes usar una de esas opciones.
3. Abre el correo de verificación y sigue el enlace. Es válido durante una hora. Si ha caducado o no te ha llegado el correo, pide otro con **Reenviar correo**.
4. Inicia sesión en [app.shortn.at/login](https://app.shortn.at/login) con tu correo o tu nombre de usuario.

Tu cuenta empieza con un espacio de trabajo personal en el plan Free.

## Muévete por el panel

La barra lateral reúne las secciones principales:

- **Enlaces**: todos los enlaces cortos del espacio de trabajo, con búsqueda, filtros por etiqueta, archivo y papelera.
- **Estadísticas**: clics y escaneos de códigos QR de todo el espacio de trabajo.
- **Códigos QR**: los códigos que diseñas, descargas e imprimes.
- **Páginas bio**: páginas de enlaces con su propia dirección pública.
- **Campañas**: grupos de enlaces con etiquetas UTM que lees en conjunto.
- **Configuración**: datos del espacio de trabajo, miembros, facturación, dominios, claves de API y tu cuenta.

El panel **Uso** muestra cuánto has consumido del límite mensual de tu plan, por ejemplo en **Enlaces este mes**.

Fuera de los campos de texto tienes algunos atajos de teclado: Ctrl+K (Cmd+K en Mac) abre la **Paleta de comandos**, C empieza un enlace nuevo y ? muestra todos los atajos en **Atajos de teclado**.

El idioma y el tema del panel se cambian en el menú de la cuenta.

## Crea tu primer enlace corto

1. Abre **Enlaces** en la barra lateral.
2. Selecciona **Nuevo enlace** o pega una URL en cualquier parte de la página de enlaces, fuera de un campo de texto. En los dos casos se abre el formulario.
3. Escribe la **URL de destino**. Shortn consulta la página y puede sugerirte un título y una clave; para aceptarlos, selecciona **Usar título de página** o **Usar clave sugerida**.
4. Selecciona **Crear enlace**.

El enlace se crea y se copia al portapapeles, y se abre su página de resumen. Desde ahí puedes volver a copiarlo con **Copiar enlace corto**, abrir la pestaña **Estadísticas** o crear un código QR. La guía [Crear enlaces](/docs/es/guides/links/creating-links) describe todos los campos del formulario.

## Trabaja en equipo

Cada espacio de trabajo tiene sus propios enlaces, miembros y facturación. Para crear un espacio de trabajo de equipo, abre **Configuración**, entra en **General** y selecciona **Crear espacio de trabajo** en **Espacios de trabajo de equipo**. Serás su propietario y podrás invitar a otras personas desde **Miembros**. Cada miembro tiene uno de cuatro roles: propietario, administrador, miembro o lector. Los lectores ven los enlaces, pero no pueden crearlos ni cambiarlos.

Cuántos espacios de trabajo y miembros puedes tener depende de tu plan.

## Planes

El plan Free incluye un límite mensual de enlaces y códigos QR y una página bio. Los planes de pago suben los límites y añaden funciones como claves personalizadas, campañas, desgloses de estadísticas y exportación CSV. Los límites se restablecen cada periodo de facturación, y los enlaces existentes siguen redirigiendo con cualquier plan. Consulta los [Precios](/es/pricing) para ver los planes actuales, o [Cambiar de plan](/docs/es/guides/billing/upgrading-plans) para cambiar el tuyo.

## Siguientes pasos

- [Gestionar enlaces](/docs/es/guides/links/managing-links)
- [Crear códigos QR](/docs/es/guides/qr-codes/creating-qr-codes)
- [Crear páginas bio](/docs/es/guides/bio-pages/creating-bio-pages)


---

# Guías de Shortn

Estas guías recorren el panel de Shortn tarea a tarea: crear y organizar enlaces cortos, preparar códigos QR para imprimir, publicar páginas bio, agrupar enlaces en campañas, leer las estadísticas y gestionar tu plan. Cada guía usa los nombres que ves en la aplicación, así que puedes seguirla con el panel abierto.

## Para empezar

- [Primeros pasos](/docs/es/guides/getting-started)

## Enlaces

- [Crear enlaces](/docs/es/guides/links/creating-links)
- [Gestionar enlaces](/docs/es/guides/links/managing-links)
- [Parámetros UTM](/docs/es/guides/links/utm-parameters)

## Códigos QR

- [Crear códigos QR](/docs/es/guides/qr-codes/creating-qr-codes)
- [Gestionar códigos QR](/docs/es/guides/qr-codes/managing-qr-codes)

## Páginas bio

- [Crear páginas bio](/docs/es/guides/bio-pages/creating-bio-pages)
- [Personalizar páginas bio](/docs/es/guides/bio-pages/customizing-bio-pages)

## Campañas

- [Organizar campañas](/docs/es/guides/campaigns/organizing-campaigns)

## Estadísticas

- [Entender las estadísticas](/docs/es/guides/analytics/understanding-analytics)

## Facturación

- [Cambiar de plan](/docs/es/guides/billing/upgrading-plans)

## Más recursos

- [Documentación de la API](/docs): la API REST, los webhooks y el servidor MCP, en inglés.
- [Precios](/es/pricing): qué incluye cada plan y sus límites actuales.


---

# Crear enlaces

Un enlace corto lleva a los visitantes a una URL de destino y cuenta cada visita. Esta guía recorre el formulario de creación, del destino a las reglas avanzadas de redirección, y explica cómo importar enlaces en bloque.

## Crea un enlace

1. Abre **Enlaces** en la barra lateral y selecciona **Nuevo enlace**. También puedes pegar una URL en cualquier parte de la página de enlaces, fuera de un campo de texto, o pulsar C.
2. Escribe la **URL de destino**. Es el único campo obligatorio.
3. Rellena los campos opcionales que quieras; se describen más abajo.
4. Selecciona **Crear enlace**.

Shortn copia el enlace corto al portapapeles y abre su resumen. Cada enlace que creas cuenta para el límite mensual de enlaces de tu plan, que ves en **Uso** en la barra lateral.

## Sugerencias de título y clave

Al pegar un destino, Shortn consulta la página. Cuando llegan los datos, selecciona **Usar título de página** para rellenar el **Título**, o **Usar clave sugerida** para quedarte con una clave legible. Lo que hayas escrito tú nunca se sobrescribe. Si no se puede leer la página, escribe un título o déjalo vacío.

## Clave personalizada

La clave es la parte que va después del dominio, por ejemplo `ejemplo` en `shortn.at/ejemplo`. Deja **Clave personalizada** vacía para obtener una clave aleatoria, o escribe la tuya con letras, dígitos, `-` y `_`. Las claves personalizadas dependen de tu plan; si el tuyo no las incluye, el formulario te dice qué plan las incluye.

Si más adelante cambias la clave, la anterior sigue redirigiendo al mismo enlace.

## Título, descripción y etiquetas

El **Título** y la **Descripción** te ayudan a encontrar el enlace en las listas y en las estadísticas. Los visitantes no los ven.

En **Etiquetas**, busca una etiqueta existente o escribe una nueva para crearla. Cada enlace admite hasta 50 etiquetas. Las etiquetas alimentan los filtros de la página de enlaces, del generador de campañas y del selector de enlaces de las páginas bio.

## Contraseña

En **Contraseña**, define una contraseña que los visitantes deben introducir antes de llegar al destino y, si quieres, una **Pista** que aparece en la página de acceso. La protección con contraseña depende de tu plan.

## Opciones avanzadas

Despliega **Opciones avanzadas** para configurar reglas de redirección. Cada grupo muestra un resumen de una línea con su configuración actual.

- **Programación y vencimiento**: define **Disponible desde (UTC)** y **Vence el (UTC)**, un **Límite aproximado de visitas acumuladas** y una **URL alternativa cuando no esté disponible** para quien llegue fuera de ese periodo. El límite incluye los escaneos QR y es aproximado, porque varias visitas simultáneas pueden superarlo.
- **Destino por país**: envía a los visitantes de ciertos países a otras URL. Usa códigos de país de dos letras; se aplica la primera regla que coincida.
- **Destino por dispositivo**: redirige según el sistema operativo, el tipo de dispositivo o ambos.
- **Rotación ponderada**: reparte el tráfico entre hasta 10 destinos según pesos relativos. Quien vuelve mantiene el mismo destino.
- **Enlaces de aplicaciones**: abre una aplicación de iOS o Android, con alternativa al destino web o a la tienda de aplicaciones.
- **Redirección y parámetros**: reenvía los parámetros recibidos, usa una redirección permanente HTTP 301 y fija valores UTM. Consulta [Parámetros UTM](/docs/es/guides/links/utm-parameters).
- **Vista previa social**: define el título, la descripción y la imagen que muestran los robots de vista previa cuando se comparte el enlace.

## Importa enlaces desde un archivo CSV

Para crear muchos enlaces a la vez, selecciona **Importar** en la página de enlaces. Sube un archivo CSV en UTF-8 con fila de cabecera y hasta 50 000 filas, asigna sus columnas, revisa el resultado de la validación y crea los enlaces válidos. Los enlaces importados cuentan para el límite mensual de tu plan.

## Después de crear el enlace

La página del enlace tiene las pestañas **Resumen**, **Estadísticas**, **Código QR**, **Variantes UTM** y **Editar**. En la pestaña **Código QR**, selecciona **Crear un código QR** para generar un código imprimible que use este enlace.

Los cambios que guardes en **Editar** se aplican desde el siguiente clic.

## Siguientes pasos

- [Gestionar enlaces](/docs/es/guides/links/managing-links)
- [Parámetros UTM](/docs/es/guides/links/utm-parameters)
- [Crear códigos QR](/docs/es/guides/qr-codes/creating-qr-codes)


---

# Gestionar enlaces

La página de enlaces está pensada para bibliotecas grandes. Puedes buscar y filtrar en el servidor, seleccionar enlaces de varias páginas y aplicarles el mismo cambio, archivar lo que ya no necesitas ver y restaurar enlaces eliminados durante 30 días.

## Encuentra tus enlaces

Abre **Enlaces** en la barra lateral. La lista muestra 25 enlaces por página, con **Anterior** y **Siguiente** para moverte. Usa los filtros que hay encima:

- **Buscar**: busca en la clave, el destino o el título.
- **Etiqueta**: elige una o varias etiquetas. La lista busca entre todas las etiquetas del espacio de trabajo.
- **Estado**: muestra solo los enlaces **Protegido con contraseña**, los que **Muestra página de seguridad** o los **Desactivado**.
- **Creado**: limita la lista a un periodo reciente o a un intervalo de fechas personalizado.
- **Ordenar por**: **Más recientes**, **Más antiguos** o **Más clics**.

Los filtros forman parte de la dirección de la página, así que puedes guardar o compartir una vista filtrada.

El aviso **Error en el destino** en una fila indica que las comprobaciones semanales de ese destino han devuelto errores. Son solo informativas; el enlace sigue redirigiendo.

## Edita un enlace

1. Selecciona el enlace para abrir su resumen.
2. Abre la pestaña **Editar**.
3. Cambia el destino, la clave, el título, la descripción, las etiquetas, la contraseña o las opciones avanzadas.
4. Selecciona **Guardar cambios**.

Los cambios se aplican desde el siguiente clic. Si cambias la clave, la anterior sigue redirigiendo al mismo enlace.

## Selecciona enlaces de varias páginas

Marca la casilla de cada fila que quieras o usa **Seleccionar esta página**. La selección se mantiene al cambiar de página o de filtros, así que puedes construirla a partir de varias búsquedas. La barra de selección muestra cuántos enlaces llevas y el máximo por acción. Usa **Borrar selección** para empezar de nuevo.

## Acciones en bloque

Con enlaces seleccionados, elige una acción. Cada una abre su propia página, donde puedes revisar los enlaces seleccionados, filtrar esa revisión y confirmar. Los filtros de la revisión solo cambian lo que ves; la acción se aplica a toda la selección.

- **Editar etiquetas**: elige la **Operación** (**Añadir etiquetas**, **Eliminar etiquetas** o **Reemplazar todas las etiquetas**), elige las etiquetas y selecciona **Aplicar cambios**. **Eliminar todas las etiquetas** quita todas las etiquetas de la selección.
- **Mover a campaña**: elige la **Campaña de destino** y selecciona **Mover enlaces seleccionados**. Cada enlace conserva su origen, medio, término y contenido UTM. Requiere un plan con campañas.
- **Archivar**: saca los enlaces de la lista activa. Lo explicamos más abajo.
- **Eliminar seleccionados**: envía los enlaces a la papelera.

Los miembros solo pueden eliminar los enlaces que han creado. Si tu selección incluye enlaces de otras personas, la página de eliminación te indica cuántos puedes eliminar.

## Archivo

Archivar ordena la lista activa sin romper nada. Los enlaces archivados siguen redirigiendo y se mantienen en sus códigos QR, campañas y páginas bio. Sus estadísticas también se conservan.

Para verlos, selecciona **Archivados** encima de la lista. Selecciona enlaces ahí y elige **Desarchivar** para devolverlos a la lista activa.

## Elimina y restaura

Al eliminar un enlace, su URL corta deja de funcionar al instante, también en los códigos QR impresos. Durante 30 días:

- el enlace queda en la **Papelera** y puedes restaurarlo con su clave, su configuración, sus campañas y sus referencias en páginas bio;
- su clave queda reservada y nadie más puede usarla.

Pasados 30 días, el enlace se elimina para siempre. Las estadísticas agregadas se conservan.

Para restaurar enlaces:

1. Selecciona **Papelera** encima de la lista. La columna **Restaurar hasta** muestra el plazo de cada enlace.
2. Selecciona los enlaces que quieras, de varias páginas si hace falta.
3. Selecciona **Restaurar seleccionados**.

En la papelera y en los archivados también puedes usar **Agrupar esta página** con **Por primera etiqueta** para agrupar la página actual por etiqueta.

## Exporta

Selecciona **Exportar** en la página de enlaces para preparar un archivo con todos los enlaces que coinciden con los filtros actuales, en todas las páginas. Una exportación en CSV se puede volver a importar en Shortn.

## Siguientes pasos

- [Parámetros UTM](/docs/es/guides/links/utm-parameters)
- [Organizar campañas](/docs/es/guides/campaigns/organizing-campaigns)
- [Entender las estadísticas](/docs/es/guides/analytics/understanding-analytics)


---

# Parámetros UTM

Los parámetros UTM son etiquetas en la URL, como `utm_source=newsletter`, que las herramientas de estadísticas leen para saber de dónde viene una visita. En Shortn aparecen en tres sitios: en las variantes UTM de un enlace, en el generador de UTM de una campaña y en la configuración de redirección, que decide qué parámetros llegan al destino.

## Los cinco parámetros

- `utm_source`: de dónde viene el tráfico, por ejemplo `newsletter` o `facebook`.
- `utm_medium`: el tipo de canal, por ejemplo `email` o `social`.
- `utm_campaign`: la campaña a la que pertenece la visita.
- `utm_term`: la palabra clave de búsqueda de pago. Opcional.
- `utm_content`: distingue enlaces parecidos, como `cabecera` y `pie`. Opcional.

## Añade variantes UTM a un enlace

Una variante es una versión etiquetada de un enlace corto para un canal. Las visitas que llegan por ella se cuentan por origen, medio y campaña.

1. Abre el enlace y selecciona la pestaña **Variantes UTM**.
2. Selecciona **Añadir variante**.
3. Rellena **Origen**, **Medio** y **Campaña** y, si quieres, **Término** y **Contenido**. Al elegir una campaña guardada verás sus valores sugeridos.
4. Selecciona **Guardar variante**.

La lista de variantes muestra cada **URL etiquetada**: tu enlace corto con los parámetros añadidos, por ejemplo `shortn.at/primavera?utm_source=newsletter&utm_medium=email&utm_campaign=rebajas-primavera`. Selecciona **Copiar** para copiarla.

El valor de **Campaña** vincula la variante con la campaña del mismo nombre, y así el enlace entra en esa campaña. Las variantes UTM y las campañas dependen de tu plan.

Para cambiar una variante, selecciona **Editar**. Para borrarla, selecciona **Quitar** y confirma con **Eliminar variante**. Las URL que ya hayas compartido siguen redirigiendo y sus estadísticas se conservan. Si era la última variante del enlace en una campaña, el enlace sale de esa campaña.

## Etiqueta muchos enlaces a la vez

Para etiquetar varios enlaces para el mismo canal, usa la pestaña **Generador de UTM** de una campaña. Siempre fija `utm_campaign` con el título de la campaña, te sugiere los valores guardados y guarda la misma variante en todos los enlaces que selecciones. Consulta [Organizar campañas](/docs/es/guides/campaigns/organizing-campaigns).

## Controla lo que llega al destino

La URL etiquetada lleva los parámetros hasta Shortn, que los registra con cada visita. Que pasen o no al destino depende de la configuración de redirección del enlace.

1. Abre la pestaña **Editar** del enlace y despliega **Opciones avanzadas**.
2. Abre **Redirección y parámetros**.
3. Elige la configuración:
   - **Reenviar parámetros recibidos** pasa al destino los parámetros del visitante, incluidas las etiquetas UTM.
   - Los campos `utm_source`, `utm_medium`, `utm_campaign`, `utm_term` y `utm_content` fijan valores que siempre se añaden al destino.
4. Selecciona **Guardar cambios**.

Cuando el mismo parámetro aparece en más de un sitio, se aplican estas reglas:

- un valor UTM fijado en la configuración de redirección siempre gana;
- si no, un parámetro que ya esté en la URL de destino se mantiene frente al recibido;
- los parámetros recibidos solo se reenvían con el reenvío activado.

## Lee los resultados UTM

En la página **Estadísticas**, los desgloses **Fuentes UTM**, **Medios UTM** y **Campañas UTM** agrupan las visitas por etiqueta, y puedes filtrar por cualquier valor. Los desgloses dependen de tu plan. Consulta [Entender las estadísticas](/docs/es/guides/analytics/understanding-analytics).

## Buenas prácticas

- Escribe los valores en minúsculas y usa una sola grafía por canal, para que los resultados de un mismo canal no se repartan entre varios valores.
- Usa `utm_content` para comparar ubicaciones del mismo enlace, como la cabecera y el pie.
- No añadas etiquetas UTM a enlaces que apuntan a tu propio sitio; sustituyen el origen real de la visita.

## Siguientes pasos

- [Organizar campañas](/docs/es/guides/campaigns/organizing-campaigns)
- [Gestionar enlaces](/docs/es/guides/links/managing-links)


---

# Crear códigos QR

Cada código QR de Shortn abre un enlace corto. El patrón impreso no cambia nunca, así que puedes cambiar adónde lleva el código después de imprimirlo y seguir contando cada escaneo. Esta guía explica cómo crear un código y sacar tu primer archivo.

## Antes de empezar

Cada código QR que creas cuenta para el límite mensual de códigos QR de tu plan, que ves en **Uso** en la barra lateral. Un código puede apuntar a:

- **una URL**: Shortn crea un enlace corto para el código;
- **uno de tus enlaces**: el código queda vinculado a ese enlace y lo abre.

## Crea un código QR

1. Abre **Códigos QR** en la barra lateral y selecciona **Nuevo código QR**.
2. En **URL de destino**, escribe adónde debe llevar el escaneo. Para usar un enlace que ya tienes, selecciona **Elegir un enlace** en **O usa uno de tus enlaces:** y elígelo de la lista. **Usar una URL** vuelve a la opción anterior.
3. Si quieres, añade un **Título** para encontrar el código más tarde, y **Etiquetas**.
4. Selecciona **Crear código QR**.

Se abre el resumen del código. La vista previa del formulario usa un enlace corto de muestra; tu código recibe su propio enlace corto al crearlo.

También puedes partir de un enlace: ábrelo, selecciona su pestaña **Código QR** y luego **Crear un código QR**. El código queda vinculado a ese enlace y lo encontrarás en esa pestaña. Si el enlace ya tenía un código, el nuevo pasa a ser el código del enlace, y el antiguo sigue funcionando.

## Diseña el código

Selecciona **Editar diseño** en el resumen o abre la pestaña **Diseño**. Puedes cambiar los colores, el patrón, las esquinas, la corrección de errores, el logotipo, el marco y la leyenda. La comprobación de escaneo de la misma página te avisa si el contraste es bajo o el logotipo es demasiado grande. Cuando acabes, selecciona **Guardar diseño**.

Los cambios de diseño nunca cambian el enlace que lleva el código. La guía [Gestionar códigos QR](/docs/es/guides/qr-codes/managing-qr-codes) explica cada opción.

## Descarga el código

En el resumen, en **Descargar**, selecciona **Exportar para impresión**:

1. Elige el **Formato**: SVG, PNG o PDF. SVG es vectorial y se adapta a cualquier tamaño de impresión.
2. Elige el **Tamaño** en píxeles.
3. Selecciona **Preparar descarga** y, cuando esté listo, **Descargar archivo**.

La página de exportación indica el tamaño mínimo de impresión recomendado para el código. Para pantallas y correo, abre **JPEG para pantallas** en el resumen.

## Pruébalo antes de imprimir

- Escanea el archivo descargado con dos o tres móviles distintos.
- Deja espacio libre alrededor del código; los marcos y las leyendas se colocan fuera de ese espacio.
- Mantén un contraste fuerte entre el código y el fondo, con el código oscuro sobre fondo claro.
- No imprimas el código por debajo del tamaño mínimo recomendado.
- Añade junto al código una frase corta que diga qué se obtiene al escanearlo.

## Cambia el destino más adelante

Abre la pestaña **Detalles**, cambia la **URL de destino** y selecciona **Guardar detalles**. Las copias impresas siguen el nuevo destino desde el siguiente escaneo. Si el código está vinculado a un enlace, selecciona **Actualizar también el enlace** para copiar también a ese enlace el nuevo destino o título.

## Siguientes pasos

- [Gestionar códigos QR](/docs/es/guides/qr-codes/managing-qr-codes)
- [Entender las estadísticas](/docs/es/guides/analytics/understanding-analytics)


---

# Gestionar códigos QR

Esta guía describe las opciones de diseño de un código QR, la exportación de archivos para imprimir, la exportación de varios códigos en un archivo ZIP y la eliminación de códigos.

## Encuentra tus códigos

Abre **Códigos QR** en la barra lateral. Usa **Buscar** para buscar por título, destino o código, filtra por **Etiqueta** y por **Creado**, y cambia la **Vista** entre **Cuadrícula** y **Lista**. Selecciona un código para abrir su resumen, donde están las pestañas **Resumen**, **Diseño**, **Escaneos** y **Detalles**.

## Opciones de diseño

Abre la pestaña **Diseño**. La vista previa se actualiza con cada cambio y te avisa cuando incluye cambios sin guardar. Selecciona **Guardar diseño** para aplicarlos.

- **Colores**: elige un predefinido o define los colores del **Código** y del **Fondo**. En **Esquinas**, mantén el color del código o elige otro para las esquinas.
- **Patrón**: elige la forma de los **Puntos**, de los **Cuadrados de las esquinas** y de los **Puntos de las esquinas**. Las formas solo cambian el aspecto; el enlace sigue siendo el mismo.
- **Corrección de errores**: elige un **Nivel**. Los niveles más altos permiten que el código aguante roces y un logotipo, a cambio de un patrón más denso.
- **Logotipo**: sube una imagen para el centro del código, ajusta el **Tamaño del logotipo** y, si quieres, activa **Ocultar el patrón detrás del logotipo**. Mantén el logotipo en el 50 % de la corrección de errores o menos. Los logotipos dependen de tu plan.
- **Forma y leyenda**: elige una **Forma** cuadrada o circular, un **Marco** (ninguno, contorno o sólido) y una **Leyenda** opcional de hasta 64 caracteres. Marcos y leyendas aparecen en las descargas.

El panel **Comprobación de escaneo** valora el contraste entre el código y el fondo y avisa de colores invertidos, fondo transparente y tamaño del logotipo.

## Exporta para imprimir

1. En el resumen del código, en **Descargar**, selecciona **Exportar para impresión**.
2. Elige el **Formato** (SVG, PNG o PDF) y el **Tamaño**.
3. Lee las indicaciones de impresión de la página: cuatro módulos de margen alrededor del código, corrección Q (H con logotipo), contraste mínimo de 3:1 y el tamaño mínimo recomendado a 300 dpi.
4. Selecciona **Preparar descarga** y después **Descargar archivo**.

Las descargas caducan 24 horas después de iniciar la exportación. Para pantallas, **JPEG para pantallas** en el resumen te da un JPEG al momento.

## Exporta varios códigos en un ZIP

1. En la lista de códigos QR, marca los que quieras o usa **Seleccionar esta página**. La selección se mantiene al cambiar de página y de filtros.
2. Selecciona **Exportar selección**.
3. Revisa la selección. Los filtros de esta página solo cambian la lista que ves; el ZIP incluye todos los códigos seleccionados.
4. Elige el **Formato** y el **Tamaño** y selecciona **Crear ZIP**.
5. Cuando la exportación esté lista, selecciona **Descargar ZIP**.

La página de exportación indica cuántos códigos caben en un ZIP. La exportación continúa aunque salgas de la página.

## Edita los detalles y el destino

La pestaña **Detalles** reúne la **URL de destino**, el **Título** y las **Etiquetas**. Cambiar el destino no altera el patrón impreso; las copias impresas siguen el nuevo destino desde el siguiente escaneo. Selecciona **Guardar detalles** para aplicarlo.

## Escaneos

El resumen muestra los últimos 30 días: escaneos, visitantes, escaneos totales y la fecha del último escaneo. La pestaña **Escaneos** abre las estadísticas completas del código. Los escaneos también aparecen en la página **Estadísticas** del espacio de trabajo, separados de los clics en enlaces. Consulta [Entender las estadísticas](/docs/es/guides/analytics/understanding-analytics).

## Elimina códigos QR

Selecciona **Eliminar código QR** en el resumen, o selecciona códigos en la lista y elige **Eliminar seleccionados**. La página de confirmación explica qué ocurre:

- el código deja de funcionar al instante, porque su enlace corto se elimina con él;
- los enlaces a los que estaba vinculado el código siguen funcionando y solo pierden su código QR;
- el historial de escaneos se archiva y deja de mostrarse.

Eliminar un código QR no se puede deshacer y las copias impresas no se pueden recuperar. Los miembros solo pueden eliminar los códigos que han creado.

## Siguientes pasos

- [Crear códigos QR](/docs/es/guides/qr-codes/creating-qr-codes)
- [Crear páginas bio](/docs/es/guides/bio-pages/creating-bio-pages)


---

# Shortn API reference

Base URL: `https://api.shortn.at`. Authenticate with a scoped bearer API key.

This reference is generated from the implemented OpenAPI contract.

## GET /v1/links


```json
{
  "security": [
    {
      "ApiKey": []
    }
  ],
  "parameters": [
    {
      "schema": {
        "type": "string",
        "pattern": "^\\d+_[a-f\\d]{24}$/i"
      },
      "required": false,
      "name": "cursor",
      "in": "query"
    },
    {
      "schema": {
        "type": "integer",
        "minimum": 1,
        "maximum": 200,
        "default": 50
      },
      "required": false,
      "name": "limit",
      "in": "query"
    },
    {
      "schema": {
        "type": "string",
        "maxLength": 200
      },
      "required": false,
      "name": "search",
      "in": "query"
    },
    {
      "schema": {
        "type": "string",
        "maxLength": 2000
      },
      "required": false,
      "name": "tags",
      "in": "query"
    },
    {
      "schema": {
        "type": "string",
        "enum": [
          "true",
          "false"
        ],
        "default": "false"
      },
      "required": false,
      "name": "trash",
      "in": "query"
    },
    {
      "schema": {
        "type": "string",
        "maxLength": 100,
        "pattern": "^(?:tags|campaign)(?:,(?:tags|campaign))*$",
        "description": "Comma-separated full metadata expansions: tags,campaign. Each requires its resource read scope. Results are in expanded.tags / expanded.campaign arrays; unavailable references are omitted."
      },
      "required": false,
      "description": "Comma-separated full metadata expansions: tags,campaign. Each requires its resource read scope. Results are in expanded.tags / expanded.campaign arrays; unavailable references are omitted.",
      "name": "expand",
      "in": "query"
    }
  ],
  "responses": {
    "200": {
      "description": "Paginated links",
      "content": {
        "application/json": {
          "schema": {
            "type": "object",
            "properties": {
              "items": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Link"
                }
              },
              "nextCursor": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "total": {
                "type": [
                  "number",
                  "null"
                ]
              }
            },
            "required": [
              "items",
              "nextCursor",
              "total"
            ]
          }
        }
      }
    },
    "400": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "401": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "403": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "404": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "409": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "413": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "429": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "500": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    }
  }
}
```

## POST /v1/links


```json
{
  "security": [
    {
      "ApiKey": []
    }
  ],
  "requestBody": {
    "required": true,
    "content": {
      "application/json": {
        "schema": {
          "$ref": "#/components/schemas/CreateLink"
        }
      }
    }
  },
  "responses": {
    "201": {
      "description": "Created link",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/Link"
          }
        }
      }
    },
    "400": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "401": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "403": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "404": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "409": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "413": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "429": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "500": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    }
  }
}
```

## GET /v1/links/by-key


```json
{
  "security": [
    {
      "ApiKey": []
    }
  ],
  "parameters": [
    {
      "schema": {
        "type": "string",
        "pattern": "^[A-Za-z0-9_-]{1,64}$"
      },
      "required": true,
      "name": "key",
      "in": "query"
    },
    {
      "schema": {
        "type": "string",
        "minLength": 1,
        "maxLength": 253
      },
      "required": false,
      "name": "domain",
      "in": "query"
    },
    {
      "schema": {
        "type": "string",
        "maxLength": 100,
        "pattern": "^(?:tags|campaign)(?:,(?:tags|campaign))*$",
        "description": "Comma-separated full metadata expansions: tags,campaign. Each requires its resource read scope. Results are in expanded.tags / expanded.campaign arrays; unavailable references are omitted."
      },
      "required": false,
      "description": "Comma-separated full metadata expansions: tags,campaign. Each requires its resource read scope. Results are in expanded.tags / expanded.campaign arrays; unavailable references are omitted.",
      "name": "expand",
      "in": "query"
    }
  ],
  "responses": {
    "200": {
      "description": "A workspace link identified by domain and short key",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/Link"
          }
        }
      }
    },
    "400": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "401": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "403": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "404": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "409": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "413": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "429": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "500": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    }
  }
}
```

## GET /v1/links/by-key/{domain}/{key}


```json
{
  "security": [
    {
      "ApiKey": []
    }
  ],
  "parameters": [
    {
      "schema": {
        "type": "string",
        "minLength": 1,
        "maxLength": 253
      },
      "required": true,
      "name": "domain",
      "in": "path"
    },
    {
      "schema": {
        "type": "string",
        "pattern": "^[A-Za-z0-9_-]{1,64}$"
      },
      "required": true,
      "name": "key",
      "in": "path"
    },
    {
      "schema": {
        "type": "string",
        "maxLength": 100,
        "pattern": "^(?:tags|campaign)(?:,(?:tags|campaign))*$",
        "description": "Comma-separated full metadata expansions: tags,campaign. Each requires its resource read scope. Results are in expanded.tags / expanded.campaign arrays; unavailable references are omitted."
      },
      "required": false,
      "description": "Comma-separated full metadata expansions: tags,campaign. Each requires its resource read scope. Results are in expanded.tags / expanded.campaign arrays; unavailable references are omitted.",
      "name": "expand",
      "in": "query"
    }
  ],
  "responses": {
    "200": {
      "description": "Link by domain and short key",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/Link"
          }
        }
      }
    },
    "400": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "401": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "403": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "404": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "409": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "413": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "429": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "500": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    }
  }
}
```

## DELETE /v1/links/{id}


```json
{
  "security": [
    {
      "ApiKey": []
    }
  ],
  "parameters": [
    {
      "schema": {
        "type": "string",
        "pattern": "^[a-f\\d]{24}$/i"
      },
      "required": true,
      "name": "id",
      "in": "path"
    }
  ],
  "responses": {
    "204": {
      "description": "Link moved to trash for 30 days; keys remain reserved"
    },
    "400": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "401": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "403": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "404": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "409": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "413": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "429": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "500": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    }
  }
}
```

## GET /v1/links/{id}


```json
{
  "security": [
    {
      "ApiKey": []
    }
  ],
  "parameters": [
    {
      "schema": {
        "type": "string",
        "pattern": "^[a-f\\d]{24}$/i"
      },
      "required": true,
      "name": "id",
      "in": "path"
    },
    {
      "schema": {
        "type": "string",
        "maxLength": 100,
        "pattern": "^(?:tags|campaign)(?:,(?:tags|campaign))*$",
        "description": "Comma-separated full metadata expansions: tags,campaign. Each requires its resource read scope. Results are in expanded.tags / expanded.campaign arrays; unavailable references are omitted."
      },
      "required": false,
      "description": "Comma-separated full metadata expansions: tags,campaign. Each requires its resource read scope. Results are in expanded.tags / expanded.campaign arrays; unavailable references are omitted.",
      "name": "expand",
      "in": "query"
    }
  ],
  "responses": {
    "200": {
      "description": "Link",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/Link"
          }
        }
      }
    },
    "400": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "401": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "403": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "404": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "409": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "413": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "429": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "500": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    }
  }
}
```

## PATCH /v1/links/{id}


```json
{
  "security": [
    {
      "ApiKey": []
    }
  ],
  "parameters": [
    {
      "schema": {
        "type": "string",
        "pattern": "^[a-f\\d]{24}$/i"
      },
      "required": true,
      "name": "id",
      "in": "path"
    }
  ],
  "requestBody": {
    "required": true,
    "content": {
      "application/json": {
        "schema": {
          "$ref": "#/components/schemas/UpdateLink"
        }
      }
    }
  },
  "responses": {
    "200": {
      "description": "Updated link",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/Link"
          }
        }
      }
    },
    "400": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "401": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "403": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "404": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "409": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "413": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "429": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "500": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    }
  }
}
```

## POST /v1/links/{id}/restore


```json
{
  "security": [
    {
      "ApiKey": []
    }
  ],
  "parameters": [
    {
      "schema": {
        "type": "string",
        "pattern": "^[a-f\\d]{24}$/i"
      },
      "required": true,
      "name": "id",
      "in": "path"
    }
  ],
  "responses": {
    "200": {
      "description": "Restored link",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/Link"
          }
        }
      }
    },
    "400": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "401": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "403": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "404": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "409": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "413": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "429": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "500": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    }
  }
}
```

## POST /v1/links/bulk

Atomically create 1–1000 links, within the 1 MiB request body limit. Validation or quota failure creates no links or tags. Use Idempotency-Key for safe retries. The response preserves input order. Error context.index is zero-based; quota errors include limit, used and requested.


```json
{
  "security": [
    {
      "ApiKey": []
    }
  ],
  "description": "Atomically create 1–1000 links, within the 1 MiB request body limit. Validation or quota failure creates no links or tags. Use Idempotency-Key for safe retries. The response preserves input order. Error context.index is zero-based; quota errors include limit, used and requested.",
  "requestBody": {
    "required": true,
    "content": {
      "application/json": {
        "schema": {
          "$ref": "#/components/schemas/CreateLinksBulk"
        }
      }
    }
  },
  "responses": {
    "201": {
      "description": "All requested links created",
      "content": {
        "application/json": {
          "schema": {
            "type": "object",
            "properties": {
              "items": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Link"
                }
              },
              "count": {
                "type": "integer"
              }
            },
            "required": [
              "items",
              "count"
            ]
          }
        }
      }
    },
    "400": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "401": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "403": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "404": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "409": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "413": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "429": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "500": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    }
  }
}
```

## GET /v1/workspace


```json
{
  "security": [
    {
      "ApiKey": []
    }
  ],
  "responses": {
    "200": {
      "description": "The workspace associated with the bearer key",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/Workspace"
          }
        }
      }
    },
    "400": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "401": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "403": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "404": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "409": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "413": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "429": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "500": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    }
  }
}
```

## GET /v1/workspace/usage


```json
{
  "security": [
    {
      "ApiKey": []
    }
  ],
  "responses": {
    "200": {
      "description": "Monthly link/QR creation usage and current bio-page usage; null limits mean unlimited",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/WorkspaceUsage"
          }
        }
      }
    },
    "400": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "401": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "403": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "404": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "409": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "413": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "429": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "500": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    }
  }
}
```

## GET /v1/campaigns


```json
{
  "security": [
    {
      "ApiKey": []
    }
  ],
  "parameters": [
    {
      "schema": {
        "type": "string",
        "pattern": "^[a-f\\d]{24}$/i"
      },
      "required": false,
      "name": "cursor",
      "in": "query"
    },
    {
      "schema": {
        "type": "string",
        "maxLength": 200
      },
      "required": false,
      "name": "search",
      "in": "query"
    },
    {
      "schema": {
        "type": "integer",
        "minimum": 1,
        "maximum": 200,
        "default": 50
      },
      "required": false,
      "name": "limit",
      "in": "query"
    }
  ],
  "responses": {
    "200": {
      "description": "Campaigns in descending ID order, with bounded cursor pagination",
      "content": {
        "application/json": {
          "schema": {
            "type": "object",
            "properties": {
              "items": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Campaign"
                }
              },
              "total": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "nextCursor": {
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "required": [
              "items",
              "total",
              "nextCursor"
            ]
          }
        }
      }
    },
    "400": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "401": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "403": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "404": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "409": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "413": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "429": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "500": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    }
  }
}
```

## POST /v1/campaigns


```json
{
  "security": [
    {
      "ApiKey": []
    }
  ],
  "requestBody": {
    "required": true,
    "content": {
      "application/json": {
        "schema": {
          "$ref": "#/components/schemas/CreateCampaign"
        }
      }
    }
  },
  "responses": {
    "201": {
      "description": "Created campaign",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/Campaign"
          }
        }
      }
    },
    "400": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "401": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "403": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "404": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "409": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "413": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "429": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "500": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    }
  }
}
```

## GET /v1/campaigns/{id}


```json
{
  "security": [
    {
      "ApiKey": []
    }
  ],
  "parameters": [
    {
      "schema": {
        "type": "string",
        "pattern": "^[a-f\\d]{24}$/i"
      },
      "required": true,
      "name": "id",
      "in": "path"
    }
  ],
  "responses": {
    "200": {
      "description": "Campaign",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/Campaign"
          }
        }
      }
    },
    "400": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "401": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "403": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "404": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "409": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "413": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "429": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "500": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    }
  }
}
```

## PATCH /v1/campaigns/{id}


```json
{
  "security": [
    {
      "ApiKey": []
    }
  ],
  "parameters": [
    {
      "schema": {
        "type": "string",
        "pattern": "^[a-f\\d]{24}$/i"
      },
      "required": true,
      "name": "id",
      "in": "path"
    }
  ],
  "requestBody": {
    "required": true,
    "content": {
      "application/json": {
        "schema": {
          "$ref": "#/components/schemas/UpdateCampaign"
        }
      }
    }
  },
  "responses": {
    "200": {
      "description": "Updated campaign",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/Campaign"
          }
        }
      }
    },
    "400": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "401": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "403": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "404": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "409": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "413": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "429": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "500": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    }
  }
}
```

## DELETE /v1/campaigns/{id}


```json
{
  "security": [
    {
      "ApiKey": []
    }
  ],
  "parameters": [
    {
      "schema": {
        "type": "string",
        "pattern": "^[a-f\\d]{24}$/i"
      },
      "required": true,
      "name": "id",
      "in": "path"
    }
  ],
  "responses": {
    "204": {
      "description": "Campaign removed; linked short URLs are retained"
    },
    "400": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "401": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "403": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "404": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "409": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "413": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "429": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "500": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    }
  }
}
```

## GET /v1/qr-codes


```json
{
  "security": [
    {
      "ApiKey": []
    }
  ],
  "parameters": [
    {
      "schema": {
        "type": "string",
        "pattern": "^[a-f\\d]{24}$/i"
      },
      "required": false,
      "name": "cursor",
      "in": "query"
    },
    {
      "schema": {
        "type": "string",
        "maxLength": 200
      },
      "required": false,
      "name": "search",
      "in": "query"
    },
    {
      "schema": {
        "anyOf": [
          {
            "type": "string"
          },
          {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        ]
      },
      "required": false,
      "name": "tag",
      "in": "query"
    },
    {
      "schema": {
        "type": "integer",
        "minimum": 1,
        "maximum": 200,
        "default": 50
      },
      "required": false,
      "name": "limit",
      "in": "query"
    }
  ],
  "responses": {
    "200": {
      "description": "QR codes in descending ID order with bounded cursor pagination",
      "content": {
        "application/json": {
          "schema": {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/QrCode"
                }
              },
              "nextCursor": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "total": {
                "type": [
                  "number",
                  "null"
                ]
              }
            },
            "required": [
              "data",
              "nextCursor",
              "total"
            ]
          }
        }
      }
    },
    "400": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "401": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "403": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "404": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "409": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "413": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "429": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "500": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    }
  }
}
```

## POST /v1/qr-codes


```json
{
  "security": [
    {
      "ApiKey": []
    }
  ],
  "requestBody": {
    "required": true,
    "content": {
      "application/json": {
        "schema": {
          "$ref": "#/components/schemas/CreateQrCode"
        }
      }
    }
  },
  "responses": {
    "201": {
      "description": "QR code created with an independent backing short URL",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/QrCode"
          }
        }
      }
    },
    "400": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "401": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "403": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "404": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "409": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "413": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "429": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "500": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    }
  }
}
```

## GET /v1/qr-codes/{id}


```json
{
  "security": [
    {
      "ApiKey": []
    }
  ],
  "parameters": [
    {
      "schema": {
        "type": "string",
        "pattern": "^[a-f\\d]{24}$/i"
      },
      "required": true,
      "name": "id",
      "in": "path"
    }
  ],
  "responses": {
    "200": {
      "description": "Workspace-owned QR code",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/QrCode"
          }
        }
      }
    },
    "400": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "401": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "403": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "404": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "409": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "413": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "429": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "500": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    }
  }
}
```

## PATCH /v1/qr-codes/{id}


```json
{
  "security": [
    {
      "ApiKey": []
    }
  ],
  "parameters": [
    {
      "schema": {
        "type": "string",
        "pattern": "^[a-f\\d]{24}$/i"
      },
      "required": true,
      "name": "id",
      "in": "path"
    }
  ],
  "requestBody": {
    "required": true,
    "content": {
      "application/json": {
        "schema": {
          "$ref": "#/components/schemas/UpdateQrCode"
        }
      }
    }
  },
  "responses": {
    "200": {
      "description": "Updated QR code; applying changes to its attached link also requires links:write",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/QrCode"
          }
        }
      }
    },
    "400": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "401": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "403": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "404": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "409": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "413": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "429": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "500": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    }
  }
}
```

## DELETE /v1/qr-codes/{id}


```json
{
  "security": [
    {
      "ApiKey": []
    }
  ],
  "parameters": [
    {
      "schema": {
        "type": "string",
        "pattern": "^[a-f\\d]{24}$/i"
      },
      "required": true,
      "name": "id",
      "in": "path"
    }
  ],
  "responses": {
    "204": {
      "description": "QR code and its independent backing link removed; attached links retained"
    },
    "400": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "401": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "403": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "404": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "409": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "413": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "429": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "500": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    }
  }
}
```

## POST /v1/qr-codes/export


```json
{
  "security": [
    {
      "ApiKey": []
    }
  ],
  "requestBody": {
    "required": true,
    "content": {
      "application/json": {
        "schema": {
          "type": "object",
          "properties": {
            "ids": {
              "type": "array",
              "items": {
                "type": "string",
                "pattern": "^[a-f0-9]{24}$/i"
              },
              "minItems": 1,
              "maxItems": 500
            },
            "format": {
              "type": "string",
              "enum": [
                "svg",
                "png",
                "pdf"
              ],
              "default": "svg"
            },
            "size": {
              "type": "integer",
              "minimum": 128,
              "maximum": 4096,
              "default": 1024
            }
          },
          "required": [
            "ids"
          ],
          "additionalProperties": false
        }
      }
    }
  },
  "responses": {
    "202": {
      "description": "ZIP job queued; up to 500 selected QR codes, maximum 536870912 bytes",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/QrZipExportJob"
          }
        }
      }
    },
    "400": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "401": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "403": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "404": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "409": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "413": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "429": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "500": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    }
  }
}
```

## GET /v1/qr-exports/zip/{id}


```json
{
  "security": [
    {
      "ApiKey": []
    }
  ],
  "parameters": [
    {
      "schema": {
        "type": "string",
        "pattern": "^[a-f\\d]{24}$/i"
      },
      "required": true,
      "name": "id",
      "in": "path"
    }
  ],
  "responses": {
    "200": {
      "description": "ZIP progress and private delivery; all selected QR codes must remain readable",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/QrZipExportJob"
          }
        }
      }
    },
    "400": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "401": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "403": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "404": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "409": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "413": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "429": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "500": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "503": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    }
  }
}
```

## GET /v1/qr-codes/{id}/export


```json
{
  "security": [
    {
      "ApiKey": []
    }
  ],
  "parameters": [
    {
      "schema": {
        "type": "string",
        "pattern": "^[a-f\\d]{24}$/i"
      },
      "required": true,
      "name": "id",
      "in": "path"
    },
    {
      "schema": {
        "type": "string",
        "enum": [
          "svg",
          "png",
          "pdf"
        ],
        "default": "svg"
      },
      "required": false,
      "name": "format",
      "in": "query"
    },
    {
      "schema": {
        "type": "integer",
        "minimum": 128,
        "maximum": 4096,
        "default": 1024
      },
      "required": false,
      "name": "size",
      "in": "query"
    }
  ],
  "responses": {
    "202": {
      "description": "Export queued or rendering; poll Location with the same workspace credential",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/QrExportJob"
          }
        }
      }
    },
    "303": {
      "description": "Export ready; Location is a private attachment URL expiring with the job"
    },
    "400": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "401": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "403": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "404": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "409": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "413": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "429": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "500": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "503": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    }
  }
}
```

## GET /v1/qr-exports/{id}


```json
{
  "security": [
    {
      "ApiKey": []
    }
  ],
  "parameters": [
    {
      "schema": {
        "type": "string",
        "pattern": "^[a-f\\d]{24}$/i"
      },
      "required": true,
      "name": "id",
      "in": "path"
    }
  ],
  "responses": {
    "200": {
      "description": "Authorized export status and, when ready, an expiring private download URL",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/QrExportJob"
          }
        }
      }
    },
    "400": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "401": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "403": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "404": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "409": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "413": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "429": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "500": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "503": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    }
  }
}
```

## GET /v1/tags


```json
{
  "security": [
    {
      "ApiKey": []
    }
  ],
  "parameters": [
    {
      "schema": {
        "type": "string",
        "pattern": "^[a-f\\d]{24}$/i"
      },
      "required": false,
      "name": "cursor",
      "in": "query"
    },
    {
      "schema": {
        "type": "string",
        "maxLength": 200
      },
      "required": false,
      "name": "search",
      "in": "query"
    },
    {
      "schema": {
        "type": "integer",
        "minimum": 1,
        "maximum": 200,
        "default": 50
      },
      "required": false,
      "name": "limit",
      "in": "query"
    }
  ],
  "responses": {
    "200": {
      "description": "Workspace tags in descending ID order with cursor pagination",
      "content": {
        "application/json": {
          "schema": {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Tag"
                }
              },
              "nextCursor": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "total": {
                "type": [
                  "number",
                  "null"
                ]
              }
            },
            "required": [
              "data",
              "nextCursor",
              "total"
            ]
          }
        }
      }
    },
    "400": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "401": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "403": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "404": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "409": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "413": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "429": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "500": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    }
  }
}
```

## POST /v1/tags


```json
{
  "security": [
    {
      "ApiKey": []
    }
  ],
  "requestBody": {
    "required": true,
    "content": {
      "application/json": {
        "schema": {
          "$ref": "#/components/schemas/WriteTag"
        }
      }
    }
  },
  "responses": {
    "201": {
      "description": "Workspace tag created",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/Tag"
          }
        }
      }
    },
    "400": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "401": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "403": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "404": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "409": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "413": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "429": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "500": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    }
  }
}
```

## GET /v1/tags/{id}


```json
{
  "security": [
    {
      "ApiKey": []
    }
  ],
  "parameters": [
    {
      "schema": {
        "type": "string",
        "pattern": "^[a-z\\d_-]{1,64}$/i"
      },
      "required": true,
      "name": "id",
      "in": "path"
    }
  ],
  "responses": {
    "200": {
      "description": "Workspace tag, accepting canonical or legacy identifiers",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/Tag"
          }
        }
      }
    },
    "400": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "401": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "403": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "404": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "409": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "413": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "429": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "500": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    }
  }
}
```

## PATCH /v1/tags/{id}


```json
{
  "security": [
    {
      "ApiKey": []
    }
  ],
  "parameters": [
    {
      "schema": {
        "type": "string",
        "pattern": "^[a-z\\d_-]{1,64}$/i"
      },
      "required": true,
      "name": "id",
      "in": "path"
    }
  ],
  "requestBody": {
    "required": true,
    "content": {
      "application/json": {
        "schema": {
          "$ref": "#/components/schemas/WriteTag"
        }
      }
    }
  },
  "responses": {
    "200": {
      "description": "Tag and its link/QR references renamed transactionally",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/Tag"
          }
        }
      }
    },
    "400": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "401": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "403": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "404": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "409": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "413": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "429": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "500": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    }
  }
}
```

## DELETE /v1/tags/{id}


```json
{
  "security": [
    {
      "ApiKey": []
    }
  ],
  "parameters": [
    {
      "schema": {
        "type": "string",
        "pattern": "^[a-z\\d_-]{1,64}$/i"
      },
      "required": true,
      "name": "id",
      "in": "path"
    }
  ],
  "responses": {
    "204": {
      "description": "Tag removed from the workspace and its link/QR references"
    },
    "400": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "401": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "403": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "404": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "409": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "413": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "429": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "500": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    }
  }
}
```

## GET /v1/analytics


```json
{
  "security": [
    {
      "ApiKey": []
    }
  ],
  "parameters": [
    {
      "schema": {
        "type": "string",
        "format": "date-time"
      },
      "required": true,
      "name": "from",
      "in": "query"
    },
    {
      "schema": {
        "type": "string",
        "format": "date-time"
      },
      "required": true,
      "name": "to",
      "in": "query"
    },
    {
      "schema": {
        "type": "string",
        "maxLength": 64,
        "default": "UTC"
      },
      "required": false,
      "name": "timezone",
      "in": "query"
    },
    {
      "schema": {
        "type": "string",
        "enum": [
          "hour",
          "day",
          "week",
          "month"
        ],
        "default": "day"
      },
      "required": false,
      "name": "interval",
      "in": "query"
    },
    {
      "schema": {
        "type": "string",
        "enum": [
          "branch",
          "country",
          "city",
          "continent",
          "device",
          "browser",
          "os",
          "referrer",
          "utmSource",
          "utmMedium",
          "utmCampaign",
          "utmTerm",
          "utmContent",
          "language",
          "link",
          "kind"
        ]
      },
      "required": false,
      "name": "groupBy",
      "in": "query"
    },
    {
      "schema": {
        "type": "integer",
        "minimum": 1,
        "maximum": 1000,
        "default": 50
      },
      "required": false,
      "name": "limit",
      "in": "query"
    },
    {
      "schema": {
        "type": "string",
        "pattern": "^[a-f\\d]{24}$/i"
      },
      "required": false,
      "name": "link",
      "in": "query"
    },
    {
      "schema": {
        "type": "string",
        "enum": [
          "click",
          "scan"
        ]
      },
      "required": false,
      "name": "kind",
      "in": "query"
    },
    {
      "schema": {
        "type": "string",
        "minLength": 1,
        "maxLength": 200
      },
      "required": false,
      "name": "country",
      "in": "query"
    },
    {
      "schema": {
        "type": "string",
        "minLength": 1,
        "maxLength": 200
      },
      "required": false,
      "name": "city",
      "in": "query"
    },
    {
      "schema": {
        "type": "string",
        "minLength": 1,
        "maxLength": 200
      },
      "required": false,
      "name": "continent",
      "in": "query"
    },
    {
      "schema": {
        "type": "string",
        "minLength": 1,
        "maxLength": 200
      },
      "required": false,
      "name": "device",
      "in": "query"
    },
    {
      "schema": {
        "type": "string",
        "minLength": 1,
        "maxLength": 200
      },
      "required": false,
      "name": "browser",
      "in": "query"
    },
    {
      "schema": {
        "type": "string",
        "minLength": 1,
        "maxLength": 200
      },
      "required": false,
      "name": "os",
      "in": "query"
    },
    {
      "schema": {
        "type": "string",
        "minLength": 1,
        "maxLength": 200
      },
      "required": false,
      "name": "referrer",
      "in": "query"
    },
    {
      "schema": {
        "type": "string",
        "minLength": 1,
        "maxLength": 200
      },
      "required": false,
      "name": "utmSource",
      "in": "query"
    },
    {
      "schema": {
        "type": "string",
        "minLength": 1,
        "maxLength": 200
      },
      "required": false,
      "name": "utmMedium",
      "in": "query"
    },
    {
      "schema": {
        "type": "string",
        "minLength": 1,
        "maxLength": 200
      },
      "required": false,
      "name": "utmCampaign",
      "in": "query"
    },
    {
      "schema": {
        "type": "string",
        "minLength": 1,
        "maxLength": 200
      },
      "required": false,
      "name": "utmTerm",
      "in": "query"
    },
    {
      "schema": {
        "type": "string",
        "minLength": 1,
        "maxLength": 200
      },
      "required": false,
      "name": "utmContent",
      "in": "query"
    },
    {
      "schema": {
        "type": "string",
        "minLength": 1,
        "maxLength": 200
      },
      "required": false,
      "name": "language",
      "in": "query"
    },
    {
      "schema": {
        "type": "string",
        "minLength": 1,
        "maxLength": 64
      },
      "required": false,
      "name": "branch",
      "in": "query"
    }
  ],
  "responses": {
    "200": {
      "description": "Analytics within the plan query window; gated lists unavailable breakdowns",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/Analytics"
          }
        }
      }
    },
    "400": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "401": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "403": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "404": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "409": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "413": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "429": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "500": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    }
  }
}
```

## GET /v1/links/{id}/analytics


```json
{
  "security": [
    {
      "ApiKey": []
    }
  ],
  "parameters": [
    {
      "schema": {
        "type": "string",
        "pattern": "^[a-f\\d]{24}$/i"
      },
      "required": true,
      "name": "id",
      "in": "path"
    },
    {
      "schema": {
        "type": "string",
        "format": "date-time"
      },
      "required": true,
      "name": "from",
      "in": "query"
    },
    {
      "schema": {
        "type": "string",
        "format": "date-time"
      },
      "required": true,
      "name": "to",
      "in": "query"
    },
    {
      "schema": {
        "type": "string",
        "maxLength": 64,
        "default": "UTC"
      },
      "required": false,
      "name": "timezone",
      "in": "query"
    },
    {
      "schema": {
        "type": "string",
        "enum": [
          "hour",
          "day",
          "week",
          "month"
        ],
        "default": "day"
      },
      "required": false,
      "name": "interval",
      "in": "query"
    },
    {
      "schema": {
        "type": "string",
        "enum": [
          "branch",
          "country",
          "city",
          "continent",
          "device",
          "browser",
          "os",
          "referrer",
          "utmSource",
          "utmMedium",
          "utmCampaign",
          "utmTerm",
          "utmContent",
          "language",
          "link",
          "kind"
        ]
      },
      "required": false,
      "name": "groupBy",
      "in": "query"
    },
    {
      "schema": {
        "type": "integer",
        "minimum": 1,
        "maximum": 1000,
        "default": 50
      },
      "required": false,
      "name": "limit",
      "in": "query"
    },
    {
      "schema": {
        "type": "string",
        "pattern": "^[a-f\\d]{24}$/i"
      },
      "required": false,
      "name": "link",
      "in": "query"
    },
    {
      "schema": {
        "type": "string",
        "enum": [
          "click",
          "scan"
        ]
      },
      "required": false,
      "name": "kind",
      "in": "query"
    },
    {
      "schema": {
        "type": "string",
        "minLength": 1,
        "maxLength": 200
      },
      "required": false,
      "name": "country",
      "in": "query"
    },
    {
      "schema": {
        "type": "string",
        "minLength": 1,
        "maxLength": 200
      },
      "required": false,
      "name": "city",
      "in": "query"
    },
    {
      "schema": {
        "type": "string",
        "minLength": 1,
        "maxLength": 200
      },
      "required": false,
      "name": "continent",
      "in": "query"
    },
    {
      "schema": {
        "type": "string",
        "minLength": 1,
        "maxLength": 200
      },
      "required": false,
      "name": "device",
      "in": "query"
    },
    {
      "schema": {
        "type": "string",
        "minLength": 1,
        "maxLength": 200
      },
      "required": false,
      "name": "browser",
      "in": "query"
    },
    {
      "schema": {
        "type": "string",
        "minLength": 1,
        "maxLength": 200
      },
      "required": false,
      "name": "os",
      "in": "query"
    },
    {
      "schema": {
        "type": "string",
        "minLength": 1,
        "maxLength": 200
      },
      "required": false,
      "name": "referrer",
      "in": "query"
    },
    {
      "schema": {
        "type": "string",
        "minLength": 1,
        "maxLength": 200
      },
      "required": false,
      "name": "utmSource",
      "in": "query"
    },
    {
      "schema": {
        "type": "string",
        "minLength": 1,
        "maxLength": 200
      },
      "required": false,
      "name": "utmMedium",
      "in": "query"
    },
    {
      "schema": {
        "type": "string",
        "minLength": 1,
        "maxLength": 200
      },
      "required": false,
      "name": "utmCampaign",
      "in": "query"
    },
    {
      "schema": {
        "type": "string",
        "minLength": 1,
        "maxLength": 200
      },
      "required": false,
      "name": "utmTerm",
      "in": "query"
    },
    {
      "schema": {
        "type": "string",
        "minLength": 1,
        "maxLength": 200
      },
      "required": false,
      "name": "utmContent",
      "in": "query"
    },
    {
      "schema": {
        "type": "string",
        "minLength": 1,
        "maxLength": 200
      },
      "required": false,
      "name": "language",
      "in": "query"
    },
    {
      "schema": {
        "type": "string",
        "minLength": 1,
        "maxLength": 64
      },
      "required": false,
      "name": "branch",
      "in": "query"
    }
  ],
  "responses": {
    "200": {
      "description": "Analytics within the plan query window; gated lists unavailable breakdowns",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/Analytics"
          }
        }
      }
    },
    "400": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "401": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "403": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "404": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "409": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "413": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "429": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "500": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    }
  }
}
```

## GET /v1/domains


```json
{
  "security": [
    {
      "ApiKey": []
    }
  ],
  "parameters": [
    {
      "schema": {
        "type": "string",
        "pattern": "^[a-f\\d]{24}$/i"
      },
      "required": false,
      "name": "cursor",
      "in": "query"
    },
    {
      "schema": {
        "type": "integer",
        "minimum": 1,
        "maximum": 200,
        "default": 50
      },
      "required": false,
      "name": "limit",
      "in": "query"
    },
    {
      "schema": {
        "type": "string",
        "maxLength": 253
      },
      "required": false,
      "name": "search",
      "in": "query"
    }
  ],
  "responses": {
    "200": {
      "description": "Paginated workspace domains",
      "content": {
        "application/json": {
          "schema": {
            "type": "object",
            "properties": {
              "items": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Domain"
                }
              },
              "nextCursor": {
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "required": [
              "items",
              "nextCursor"
            ]
          }
        }
      }
    },
    "400": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "401": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "403": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "404": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "409": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "413": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "429": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "500": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    }
  }
}
```

## POST /v1/domains


```json
{
  "security": [
    {
      "ApiKey": []
    }
  ],
  "requestBody": {
    "required": true,
    "content": {
      "application/json": {
        "schema": {
          "type": "object",
          "properties": {
            "hostname": {
              "type": "string",
              "maxLength": 253
            },
            "kind": {
              "type": "string",
              "enum": [
                "links",
                "bio",
                "both"
              ]
            },
            "bioPageId": {
              "type": "string",
              "pattern": "^[a-f\\d]{24}$/i"
            },
            "rootRedirect": {
              "type": "string",
              "format": "uri"
            },
            "notFoundRedirect": {
              "type": "string",
              "format": "uri"
            }
          },
          "required": [
            "hostname",
            "kind"
          ],
          "additionalProperties": false
        }
      }
    }
  },
  "responses": {
    "201": {
      "description": "Hostname reserved for asynchronous verification; requires workspace owner and paid domain add-on",
      "content": {
        "application/json": {
          "schema": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              }
            },
            "required": [
              "id"
            ]
          }
        }
      }
    },
    "400": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "401": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "403": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "404": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "409": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "413": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "429": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "500": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    }
  }
}
```

## GET /v1/domains/{id}/status


```json
{
  "security": [
    {
      "ApiKey": []
    }
  ],
  "parameters": [
    {
      "schema": {
        "type": "string",
        "pattern": "^[a-f\\d]{24}$/i"
      },
      "required": true,
      "name": "id",
      "in": "path"
    }
  ],
  "responses": {
    "200": {
      "description": "Persisted hostname verification status; provider polling runs in the worker",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/Domain"
          }
        }
      }
    },
    "400": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "401": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "403": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "404": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "409": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "413": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "429": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "500": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    }
  }
}
```

## DELETE /v1/domains/{id}


```json
{
  "security": [
    {
      "ApiKey": []
    }
  ],
  "parameters": [
    {
      "schema": {
        "type": "string",
        "pattern": "^[a-f\\d]{24}$/i"
      },
      "required": true,
      "name": "id",
      "in": "path"
    }
  ],
  "responses": {
    "204": {
      "description": "Domain disabled immediately; provider removal is asynchronous"
    },
    "400": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "401": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "403": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "404": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "409": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "413": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "429": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "500": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    }
  }
}
```

## GET /v1/bio-pages


```json
{
  "security": [
    {
      "ApiKey": []
    }
  ],
  "parameters": [
    {
      "schema": {
        "type": "string",
        "pattern": "^[a-f\\d]{24}$/i"
      },
      "required": false,
      "name": "cursor",
      "in": "query"
    },
    {
      "schema": {
        "type": "integer",
        "minimum": 1,
        "maximum": 200,
        "default": 50
      },
      "required": false,
      "name": "limit",
      "in": "query"
    },
    {
      "schema": {
        "type": "string",
        "maxLength": 200
      },
      "required": false,
      "name": "search",
      "in": "query"
    }
  ],
  "responses": {
    "200": {
      "description": "Paginated bio page summaries",
      "content": {
        "application/json": {
          "schema": {
            "type": "object",
            "properties": {
              "items": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/BioSummary"
                }
              },
              "nextCursor": {
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "required": [
              "items",
              "nextCursor"
            ]
          }
        }
      }
    },
    "400": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "401": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "403": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "404": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "409": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "413": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "429": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "500": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    }
  }
}
```

## POST /v1/bio-pages


```json
{
  "security": [
    {
      "ApiKey": []
    }
  ],
  "requestBody": {
    "required": true,
    "content": {
      "application/json": {
        "schema": {
          "type": "object",
          "properties": {
            "slug": {
              "type": "string",
              "minLength": 1,
              "maxLength": 63
            },
            "title": {
              "type": "string",
              "maxLength": 120
            },
            "linkId": {
              "type": "string",
              "pattern": "^[a-f\\d]{24}$/i"
            }
          },
          "required": [
            "slug"
          ],
          "additionalProperties": false
        }
      }
    }
  },
  "responses": {
    "201": {
      "description": "Created unpublished bio page",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/BioSummary"
          }
        }
      }
    },
    "400": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "401": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "403": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "404": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "409": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "413": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "429": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "500": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    }
  }
}
```

## GET /v1/bio-pages/{id}


```json
{
  "security": [
    {
      "ApiKey": []
    }
  ],
  "parameters": [
    {
      "schema": {
        "type": "string",
        "pattern": "^[a-f\\d]{24}$/i"
      },
      "required": true,
      "name": "id",
      "in": "path"
    }
  ],
  "responses": {
    "200": {
      "description": "Current editable draft and revision",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/BioDraft"
          }
        }
      }
    },
    "400": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "401": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "403": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "404": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "409": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "413": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "429": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "500": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    }
  }
}
```

## PATCH /v1/bio-pages/{id}


```json
{
  "security": [
    {
      "ApiKey": []
    }
  ],
  "parameters": [
    {
      "schema": {
        "type": "string",
        "pattern": "^[a-f\\d]{24}$/i"
      },
      "required": true,
      "name": "id",
      "in": "path"
    }
  ],
  "requestBody": {
    "required": true,
    "content": {
      "application/json": {
        "schema": {
          "type": "object",
          "properties": {
            "revision": {
              "type": "integer",
              "minimum": 0
            },
            "content": {
              "type": "object",
              "properties": {
                "locale": {
                  "type": "string",
                  "enum": [
                    "en",
                    "pt",
                    "es"
                  ]
                },
                "blocks": {
                  "type": "array",
                  "items": {
                    "oneOf": [
                      {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "minLength": 1,
                            "maxLength": 100
                          },
                          "visible": {
                            "type": "boolean",
                            "default": true
                          },
                          "schedule": {
                            "type": "object",
                            "properties": {
                              "from": {
                                "type": "string",
                                "format": "date-time"
                              },
                              "to": {
                                "type": "string",
                                "format": "date-time"
                              }
                            }
                          },
                          "type": {
                            "type": "string",
                            "enum": [
                              "profile"
                            ]
                          },
                          "title": {
                            "type": "string",
                            "maxLength": 120
                          },
                          "description": {
                            "type": "string",
                            "maxLength": 500,
                            "default": ""
                          },
                          "avatar": {
                            "anyOf": [
                              {
                                "type": "string",
                                "maxLength": 2048,
                                "format": "uri"
                              },
                              {
                                "type": "string",
                                "enum": [
                                  ""
                                ]
                              }
                            ],
                            "default": ""
                          },
                          "align": {
                            "type": "string",
                            "enum": [
                              "center",
                              "left",
                              "right"
                            ],
                            "default": "center"
                          },
                          "avatarShape": {
                            "type": "string",
                            "enum": [
                              "circle",
                              "rounded",
                              "square"
                            ],
                            "default": "circle"
                          },
                          "paidBadge": {
                            "type": "boolean"
                          }
                        },
                        "required": [
                          "id",
                          "type",
                          "title"
                        ]
                      },
                      {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "minLength": 1,
                            "maxLength": 100
                          },
                          "visible": {
                            "type": "boolean",
                            "default": true
                          },
                          "schedule": {
                            "type": "object",
                            "properties": {
                              "from": {
                                "type": "string",
                                "format": "date-time"
                              },
                              "to": {
                                "type": "string",
                                "format": "date-time"
                              }
                            }
                          },
                          "type": {
                            "type": "string",
                            "enum": [
                              "link"
                            ]
                          },
                          "linkId": {
                            "type": "string",
                            "pattern": "^[a-f\\d]{24}$/i"
                          },
                          "title": {
                            "type": "string",
                            "maxLength": 120
                          },
                          "image": {
                            "anyOf": [
                              {
                                "type": "string",
                                "maxLength": 2048,
                                "format": "uri"
                              },
                              {
                                "type": "string",
                                "enum": [
                                  ""
                                ]
                              }
                            ],
                            "default": ""
                          },
                          "highlight": {
                            "type": "boolean",
                            "default": false
                          }
                        },
                        "required": [
                          "id",
                          "type",
                          "linkId",
                          "title"
                        ]
                      },
                      {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "minLength": 1,
                            "maxLength": 100
                          },
                          "visible": {
                            "type": "boolean",
                            "default": true
                          },
                          "schedule": {
                            "type": "object",
                            "properties": {
                              "from": {
                                "type": "string",
                                "format": "date-time"
                              },
                              "to": {
                                "type": "string",
                                "format": "date-time"
                              }
                            }
                          },
                          "type": {
                            "type": "string",
                            "enum": [
                              "links"
                            ]
                          },
                          "title": {
                            "type": "string",
                            "maxLength": 120,
                            "default": ""
                          },
                          "layout": {
                            "type": "string",
                            "enum": [
                              "vertical",
                              "paginated"
                            ],
                            "default": "vertical"
                          },
                          "pageSize": {
                            "type": "integer",
                            "minimum": 4,
                            "maximum": 50,
                            "default": 10
                          },
                          "cardStyle": {
                            "type": "string",
                            "enum": [
                              "list",
                              "small-grid",
                              "large-grid",
                              "image-grid"
                            ],
                            "default": "list"
                          },
                          "cardContent": {
                            "type": "string",
                            "enum": [
                              "image-text",
                              "image-only",
                              "text"
                            ],
                            "default": "image-text"
                          },
                          "cardRadius": {
                            "type": "string",
                            "enum": [
                              "theme",
                              "square",
                              "rounded"
                            ],
                            "default": "theme"
                          },
                          "imageFit": {
                            "type": "string",
                            "enum": [
                              "cover",
                              "contain"
                            ],
                            "default": "cover"
                          },
                          "linkSurface": {
                            "type": "string",
                            "enum": [
                              "theme",
                              "minimal",
                              "soft",
                              "filled",
                              "outline"
                            ],
                            "default": "theme"
                          },
                          "linkBorder": {
                            "type": "string",
                            "enum": [
                              "theme",
                              "none",
                              "subtle",
                              "solid",
                              "divider"
                            ],
                            "default": "theme"
                          },
                          "linkWeight": {
                            "type": "string",
                            "enum": [
                              "theme",
                              "regular",
                              "medium",
                              "bold"
                            ],
                            "default": "theme"
                          },
                          "linkSpacing": {
                            "type": "string",
                            "enum": [
                              "compact",
                              "comfortable",
                              "spacious"
                            ],
                            "default": "comfortable"
                          },
                          "showLinkArrow": {
                            "type": "boolean",
                            "default": true
                          },
                          "paginationAppearance": {
                            "type": "string",
                            "enum": [
                              "muted",
                              "minimal",
                              "outline",
                              "filled"
                            ],
                            "default": "muted"
                          },
                          "paginationSize": {
                            "type": "string",
                            "enum": [
                              "small",
                              "comfortable"
                            ],
                            "default": "small"
                          },
                          "paginationStyle": {
                            "type": "string",
                            "enum": [
                              "arrows",
                              "labels"
                            ],
                            "default": "arrows"
                          },
                          "showPageCount": {
                            "type": "boolean",
                            "default": true
                          },
                          "showLinkCount": {
                            "type": "boolean",
                            "default": false
                          },
                          "items": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "id": {
                                  "type": "string",
                                  "minLength": 1,
                                  "maxLength": 100
                                },
                                "visible": {
                                  "type": "boolean",
                                  "default": true
                                },
                                "schedule": {
                                  "type": "object",
                                  "properties": {
                                    "from": {
                                      "type": "string",
                                      "format": "date-time"
                                    },
                                    "to": {
                                      "type": "string",
                                      "format": "date-time"
                                    }
                                  }
                                },
                                "linkId": {
                                  "type": "string",
                                  "pattern": "^[a-f\\d]{24}$/i"
                                },
                                "title": {
                                  "type": "string",
                                  "maxLength": 120
                                },
                                "image": {
                                  "anyOf": [
                                    {
                                      "type": "string",
                                      "maxLength": 2048,
                                      "format": "uri"
                                    },
                                    {
                                      "type": "string",
                                      "enum": [
                                        ""
                                      ]
                                    }
                                  ],
                                  "default": ""
                                },
                                "highlight": {
                                  "type": "boolean",
                                  "default": false
                                }
                              },
                              "required": [
                                "id",
                                "linkId",
                                "title"
                              ]
                            },
                            "maxItems": 1000
                          }
                        },
                        "required": [
                          "id",
                          "type",
                          "items"
                        ]
                      },
                      {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "minLength": 1,
                            "maxLength": 100
                          },
                          "visible": {
                            "type": "boolean",
                            "default": true
                          },
                          "schedule": {
                            "type": "object",
                            "properties": {
                              "from": {
                                "type": "string",
                                "format": "date-time"
                              },
                              "to": {
                                "type": "string",
                                "format": "date-time"
                              }
                            }
                          },
                          "type": {
                            "type": "string",
                            "enum": [
                              "socials"
                            ]
                          },
                          "items": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "platform": {
                                  "type": "string",
                                  "minLength": 1,
                                  "maxLength": 32
                                },
                                "url": {
                                  "type": "string",
                                  "maxLength": 2048,
                                  "format": "uri"
                                }
                              },
                              "required": [
                                "platform",
                                "url"
                              ]
                            },
                            "maxItems": 20
                          }
                        },
                        "required": [
                          "id",
                          "type",
                          "items"
                        ]
                      },
                      {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "minLength": 1,
                            "maxLength": 100
                          },
                          "visible": {
                            "type": "boolean",
                            "default": true
                          },
                          "schedule": {
                            "type": "object",
                            "properties": {
                              "from": {
                                "type": "string",
                                "format": "date-time"
                              },
                              "to": {
                                "type": "string",
                                "format": "date-time"
                              }
                            }
                          },
                          "type": {
                            "type": "string",
                            "enum": [
                              "header"
                            ]
                          },
                          "text": {
                            "type": "string",
                            "maxLength": 120
                          }
                        },
                        "required": [
                          "id",
                          "type",
                          "text"
                        ]
                      },
                      {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "minLength": 1,
                            "maxLength": 100
                          },
                          "visible": {
                            "type": "boolean",
                            "default": true
                          },
                          "schedule": {
                            "type": "object",
                            "properties": {
                              "from": {
                                "type": "string",
                                "format": "date-time"
                              },
                              "to": {
                                "type": "string",
                                "format": "date-time"
                              }
                            }
                          },
                          "type": {
                            "type": "string",
                            "enum": [
                              "text"
                            ]
                          },
                          "text": {
                            "type": "string",
                            "maxLength": 2000
                          }
                        },
                        "required": [
                          "id",
                          "type",
                          "text"
                        ]
                      },
                      {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "minLength": 1,
                            "maxLength": 100
                          },
                          "visible": {
                            "type": "boolean",
                            "default": true
                          },
                          "schedule": {
                            "type": "object",
                            "properties": {
                              "from": {
                                "type": "string",
                                "format": "date-time"
                              },
                              "to": {
                                "type": "string",
                                "format": "date-time"
                              }
                            }
                          },
                          "type": {
                            "type": "string",
                            "enum": [
                              "image"
                            ]
                          },
                          "url": {
                            "anyOf": [
                              {
                                "type": "string",
                                "maxLength": 2048,
                                "format": "uri"
                              },
                              {
                                "type": "string",
                                "enum": [
                                  ""
                                ]
                              }
                            ]
                          },
                          "alt": {
                            "type": "string",
                            "maxLength": 300
                          },
                          "link": {
                            "type": "string",
                            "maxLength": 2048,
                            "format": "uri"
                          }
                        },
                        "required": [
                          "id",
                          "type",
                          "url",
                          "alt"
                        ]
                      },
                      {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "minLength": 1,
                            "maxLength": 100
                          },
                          "visible": {
                            "type": "boolean",
                            "default": true
                          },
                          "schedule": {
                            "type": "object",
                            "properties": {
                              "from": {
                                "type": "string",
                                "format": "date-time"
                              },
                              "to": {
                                "type": "string",
                                "format": "date-time"
                              }
                            }
                          },
                          "type": {
                            "type": "string",
                            "enum": [
                              "embed"
                            ]
                          },
                          "provider": {
                            "type": "string",
                            "enum": [
                              "youtube",
                              "spotify",
                              "soundcloud",
                              "vimeo",
                              "apple-music"
                            ]
                          },
                          "url": {
                            "anyOf": [
                              {
                                "type": "string",
                                "maxLength": 2048,
                                "format": "uri"
                              },
                              {
                                "type": "string",
                                "enum": [
                                  ""
                                ]
                              }
                            ]
                          }
                        },
                        "required": [
                          "id",
                          "type",
                          "provider",
                          "url"
                        ]
                      },
                      {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "minLength": 1,
                            "maxLength": 100
                          },
                          "visible": {
                            "type": "boolean",
                            "default": true
                          },
                          "schedule": {
                            "type": "object",
                            "properties": {
                              "from": {
                                "type": "string",
                                "format": "date-time"
                              },
                              "to": {
                                "type": "string",
                                "format": "date-time"
                              }
                            }
                          },
                          "type": {
                            "type": "string",
                            "enum": [
                              "qr"
                            ]
                          },
                          "linkId": {
                            "type": "string",
                            "pattern": "^[a-f\\d]{24}$/i"
                          },
                          "title": {
                            "type": "string",
                            "maxLength": 120,
                            "default": ""
                          }
                        },
                        "required": [
                          "id",
                          "type",
                          "linkId"
                        ]
                      }
                    ]
                  },
                  "maxItems": 150
                },
                "theme": {
                  "type": "object",
                  "properties": {
                    "background": {
                      "type": "string",
                      "pattern": "^#[a-f\\d]{6}$/i"
                    },
                    "surface": {
                      "type": "string",
                      "pattern": "^#[a-f\\d]{6}$/i"
                    },
                    "text": {
                      "type": "string",
                      "pattern": "^#[a-f\\d]{6}$/i"
                    },
                    "accent": {
                      "type": "string",
                      "pattern": "^#[a-f\\d]{6}$/i"
                    },
                    "buttonStyle": {
                      "type": "string",
                      "enum": [
                        "fill",
                        "outline",
                        "soft",
                        "shadow-less"
                      ]
                    },
                    "radius": {
                      "anyOf": [
                        {
                          "type": "number",
                          "enum": [
                            0
                          ]
                        },
                        {
                          "type": "number",
                          "enum": [
                            6
                          ]
                        },
                        {
                          "type": "number",
                          "enum": [
                            12
                          ]
                        },
                        {
                          "type": "number",
                          "enum": [
                            999
                          ]
                        }
                      ]
                    },
                    "font": {
                      "type": "string",
                      "enum": [
                        "Geist",
                        "Inter",
                        "Roboto",
                        "Open Sans",
                        "Lato",
                        "Montserrat",
                        "Poppins",
                        "DM Sans",
                        "Space Grotesk",
                        "Nunito",
                        "Raleway",
                        "Playfair Display",
                        "Merriweather",
                        "Lora",
                        "Oswald",
                        "Source Serif 4"
                      ]
                    },
                    "density": {
                      "type": "string",
                      "enum": [
                        "comfortable",
                        "compact"
                      ]
                    },
                    "backgroundImage": {
                      "type": "string",
                      "maxLength": 2048,
                      "format": "uri"
                    },
                    "legacyFont": {
                      "type": "string",
                      "pattern": "^[a-zA-Z][a-zA-Z0-9 -]{0,63}$"
                    }
                  },
                  "additionalProperties": false
                },
                "seo": {
                  "type": "object",
                  "properties": {
                    "favicon": {
                      "type": "string",
                      "maxLength": 2048,
                      "format": "uri"
                    },
                    "title": {
                      "type": "string",
                      "maxLength": 120
                    },
                    "description": {
                      "type": "string",
                      "maxLength": 500
                    },
                    "noindex": {
                      "type": "boolean"
                    }
                  },
                  "additionalProperties": false
                },
                "hideBranding": {
                  "type": "boolean"
                }
              },
              "additionalProperties": false
            }
          },
          "required": [
            "revision",
            "content"
          ],
          "additionalProperties": false
        }
      }
    }
  },
  "responses": {
    "200": {
      "description": "Updated draft revision; blocks replace the ordered list, theme and SEO merge provided fields",
      "content": {
        "application/json": {
          "schema": {
            "type": "object",
            "properties": {
              "revision": {
                "type": "integer",
                "minimum": 0
              }
            },
            "required": [
              "revision"
            ]
          }
        }
      }
    },
    "400": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "401": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "403": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "404": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "409": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "413": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "429": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "500": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    }
  }
}
```

## POST /v1/bio-pages/{id}/publish


```json
{
  "security": [
    {
      "ApiKey": []
    }
  ],
  "parameters": [
    {
      "schema": {
        "type": "string",
        "pattern": "^[a-f\\d]{24}$/i"
      },
      "required": true,
      "name": "id",
      "in": "path"
    }
  ],
  "requestBody": {
    "required": true,
    "content": {
      "application/json": {
        "schema": {
          "type": "object",
          "properties": {
            "revision": {
              "type": "integer",
              "minimum": 0
            }
          },
          "required": [
            "revision"
          ],
          "additionalProperties": false
        }
      }
    }
  },
  "responses": {
    "200": {
      "description": "Published validated revision; durable invalidation job queued transactionally",
      "content": {
        "application/json": {
          "schema": {
            "type": "object",
            "properties": {
              "revision": {
                "type": "integer",
                "minimum": 0
              }
            },
            "required": [
              "revision"
            ]
          }
        }
      }
    },
    "400": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "401": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "403": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "404": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "409": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "413": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "429": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "500": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    }
  }
}
```

## GET /v1/webhooks


```json
{
  "security": [
    {
      "ApiKey": []
    }
  ],
  "parameters": [
    {
      "schema": {
        "type": "string",
        "pattern": "^[a-f\\d]{24}$/i"
      },
      "required": false,
      "name": "cursor",
      "in": "query"
    },
    {
      "schema": {
        "type": "integer",
        "minimum": 1,
        "maximum": 200,
        "default": 50
      },
      "required": false,
      "name": "limit",
      "in": "query"
    },
    {
      "schema": {
        "type": "string",
        "maxLength": 200
      },
      "required": false,
      "name": "search",
      "in": "query"
    }
  ],
  "responses": {
    "200": {
      "description": "Paginated webhook metadata; signing secrets omitted",
      "content": {
        "application/json": {
          "schema": {
            "type": "object",
            "properties": {
              "items": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Webhook"
                }
              },
              "nextCursor": {
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "required": [
              "items",
              "nextCursor"
            ]
          }
        }
      }
    },
    "400": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "401": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "403": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "404": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "409": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "413": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "429": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "500": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    }
  }
}
```

## POST /v1/webhooks


```json
{
  "security": [
    {
      "ApiKey": []
    }
  ],
  "requestBody": {
    "required": true,
    "content": {
      "application/json": {
        "schema": {
          "type": "object",
          "properties": {
            "name": {
              "type": "string",
              "minLength": 1,
              "maxLength": 100
            },
            "url": {
              "type": "string",
              "maxLength": 2048,
              "format": "uri"
            },
            "events": {
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "link.created",
                  "link.updated",
                  "link.deleted",
                  "link.clicked",
                  "qr.scanned",
                  "bio.published",
                  "domain.verified",
                  "import.completed"
                ]
              },
              "minItems": 1,
              "maxItems": 8
            },
            "enabled": {
              "type": "boolean",
              "default": true
            }
          },
          "required": [
            "name",
            "url",
            "events"
          ],
          "additionalProperties": false
        }
      }
    }
  },
  "responses": {
    "201": {
      "description": "Endpoint created with signing secret; retain it securely",
      "content": {
        "application/json": {
          "schema": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Webhook"
              },
              {
                "type": "object",
                "properties": {
                  "secret": {
                    "type": "string"
                  }
                },
                "required": [
                  "secret"
                ]
              }
            ]
          }
        }
      }
    },
    "400": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "401": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "403": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "404": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "409": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "413": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "429": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "500": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    }
  }
}
```

## GET /v1/webhooks/{id}


```json
{
  "security": [
    {
      "ApiKey": []
    }
  ],
  "parameters": [
    {
      "schema": {
        "type": "string",
        "pattern": "^[a-f\\d]{24}$/i"
      },
      "required": true,
      "name": "id",
      "in": "path"
    }
  ],
  "responses": {
    "200": {
      "description": "Webhook metadata",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/Webhook"
          }
        }
      }
    },
    "400": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "401": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "403": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "404": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "409": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "413": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "429": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "500": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    }
  }
}
```

## PATCH /v1/webhooks/{id}


```json
{
  "security": [
    {
      "ApiKey": []
    }
  ],
  "parameters": [
    {
      "schema": {
        "type": "string",
        "pattern": "^[a-f\\d]{24}$/i"
      },
      "required": true,
      "name": "id",
      "in": "path"
    }
  ],
  "requestBody": {
    "required": true,
    "content": {
      "application/json": {
        "schema": {
          "type": "object",
          "properties": {
            "name": {
              "type": "string",
              "minLength": 1,
              "maxLength": 100
            },
            "url": {
              "type": "string",
              "maxLength": 2048,
              "format": "uri"
            },
            "events": {
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "link.created",
                  "link.updated",
                  "link.deleted",
                  "link.clicked",
                  "qr.scanned",
                  "bio.published",
                  "domain.verified",
                  "import.completed"
                ]
              },
              "minItems": 1,
              "maxItems": 8
            },
            "enabled": {
              "type": "boolean"
            }
          },
          "additionalProperties": false
        }
      }
    }
  },
  "responses": {
    "200": {
      "description": "Updated webhook",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/Webhook"
          }
        }
      }
    },
    "400": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "401": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "403": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "404": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "409": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "413": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "429": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "500": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    }
  }
}
```

## DELETE /v1/webhooks/{id}


```json
{
  "security": [
    {
      "ApiKey": []
    }
  ],
  "parameters": [
    {
      "schema": {
        "type": "string",
        "pattern": "^[a-f\\d]{24}$/i"
      },
      "required": true,
      "name": "id",
      "in": "path"
    }
  ],
  "responses": {
    "204": {
      "description": "Endpoint removed; queued deliveries cancel before sending"
    },
    "400": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "401": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "403": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "404": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "409": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "413": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "429": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "500": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    }
  }
}
```

## POST /v1/webhooks/{id}/test


```json
{
  "security": [
    {
      "ApiKey": []
    }
  ],
  "parameters": [
    {
      "schema": {
        "type": "string",
        "pattern": "^[a-f\\d]{24}$/i"
      },
      "required": true,
      "name": "id",
      "in": "path"
    }
  ],
  "responses": {
    "202": {
      "description": "Test delivery persisted for the worker",
      "content": {
        "application/json": {
          "schema": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "eventId": {
                "type": "string"
              },
              "status": {
                "type": "string"
              }
            },
            "required": [
              "id",
              "eventId",
              "status"
            ]
          }
        }
      }
    },
    "400": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "401": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "403": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "404": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "409": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "413": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "429": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "500": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    }
  }
}
```

## GET /v1/webhooks/{id}/deliveries


```json
{
  "security": [
    {
      "ApiKey": []
    }
  ],
  "parameters": [
    {
      "schema": {
        "type": "string",
        "pattern": "^[a-f\\d]{24}$/i"
      },
      "required": true,
      "name": "id",
      "in": "path"
    },
    {
      "schema": {
        "type": "string",
        "pattern": "^[a-f\\d]{24}$/i"
      },
      "required": false,
      "name": "cursor",
      "in": "query"
    },
    {
      "schema": {
        "type": "integer",
        "minimum": 1,
        "maximum": 200,
        "default": 50
      },
      "required": false,
      "name": "limit",
      "in": "query"
    }
  ],
  "responses": {
    "200": {
      "description": "30-day paginated delivery log",
      "content": {
        "application/json": {
          "schema": {
            "type": "object",
            "properties": {
              "items": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "eventId": {
                      "type": "string"
                    },
                    "type": {
                      "type": "string",
                      "enum": [
                        "link.created",
                        "link.updated",
                        "link.deleted",
                        "link.clicked",
                        "qr.scanned",
                        "bio.published",
                        "domain.verified",
                        "import.completed",
                        "webhook.test"
                      ]
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "pending",
                        "sending",
                        "delivered",
                        "failed",
                        "cancelled"
                      ]
                    },
                    "attempts": {
                      "type": "number"
                    },
                    "createdAt": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "deliveredAt": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    },
                    "lastStatus": {
                      "type": [
                        "number",
                        "null"
                      ]
                    },
                    "lastError": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  },
                  "required": [
                    "id",
                    "eventId",
                    "type",
                    "status",
                    "attempts",
                    "createdAt",
                    "deliveredAt",
                    "lastStatus",
                    "lastError"
                  ]
                }
              },
              "nextCursor": {
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "required": [
              "items",
              "nextCursor"
            ]
          }
        }
      }
    },
    "400": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "401": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "403": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "404": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "409": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "413": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "429": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "500": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    }
  }
}
```

## POST /v1/webhooks/{id}/deliveries/{deliveryId}/redeliver


```json
{
  "security": [
    {
      "ApiKey": []
    }
  ],
  "parameters": [
    {
      "schema": {
        "type": "string",
        "pattern": "^[a-f\\d]{24}$/i"
      },
      "required": true,
      "name": "id",
      "in": "path"
    },
    {
      "schema": {
        "type": "string",
        "pattern": "^[a-f\\d]{24}$/i"
      },
      "required": true,
      "name": "deliveryId",
      "in": "path"
    }
  ],
  "responses": {
    "202": {
      "description": "New delivery queued with the original event ID",
      "content": {
        "application/json": {
          "schema": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "eventId": {
                "type": "string"
              },
              "status": {
                "type": "string"
              }
            },
            "required": [
              "id",
              "eventId",
              "status"
            ]
          }
        }
      }
    },
    "400": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "401": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "403": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "404": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "409": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "413": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "429": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    },
    "500": {
      "description": "Request failed; code is stable across API versions",
      "content": {
        "application/problem+json": {
          "schema": {
            "$ref": "#/components/schemas/Problem"
          }
        }
      }
    }
  }
}
```

## Shared schemas and authentication

```json
{
  "securitySchemes": {
    "ApiKey": {
      "type": "http",
      "scheme": "bearer",
      "bearerFormat": "sk_live_…"
    }
  },
  "schemas": {
    "Link": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string"
        },
        "domain": {
          "type": "string"
        },
        "key": {
          "type": "string"
        },
        "url": {
          "type": "string"
        },
        "title": {
          "type": "string"
        },
        "description": {
          "type": [
            "string",
            "null"
          ]
        },
        "tags": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              }
            },
            "required": [
              "id",
              "name"
            ]
          }
        },
        "clicks": {
          "type": "number"
        },
        "lastClickAt": {
          "type": [
            "string",
            "null"
          ]
        },
        "createdAt": {
          "type": "string"
        },
        "createdBy": {
          "type": [
            "string",
            "null"
          ]
        },
        "password": {
          "type": [
            "object",
            "null"
          ],
          "properties": {
            "hint": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          "required": [
            "hint"
          ]
        },
        "safety": {
          "type": "object",
          "properties": {
            "status": {
              "type": "string"
            },
            "disabled": {
              "type": "boolean"
            },
            "interstitial": {
              "type": "boolean"
            }
          },
          "required": [
            "status",
            "disabled",
            "interstitial"
          ]
        },
        "health": {
          "type": "object",
          "properties": {
            "flagged": {
              "type": "boolean"
            },
            "checkedAt": {
              "type": [
                "string",
                "null"
              ],
              "format": "date-time"
            },
            "nextCheckAt": {
              "type": "string",
              "format": "date-time"
            },
            "checks": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string"
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "ok",
                      "http_error",
                      "unreachable",
                      "blocked"
                    ]
                  },
                  "httpStatus": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "minimum": 100,
                    "maximum": 599
                  },
                  "checkedAt": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "streak": {
                    "type": "integer",
                    "minimum": 0
                  },
                  "failingSince": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "date-time"
                  },
                  "error": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                },
                "required": [
                  "url",
                  "status",
                  "httpStatus",
                  "checkedAt",
                  "streak",
                  "failingSince",
                  "error"
                ]
              }
            }
          },
          "required": [
            "flagged",
            "checkedAt",
            "nextCheckAt",
            "checks"
          ]
        },
        "rules": {
          "type": "object",
          "properties": {
            "activeFrom": {
              "type": "string",
              "format": "date-time"
            },
            "expiresAt": {
              "type": "string",
              "format": "date-time"
            },
            "deepLink": {
              "type": "object",
              "properties": {
                "ios": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 2048
                },
                "android": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 2048
                },
                "fallback": {
                  "type": "string",
                  "enum": [
                    "web",
                    "store"
                  ]
                },
                "store": {
                  "type": "object",
                  "properties": {
                    "ios": {
                      "type": "string",
                      "maxLength": 2048,
                      "format": "uri"
                    },
                    "android": {
                      "type": "string",
                      "maxLength": 2048,
                      "format": "uri"
                    }
                  },
                  "additionalProperties": false
                }
              },
              "required": [
                "fallback"
              ],
              "additionalProperties": false
            },
            "maxClicks": {
              "type": "integer",
              "minimum": 0,
              "maximum": 9007199254740991,
              "description": "Approximate lifetime non-bot destination visit limit, including scans. Concurrent and queued visits may overshoot."
            },
            "rotation": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string",
                    "maxLength": 2048,
                    "format": "uri"
                  },
                  "weight": {
                    "type": "number",
                    "exclusiveMinimum": 0,
                    "maximum": 1000000
                  }
                },
                "required": [
                  "url",
                  "weight"
                ],
                "additionalProperties": false
              },
              "minItems": 1,
              "maxItems": 10
            },
            "geo": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "country": {
                    "type": "string",
                    "pattern": "^[A-Z]{2}$"
                  },
                  "url": {
                    "type": "string",
                    "maxLength": 2048,
                    "format": "uri"
                  }
                },
                "required": [
                  "country",
                  "url"
                ],
                "additionalProperties": false
              },
              "maxItems": 10
            },
            "device": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "os": {
                    "type": "string",
                    "enum": [
                      "ios",
                      "android",
                      "windows",
                      "macos"
                    ]
                  },
                  "device": {
                    "type": "string",
                    "enum": [
                      "mobile",
                      "tablet",
                      "desktop"
                    ]
                  },
                  "url": {
                    "type": "string",
                    "maxLength": 2048,
                    "format": "uri"
                  }
                },
                "required": [
                  "url"
                ],
                "additionalProperties": false
              },
              "maxItems": 10
            },
            "expiredUrl": {
              "type": "string",
              "maxLength": 2048,
              "format": "uri"
            },
            "forwardQuery": {
              "type": "boolean"
            },
            "utm": {
              "type": "object",
              "properties": {
                "source": {
                  "type": "string",
                  "maxLength": 200
                },
                "medium": {
                  "type": "string",
                  "maxLength": 200
                },
                "campaign": {
                  "type": "string",
                  "maxLength": 200
                },
                "term": {
                  "type": "string",
                  "maxLength": 200
                },
                "content": {
                  "type": "string",
                  "maxLength": 200
                }
              },
              "additionalProperties": false
            },
            "permanent": {
              "type": "boolean"
            }
          },
          "additionalProperties": false
        },
        "og": {
          "type": "object",
          "properties": {
            "title": {
              "type": "string",
              "minLength": 1,
              "maxLength": 200
            },
            "description": {
              "type": "string",
              "minLength": 1,
              "maxLength": 500
            },
            "image": {
              "type": "string",
              "maxLength": 2048,
              "format": "uri"
            }
          },
          "additionalProperties": false
        },
        "expanded": {
          "type": "object",
          "properties": {
            "tags": {
              "type": "array",
              "items": {
                "$ref": "#/components/schemas/Tag"
              }
            },
            "campaign": {
              "type": "array",
              "items": {
                "$ref": "#/components/schemas/Campaign"
              }
            }
          }
        },
        "qrCodeId": {
          "type": [
            "string",
            "null"
          ]
        },
        "deletedAt": {
          "type": [
            "string",
            "null"
          ]
        },
        "restoreUntil": {
          "type": [
            "string",
            "null"
          ]
        },
        "utm": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "source": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "medium": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "campaign": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "term": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "content": {
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "required": [
              "source",
              "medium",
              "campaign",
              "term",
              "content"
            ]
          }
        }
      },
      "required": [
        "id",
        "domain",
        "key",
        "url",
        "title",
        "description",
        "tags",
        "clicks",
        "lastClickAt",
        "createdAt",
        "createdBy",
        "password",
        "safety",
        "qrCodeId",
        "utm"
      ]
    },
    "Tag": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string"
        },
        "legacyId": {
          "type": [
            "string",
            "null"
          ]
        },
        "name": {
          "type": "string"
        },
        "createdAt": {
          "type": "string"
        },
        "createdBy": {
          "type": [
            "string",
            "null"
          ]
        }
      },
      "required": [
        "id",
        "legacyId",
        "name",
        "createdAt",
        "createdBy"
      ]
    },
    "Campaign": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string"
        },
        "title": {
          "type": "string"
        },
        "description": {
          "type": [
            "string",
            "null"
          ]
        },
        "linkCount": {
          "type": "number"
        },
        "utmDefaults": {
          "type": "object",
          "properties": {
            "sources": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "mediums": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "terms": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "contents": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          "required": [
            "sources",
            "mediums",
            "terms",
            "contents"
          ]
        },
        "createdAt": {
          "type": "string"
        },
        "createdBy": {
          "type": [
            "string",
            "null"
          ]
        }
      },
      "required": [
        "id",
        "title",
        "description",
        "linkCount",
        "utmDefaults",
        "createdAt",
        "createdBy"
      ]
    },
    "Problem": {
      "type": "object",
      "properties": {
        "type": {
          "type": "string"
        },
        "title": {
          "type": "string"
        },
        "status": {
          "type": "number"
        },
        "code": {
          "type": "string"
        },
        "instance": {
          "type": "string"
        },
        "context": {
          "type": "object",
          "additionalProperties": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "required": [
        "type",
        "title",
        "status",
        "code",
        "instance"
      ]
    },
    "CreateLinksBulk": {
      "type": "object",
      "properties": {
        "links": {
          "type": "array",
          "items": {
            "$ref": "#/components/schemas/CreateLink"
          },
          "minItems": 1,
          "maxItems": 1000
        }
      },
      "required": [
        "links"
      ],
      "additionalProperties": false
    },
    "CreateLink": {
      "type": "object",
      "properties": {
        "url": {
          "type": "string",
          "maxLength": 8192
        },
        "rules": {
          "type": "object",
          "properties": {
            "activeFrom": {
              "type": "string",
              "format": "date-time"
            },
            "expiresAt": {
              "type": "string",
              "format": "date-time"
            },
            "deepLink": {
              "type": "object",
              "properties": {
                "ios": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 2048
                },
                "android": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 2048
                },
                "fallback": {
                  "type": "string",
                  "enum": [
                    "web",
                    "store"
                  ]
                },
                "store": {
                  "type": "object",
                  "properties": {
                    "ios": {
                      "type": "string",
                      "maxLength": 2048,
                      "format": "uri"
                    },
                    "android": {
                      "type": "string",
                      "maxLength": 2048,
                      "format": "uri"
                    }
                  },
                  "additionalProperties": false
                }
              },
              "required": [
                "fallback"
              ],
              "additionalProperties": false
            },
            "maxClicks": {
              "type": "integer",
              "minimum": 0,
              "maximum": 9007199254740991,
              "description": "Approximate lifetime non-bot destination visit limit, including scans. Concurrent and queued visits may overshoot."
            },
            "rotation": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string",
                    "maxLength": 2048,
                    "format": "uri"
                  },
                  "weight": {
                    "type": "number",
                    "exclusiveMinimum": 0,
                    "maximum": 1000000
                  }
                },
                "required": [
                  "url",
                  "weight"
                ],
                "additionalProperties": false
              },
              "minItems": 1,
              "maxItems": 10
            },
            "geo": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "country": {
                    "type": "string",
                    "pattern": "^[A-Z]{2}$"
                  },
                  "url": {
                    "type": "string",
                    "maxLength": 2048,
                    "format": "uri"
                  }
                },
                "required": [
                  "country",
                  "url"
                ],
                "additionalProperties": false
              },
              "maxItems": 10
            },
            "device": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "os": {
                    "type": "string",
                    "enum": [
                      "ios",
                      "android",
                      "windows",
                      "macos"
                    ]
                  },
                  "device": {
                    "type": "string",
                    "enum": [
                      "mobile",
                      "tablet",
                      "desktop"
                    ]
                  },
                  "url": {
                    "type": "string",
                    "maxLength": 2048,
                    "format": "uri"
                  }
                },
                "required": [
                  "url"
                ],
                "additionalProperties": false
              },
              "maxItems": 10
            },
            "expiredUrl": {
              "type": "string",
              "maxLength": 2048,
              "format": "uri"
            },
            "forwardQuery": {
              "type": "boolean"
            },
            "utm": {
              "type": "object",
              "properties": {
                "source": {
                  "type": "string",
                  "maxLength": 200
                },
                "medium": {
                  "type": "string",
                  "maxLength": 200
                },
                "campaign": {
                  "type": "string",
                  "maxLength": 200
                },
                "term": {
                  "type": "string",
                  "maxLength": 200
                },
                "content": {
                  "type": "string",
                  "maxLength": 200
                }
              },
              "additionalProperties": false
            },
            "permanent": {
              "type": "boolean"
            }
          },
          "additionalProperties": false
        },
        "og": {
          "type": "object",
          "properties": {
            "title": {
              "type": "string",
              "minLength": 1,
              "maxLength": 200
            },
            "description": {
              "type": "string",
              "minLength": 1,
              "maxLength": 500
            },
            "image": {
              "type": "string",
              "maxLength": 2048,
              "format": "uri"
            }
          },
          "additionalProperties": false
        },
        "domain": {
          "type": "string",
          "maxLength": 253
        },
        "key": {
          "type": "string",
          "maxLength": 100
        },
        "title": {
          "type": "string",
          "maxLength": 500
        },
        "description": {
          "type": "string",
          "maxLength": 5000
        },
        "tags": {
          "type": "array",
          "items": {
            "type": "string",
            "maxLength": 100
          },
          "maxItems": 100
        },
        "password": {
          "type": "string",
          "maxLength": 200
        },
        "passwordHint": {
          "type": "string",
          "maxLength": 100
        }
      },
      "required": [
        "url"
      ],
      "additionalProperties": false
    },
    "UpdateLink": {
      "type": "object",
      "properties": {
        "url": {
          "type": "string",
          "maxLength": 8192
        },
        "rules": {
          "type": [
            "object",
            "null"
          ],
          "properties": {
            "activeFrom": {
              "type": "string",
              "format": "date-time"
            },
            "expiresAt": {
              "type": "string",
              "format": "date-time"
            },
            "deepLink": {
              "type": "object",
              "properties": {
                "ios": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 2048
                },
                "android": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 2048
                },
                "fallback": {
                  "type": "string",
                  "enum": [
                    "web",
                    "store"
                  ]
                },
                "store": {
                  "type": "object",
                  "properties": {
                    "ios": {
                      "type": "string",
                      "maxLength": 2048,
                      "format": "uri"
                    },
                    "android": {
                      "type": "string",
                      "maxLength": 2048,
                      "format": "uri"
                    }
                  },
                  "additionalProperties": false
                }
              },
              "required": [
                "fallback"
              ],
              "additionalProperties": false
            },
            "maxClicks": {
              "type": "integer",
              "minimum": 0,
              "maximum": 9007199254740991,
              "description": "Approximate lifetime non-bot destination visit limit, including scans. Concurrent and queued visits may overshoot."
            },
            "rotation": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string",
                    "maxLength": 2048,
                    "format": "uri"
                  },
                  "weight": {
                    "type": "number",
                    "exclusiveMinimum": 0,
                    "maximum": 1000000
                  }
                },
                "required": [
                  "url",
                  "weight"
                ],
                "additionalProperties": false
              },
              "minItems": 1,
              "maxItems": 10
            },
            "geo": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "country": {
                    "type": "string",
                    "pattern": "^[A-Z]{2}$"
                  },
                  "url": {
                    "type": "string",
                    "maxLength": 2048,
                    "format": "uri"
                  }
                },
                "required": [
                  "country",
                  "url"
                ],
                "additionalProperties": false
              },
              "maxItems": 10
            },
            "device": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "os": {
                    "type": "string",
                    "enum": [
                      "ios",
                      "android",
                      "windows",
                      "macos"
                    ]
                  },
                  "device": {
                    "type": "string",
                    "enum": [
                      "mobile",
                      "tablet",
                      "desktop"
                    ]
                  },
                  "url": {
                    "type": "string",
                    "maxLength": 2048,
                    "format": "uri"
                  }
                },
                "required": [
                  "url"
                ],
                "additionalProperties": false
              },
              "maxItems": 10
            },
            "expiredUrl": {
              "type": "string",
              "maxLength": 2048,
              "format": "uri"
            },
            "forwardQuery": {
              "type": "boolean"
            },
            "utm": {
              "type": "object",
              "properties": {
                "source": {
                  "type": "string",
                  "maxLength": 200
                },
                "medium": {
                  "type": "string",
                  "maxLength": 200
                },
                "campaign": {
                  "type": "string",
                  "maxLength": 200
                },
                "term": {
                  "type": "string",
                  "maxLength": 200
                },
                "content": {
                  "type": "string",
                  "maxLength": 200
                }
              },
              "additionalProperties": false
            },
            "permanent": {
              "type": "boolean"
            }
          },
          "additionalProperties": false
        },
        "og": {
          "type": [
            "object",
            "null"
          ],
          "properties": {
            "title": {
              "type": "string",
              "minLength": 1,
              "maxLength": 200
            },
            "description": {
              "type": "string",
              "minLength": 1,
              "maxLength": 500
            },
            "image": {
              "type": "string",
              "maxLength": 2048,
              "format": "uri"
            }
          },
          "additionalProperties": false
        },
        "key": {
          "type": "string",
          "maxLength": 100
        },
        "title": {
          "type": "string",
          "maxLength": 500
        },
        "description": {
          "type": [
            "string",
            "null"
          ],
          "maxLength": 5000
        },
        "tags": {
          "type": "array",
          "items": {
            "type": "string",
            "maxLength": 100
          },
          "maxItems": 100
        },
        "password": {
          "type": "string",
          "maxLength": 200
        },
        "passwordHint": {
          "type": [
            "string",
            "null"
          ],
          "maxLength": 100
        },
        "removePassword": {
          "type": "boolean"
        }
      },
      "additionalProperties": false
    },
    "Workspace": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string"
        },
        "name": {
          "type": "string"
        },
        "slug": {
          "type": "string"
        },
        "personal": {
          "type": "boolean"
        },
        "defaultDomain": {
          "type": "string"
        },
        "plan": {
          "type": "string",
          "enum": [
            "free",
            "basic",
            "plus",
            "pro",
            "enterprise"
          ]
        },
        "role": {
          "type": "string",
          "enum": [
            "owner",
            "admin",
            "member",
            "viewer"
          ]
        },
        "createdAt": {
          "type": "string"
        }
      },
      "required": [
        "id",
        "name",
        "slug",
        "personal",
        "defaultDomain",
        "plan",
        "role",
        "createdAt"
      ]
    },
    "WorkspaceUsage": {
      "type": "object",
      "properties": {
        "period": {
          "type": "object",
          "properties": {
            "start": {
              "type": "string"
            },
            "end": {
              "type": "string"
            }
          },
          "required": [
            "start",
            "end"
          ]
        },
        "links": {
          "type": "object",
          "properties": {
            "consumed": {
              "type": "integer",
              "minimum": 0
            },
            "limit": {
              "type": [
                "number",
                "null"
              ]
            },
            "remaining": {
              "type": [
                "number",
                "null"
              ]
            }
          },
          "required": [
            "consumed",
            "limit",
            "remaining"
          ]
        },
        "qrCodes": {
          "type": "object",
          "properties": {
            "consumed": {
              "type": "integer",
              "minimum": 0
            },
            "limit": {
              "type": [
                "number",
                "null"
              ]
            },
            "remaining": {
              "type": [
                "number",
                "null"
              ]
            }
          },
          "required": [
            "consumed",
            "limit",
            "remaining"
          ]
        },
        "bioPages": {
          "type": "object",
          "properties": {
            "consumed": {
              "type": "integer",
              "minimum": 0
            },
            "limit": {
              "type": [
                "number",
                "null"
              ]
            },
            "remaining": {
              "type": [
                "number",
                "null"
              ]
            }
          },
          "required": [
            "consumed",
            "limit",
            "remaining"
          ]
        }
      },
      "required": [
        "period",
        "links",
        "qrCodes",
        "bioPages"
      ]
    },
    "CreateCampaign": {
      "type": "object",
      "properties": {
        "title": {
          "type": "string",
          "minLength": 1,
          "maxLength": 80
        },
        "description": {
          "type": "string",
          "maxLength": 5000
        }
      },
      "required": [
        "title"
      ],
      "additionalProperties": false
    },
    "UpdateCampaign": {
      "type": "object",
      "properties": {
        "title": {
          "type": "string",
          "minLength": 1,
          "maxLength": 80
        },
        "description": {
          "type": [
            "string",
            "null"
          ],
          "maxLength": 5000
        },
        "utmDefaults": {
          "type": "object",
          "properties": {
            "sources": {
              "type": "array",
              "items": {
                "type": "string",
                "maxLength": 200
              },
              "maxItems": 100
            },
            "mediums": {
              "type": "array",
              "items": {
                "type": "string",
                "maxLength": 200
              },
              "maxItems": 100
            },
            "terms": {
              "type": "array",
              "items": {
                "type": "string",
                "maxLength": 200
              },
              "maxItems": 100
            },
            "contents": {
              "type": "array",
              "items": {
                "type": "string",
                "maxLength": 200
              },
              "maxItems": 100
            }
          },
          "required": [
            "sources",
            "mediums",
            "terms",
            "contents"
          ],
          "additionalProperties": false
        }
      },
      "additionalProperties": false
    },
    "QrCode": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string"
        },
        "publicId": {
          "type": "string"
        },
        "key": {
          "type": "string"
        },
        "shortUrl": {
          "type": "string"
        },
        "url": {
          "type": "string"
        },
        "title": {
          "type": "string"
        },
        "tags": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              }
            },
            "required": [
              "id",
              "name"
            ]
          }
        },
        "scans": {
          "type": "number"
        },
        "lastScanAt": {
          "type": [
            "string",
            "null"
          ]
        },
        "design": {
          "type": "object",
          "properties": {
            "width": {
              "type": "integer",
              "minimum": 64,
              "maximum": 4096
            },
            "height": {
              "type": "integer",
              "minimum": 64,
              "maximum": 4096
            },
            "type": {
              "type": "string",
              "enum": [
                "svg",
                "canvas"
              ]
            },
            "shape": {
              "type": "string",
              "enum": [
                "square",
                "circle"
              ]
            },
            "frameOptions": {
              "type": "object",
              "properties": {
                "style": {
                  "type": "string",
                  "enum": [
                    "none",
                    "outline",
                    "solid"
                  ]
                },
                "caption": {
                  "type": "string",
                  "maxLength": 64
                }
              }
            },
            "data": {
              "type": "string",
              "maxLength": 2048
            },
            "image": {
              "type": "string",
              "maxLength": 2048
            },
            "margin": {
              "type": "integer",
              "minimum": 0,
              "maximum": 100
            },
            "dotsOptions": {
              "type": "object",
              "properties": {
                "color": {
                  "type": "string",
                  "maxLength": 32
                },
                "type": {
                  "type": "string",
                  "maxLength": 32
                }
              }
            },
            "backgroundOptions": {
              "type": "object",
              "properties": {
                "color": {
                  "type": "string",
                  "maxLength": 32
                }
              }
            },
            "cornersSquareOptions": {
              "type": "object",
              "properties": {
                "color": {
                  "type": "string",
                  "maxLength": 32
                },
                "type": {
                  "type": "string",
                  "maxLength": 32
                }
              }
            },
            "cornersDotOptions": {
              "type": "object",
              "properties": {
                "color": {
                  "type": "string",
                  "maxLength": 32
                },
                "type": {
                  "type": "string",
                  "maxLength": 32
                }
              }
            },
            "imageOptions": {
              "type": "object",
              "properties": {
                "crossOrigin": {
                  "type": "string",
                  "maxLength": 32
                },
                "margin": {
                  "type": "integer",
                  "minimum": 0,
                  "maximum": 50
                },
                "hideBackgroundDots": {
                  "type": "boolean"
                },
                "imageSize": {
                  "type": "number",
                  "minimum": 0,
                  "maximum": 1
                }
              }
            },
            "qrOptions": {
              "type": "object",
              "properties": {
                "errorCorrectionLevel": {
                  "type": "string",
                  "enum": [
                    "L",
                    "M",
                    "Q",
                    "H"
                  ]
                }
              }
            }
          }
        },
        "storedDesign": {
          "type": "object",
          "additionalProperties": {}
        },
        "attachedLinkKey": {
          "type": [
            "string",
            "null"
          ]
        },
        "createdAt": {
          "type": "string"
        },
        "createdBy": {
          "type": [
            "string",
            "null"
          ]
        }
      },
      "required": [
        "id",
        "publicId",
        "key",
        "shortUrl",
        "url",
        "title",
        "tags",
        "scans",
        "lastScanAt",
        "design",
        "storedDesign",
        "attachedLinkKey",
        "createdAt",
        "createdBy"
      ]
    },
    "CreateQrCode": {
      "type": "object",
      "properties": {
        "url": {
          "type": "string",
          "maxLength": 8192
        },
        "title": {
          "type": "string",
          "maxLength": 500
        },
        "tags": {
          "type": "array",
          "items": {
            "type": "string",
            "maxLength": 48
          },
          "maxItems": 50
        },
        "design": {
          "type": "object",
          "properties": {
            "width": {
              "type": "integer",
              "minimum": 64,
              "maximum": 4096
            },
            "height": {
              "type": "integer",
              "minimum": 64,
              "maximum": 4096
            },
            "type": {
              "type": "string",
              "enum": [
                "svg",
                "canvas"
              ]
            },
            "shape": {
              "type": "string",
              "enum": [
                "square",
                "circle"
              ]
            },
            "frameOptions": {
              "type": "object",
              "properties": {
                "style": {
                  "type": "string",
                  "enum": [
                    "none",
                    "outline",
                    "solid"
                  ]
                },
                "caption": {
                  "type": "string",
                  "maxLength": 64
                }
              }
            },
            "data": {
              "type": "string",
              "maxLength": 2048
            },
            "image": {
              "type": "string",
              "maxLength": 2048
            },
            "margin": {
              "type": "integer",
              "minimum": 0,
              "maximum": 100
            },
            "dotsOptions": {
              "type": "object",
              "properties": {
                "color": {
                  "type": "string",
                  "maxLength": 32
                },
                "type": {
                  "type": "string",
                  "maxLength": 32
                }
              }
            },
            "backgroundOptions": {
              "type": "object",
              "properties": {
                "color": {
                  "type": "string",
                  "maxLength": 32
                }
              }
            },
            "cornersSquareOptions": {
              "type": "object",
              "properties": {
                "color": {
                  "type": "string",
                  "maxLength": 32
                },
                "type": {
                  "type": "string",
                  "maxLength": 32
                }
              }
            },
            "cornersDotOptions": {
              "type": "object",
              "properties": {
                "color": {
                  "type": "string",
                  "maxLength": 32
                },
                "type": {
                  "type": "string",
                  "maxLength": 32
                }
              }
            },
            "imageOptions": {
              "type": "object",
              "properties": {
                "crossOrigin": {
                  "type": "string",
                  "maxLength": 32
                },
                "margin": {
                  "type": "integer",
                  "minimum": 0,
                  "maximum": 50
                },
                "hideBackgroundDots": {
                  "type": "boolean"
                },
                "imageSize": {
                  "type": "number",
                  "minimum": 0,
                  "maximum": 1
                }
              }
            },
            "qrOptions": {
              "type": "object",
              "properties": {
                "errorCorrectionLevel": {
                  "type": "string",
                  "enum": [
                    "L",
                    "M",
                    "Q",
                    "H"
                  ]
                }
              }
            }
          }
        },
        "attachToLinkId": {
          "type": "string",
          "pattern": "^[a-f\\d]{24}$/i"
        }
      },
      "required": [
        "url"
      ],
      "additionalProperties": false
    },
    "QrZipExportJob": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string"
        },
        "state": {
          "type": "string",
          "enum": [
            "pending",
            "running",
            "completed",
            "failed"
          ]
        },
        "phase": {
          "type": "string",
          "enum": [
            "rendering",
            "assembling"
          ]
        },
        "itemFormat": {
          "type": "string",
          "enum": [
            "svg",
            "png",
            "pdf"
          ]
        },
        "size": {
          "type": "number"
        },
        "total": {
          "type": "number"
        },
        "done": {
          "type": "number"
        },
        "limit": {
          "type": "number"
        },
        "maximumBytes": {
          "type": "number"
        },
        "createdAt": {
          "type": "string",
          "format": "date-time"
        },
        "expiresAt": {
          "type": "string",
          "format": "date-time"
        },
        "error": {
          "type": "string"
        },
        "failedQrId": {
          "type": "string"
        },
        "downloadUrl": {
          "type": "string",
          "format": "uri"
        },
        "downloadExpiresAt": {
          "type": "string",
          "format": "date-time"
        }
      },
      "required": [
        "id",
        "state",
        "phase",
        "itemFormat",
        "size",
        "total",
        "done",
        "limit",
        "maximumBytes",
        "createdAt",
        "expiresAt"
      ]
    },
    "QrExportJob": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string"
        },
        "state": {
          "type": "string",
          "enum": [
            "pending",
            "running",
            "completed",
            "failed"
          ]
        },
        "format": {
          "type": "string",
          "enum": [
            "svg",
            "png",
            "pdf"
          ]
        },
        "size": {
          "type": "number"
        },
        "createdAt": {
          "type": "string",
          "format": "date-time"
        },
        "expiresAt": {
          "type": "string",
          "format": "date-time"
        },
        "error": {
          "type": "string"
        },
        "print": {
          "type": "object",
          "properties": {
            "minimumMm": {
              "type": "number"
            },
            "quietModules": {
              "type": "number"
            },
            "correction": {
              "type": "string"
            }
          },
          "required": [
            "minimumMm",
            "quietModules",
            "correction"
          ]
        },
        "downloadUrl": {
          "type": "string",
          "format": "uri"
        },
        "downloadExpiresAt": {
          "type": "string",
          "format": "date-time"
        }
      },
      "required": [
        "id",
        "state",
        "format",
        "size",
        "createdAt",
        "expiresAt"
      ]
    },
    "UpdateQrCode": {
      "type": "object",
      "properties": {
        "url": {
          "type": "string",
          "maxLength": 8192
        },
        "title": {
          "type": "string",
          "maxLength": 500
        },
        "tags": {
          "type": "array",
          "items": {
            "type": "string",
            "maxLength": 48
          },
          "maxItems": 50
        },
        "design": {
          "type": "object",
          "properties": {
            "width": {
              "type": "integer",
              "minimum": 64,
              "maximum": 4096
            },
            "height": {
              "type": "integer",
              "minimum": 64,
              "maximum": 4096
            },
            "type": {
              "type": "string",
              "enum": [
                "svg",
                "canvas"
              ]
            },
            "shape": {
              "type": "string",
              "enum": [
                "square",
                "circle"
              ]
            },
            "frameOptions": {
              "type": "object",
              "properties": {
                "style": {
                  "type": "string",
                  "enum": [
                    "none",
                    "outline",
                    "solid"
                  ]
                },
                "caption": {
                  "type": "string",
                  "maxLength": 64
                }
              }
            },
            "data": {
              "type": "string",
              "maxLength": 2048
            },
            "image": {
              "type": "string",
              "maxLength": 2048
            },
            "margin": {
              "type": "integer",
              "minimum": 0,
              "maximum": 100
            },
            "dotsOptions": {
              "type": "object",
              "properties": {
                "color": {
                  "type": "string",
                  "maxLength": 32
                },
                "type": {
                  "type": "string",
                  "maxLength": 32
                }
              }
            },
            "backgroundOptions": {
              "type": "object",
              "properties": {
                "color": {
                  "type": "string",
                  "maxLength": 32
                }
              }
            },
            "cornersSquareOptions": {
              "type": "object",
              "properties": {
                "color": {
                  "type": "string",
                  "maxLength": 32
                },
                "type": {
                  "type": "string",
                  "maxLength": 32
                }
              }
            },
            "cornersDotOptions": {
              "type": "object",
              "properties": {
                "color": {
                  "type": "string",
                  "maxLength": 32
                },
                "type": {
                  "type": "string",
                  "maxLength": 32
                }
              }
            },
            "imageOptions": {
              "type": "object",
              "properties": {
                "crossOrigin": {
                  "type": "string",
                  "maxLength": 32
                },
                "margin": {
                  "type": "integer",
                  "minimum": 0,
                  "maximum": 50
                },
                "hideBackgroundDots": {
                  "type": "boolean"
                },
                "imageSize": {
                  "type": "number",
                  "minimum": 0,
                  "maximum": 1
                }
              }
            },
            "qrOptions": {
              "type": "object",
              "properties": {
                "errorCorrectionLevel": {
                  "type": "string",
                  "enum": [
                    "L",
                    "M",
                    "Q",
                    "H"
                  ]
                }
              }
            }
          }
        },
        "applyToLink": {
          "type": "boolean"
        }
      },
      "additionalProperties": false
    },
    "WriteTag": {
      "type": "object",
      "properties": {
        "name": {
          "type": "string",
          "minLength": 1,
          "maxLength": 48
        }
      },
      "required": [
        "name"
      ],
      "additionalProperties": false
    },
    "Analytics": {
      "type": "object",
      "properties": {
        "range": {
          "type": "object",
          "properties": {
            "from": {
              "type": "string",
              "format": "date-time"
            },
            "to": {
              "type": "string",
              "format": "date-time"
            },
            "clampedFrom": {
              "type": [
                "string",
                "null"
              ],
              "format": "date-time"
            }
          },
          "required": [
            "from",
            "to",
            "clampedFrom"
          ]
        },
        "totals": {
          "type": "object",
          "properties": {
            "clicks": {
              "type": "number"
            },
            "scans": {
              "type": "number"
            },
            "uniques": {
              "type": "number"
            }
          },
          "required": [
            "clicks",
            "scans",
            "uniques"
          ]
        },
        "previous": {
          "type": "object",
          "properties": {
            "clicks": {
              "type": "number"
            },
            "scans": {
              "type": "number"
            },
            "uniques": {
              "type": "number"
            }
          },
          "required": [
            "clicks",
            "scans",
            "uniques"
          ]
        },
        "series": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "t": {
                "type": "string",
                "format": "date-time"
              },
              "clicks": {
                "type": "number"
              },
              "scans": {
                "type": "number"
              }
            },
            "required": [
              "t",
              "clicks",
              "scans"
            ]
          }
        },
        "breakdowns": {
          "type": "object",
          "properties": {
            "branch": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "value": {
                    "type": "string"
                  },
                  "clicks": {
                    "type": "number"
                  },
                  "scans": {
                    "type": "number"
                  }
                },
                "required": [
                  "value",
                  "clicks",
                  "scans"
                ]
              }
            },
            "country": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "value": {
                    "type": "string"
                  },
                  "clicks": {
                    "type": "number"
                  },
                  "scans": {
                    "type": "number"
                  }
                },
                "required": [
                  "value",
                  "clicks",
                  "scans"
                ]
              }
            },
            "city": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "value": {
                    "type": "string"
                  },
                  "clicks": {
                    "type": "number"
                  },
                  "scans": {
                    "type": "number"
                  }
                },
                "required": [
                  "value",
                  "clicks",
                  "scans"
                ]
              }
            },
            "continent": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "value": {
                    "type": "string"
                  },
                  "clicks": {
                    "type": "number"
                  },
                  "scans": {
                    "type": "number"
                  }
                },
                "required": [
                  "value",
                  "clicks",
                  "scans"
                ]
              }
            },
            "device": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "value": {
                    "type": "string"
                  },
                  "clicks": {
                    "type": "number"
                  },
                  "scans": {
                    "type": "number"
                  }
                },
                "required": [
                  "value",
                  "clicks",
                  "scans"
                ]
              }
            },
            "browser": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "value": {
                    "type": "string"
                  },
                  "clicks": {
                    "type": "number"
                  },
                  "scans": {
                    "type": "number"
                  }
                },
                "required": [
                  "value",
                  "clicks",
                  "scans"
                ]
              }
            },
            "os": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "value": {
                    "type": "string"
                  },
                  "clicks": {
                    "type": "number"
                  },
                  "scans": {
                    "type": "number"
                  }
                },
                "required": [
                  "value",
                  "clicks",
                  "scans"
                ]
              }
            },
            "referrer": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "value": {
                    "type": "string"
                  },
                  "clicks": {
                    "type": "number"
                  },
                  "scans": {
                    "type": "number"
                  }
                },
                "required": [
                  "value",
                  "clicks",
                  "scans"
                ]
              }
            },
            "utmSource": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "value": {
                    "type": "string"
                  },
                  "clicks": {
                    "type": "number"
                  },
                  "scans": {
                    "type": "number"
                  }
                },
                "required": [
                  "value",
                  "clicks",
                  "scans"
                ]
              }
            },
            "utmMedium": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "value": {
                    "type": "string"
                  },
                  "clicks": {
                    "type": "number"
                  },
                  "scans": {
                    "type": "number"
                  }
                },
                "required": [
                  "value",
                  "clicks",
                  "scans"
                ]
              }
            },
            "utmCampaign": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "value": {
                    "type": "string"
                  },
                  "clicks": {
                    "type": "number"
                  },
                  "scans": {
                    "type": "number"
                  }
                },
                "required": [
                  "value",
                  "clicks",
                  "scans"
                ]
              }
            },
            "utmTerm": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "value": {
                    "type": "string"
                  },
                  "clicks": {
                    "type": "number"
                  },
                  "scans": {
                    "type": "number"
                  }
                },
                "required": [
                  "value",
                  "clicks",
                  "scans"
                ]
              }
            },
            "utmContent": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "value": {
                    "type": "string"
                  },
                  "clicks": {
                    "type": "number"
                  },
                  "scans": {
                    "type": "number"
                  }
                },
                "required": [
                  "value",
                  "clicks",
                  "scans"
                ]
              }
            },
            "language": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "value": {
                    "type": "string"
                  },
                  "clicks": {
                    "type": "number"
                  },
                  "scans": {
                    "type": "number"
                  }
                },
                "required": [
                  "value",
                  "clicks",
                  "scans"
                ]
              }
            },
            "link": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "value": {
                    "type": "string"
                  },
                  "clicks": {
                    "type": "number"
                  },
                  "scans": {
                    "type": "number"
                  }
                },
                "required": [
                  "value",
                  "clicks",
                  "scans"
                ]
              }
            },
            "kind": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "value": {
                    "type": "string"
                  },
                  "clicks": {
                    "type": "number"
                  },
                  "scans": {
                    "type": "number"
                  }
                },
                "required": [
                  "value",
                  "clicks",
                  "scans"
                ]
              }
            }
          }
        },
        "gated": {
          "type": "array",
          "items": {
            "type": "string",
            "enum": [
              "branch",
              "country",
              "city",
              "continent",
              "device",
              "browser",
              "os",
              "referrer",
              "utmSource",
              "utmMedium",
              "utmCampaign",
              "utmTerm",
              "utmContent",
              "language",
              "link",
              "kind"
            ]
          }
        },
        "options": {
          "type": "object",
          "properties": {
            "branch": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "country": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "city": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "continent": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "device": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "browser": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "os": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "referrer": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "utmSource": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "utmMedium": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "utmCampaign": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "utmTerm": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "utmContent": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "language": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "link": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "kind": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          }
        },
        "linkLabels": {
          "type": "object",
          "additionalProperties": {
            "type": "string"
          }
        },
        "scopeFilters": {
          "type": "object",
          "additionalProperties": {
            "type": "string"
          }
        }
      },
      "required": [
        "range",
        "totals",
        "previous",
        "series",
        "breakdowns",
        "gated",
        "options",
        "linkLabels",
        "scopeFilters"
      ]
    },
    "Domain": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string"
        },
        "hostname": {
          "type": "string"
        },
        "kind": {
          "type": "string"
        },
        "status": {
          "type": "string"
        },
        "error": {
          "type": "string"
        },
        "verification": {
          "type": "object",
          "properties": {
            "type": {
              "type": "string"
            },
            "name": {
              "type": "string"
            },
            "value": {
              "type": "string"
            }
          },
          "required": [
            "type"
          ]
        },
        "validationRecords": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "txt_name": {
                "type": "string"
              },
              "txt_value": {
                "type": "string"
              }
            }
          }
        },
        "removing": {
          "type": "boolean"
        },
        "bioPageId": {
          "type": "string"
        },
        "createdAt": {
          "type": [
            "string",
            "null"
          ],
          "format": "date-time"
        },
        "updatedAt": {
          "type": [
            "string",
            "null"
          ],
          "format": "date-time"
        }
      },
      "required": [
        "id",
        "hostname",
        "kind",
        "status",
        "error",
        "validationRecords",
        "removing",
        "createdAt",
        "updatedAt"
      ]
    },
    "BioSummary": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string"
        },
        "slug": {
          "type": "string"
        },
        "title": {
          "type": "string"
        },
        "linkCount": {
          "type": "number"
        },
        "avatarUrl": {
          "type": [
            "string",
            "null"
          ]
        },
        "updatedAt": {
          "type": "string",
          "format": "date-time"
        },
        "createdBy": {
          "type": [
            "string",
            "null"
          ]
        }
      },
      "required": [
        "id",
        "slug",
        "title",
        "linkCount",
        "avatarUrl",
        "updatedAt",
        "createdBy"
      ]
    },
    "BioDraft": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string"
        },
        "handle": {
          "type": "string"
        },
        "revision": {
          "type": "number"
        },
        "publishedRevision": {
          "type": "number"
        },
        "published": {
          "type": "boolean"
        },
        "content": {
          "type": "object",
          "properties": {
            "locale": {
              "type": "string",
              "enum": [
                "en",
                "pt",
                "es"
              ],
              "default": "en"
            },
            "blocks": {
              "type": "array",
              "items": {
                "oneOf": [
                  {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 100
                      },
                      "visible": {
                        "type": "boolean",
                        "default": true
                      },
                      "schedule": {
                        "type": "object",
                        "properties": {
                          "from": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "to": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      },
                      "type": {
                        "type": "string",
                        "enum": [
                          "profile"
                        ]
                      },
                      "title": {
                        "type": "string",
                        "maxLength": 120
                      },
                      "description": {
                        "type": "string",
                        "maxLength": 500,
                        "default": ""
                      },
                      "avatar": {
                        "anyOf": [
                          {
                            "type": "string",
                            "maxLength": 2048,
                            "format": "uri"
                          },
                          {
                            "type": "string",
                            "enum": [
                              ""
                            ]
                          }
                        ],
                        "default": ""
                      },
                      "align": {
                        "type": "string",
                        "enum": [
                          "center",
                          "left",
                          "right"
                        ],
                        "default": "center"
                      },
                      "avatarShape": {
                        "type": "string",
                        "enum": [
                          "circle",
                          "rounded",
                          "square"
                        ],
                        "default": "circle"
                      },
                      "paidBadge": {
                        "type": "boolean"
                      }
                    },
                    "required": [
                      "id",
                      "type",
                      "title"
                    ]
                  },
                  {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 100
                      },
                      "visible": {
                        "type": "boolean",
                        "default": true
                      },
                      "schedule": {
                        "type": "object",
                        "properties": {
                          "from": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "to": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      },
                      "type": {
                        "type": "string",
                        "enum": [
                          "link"
                        ]
                      },
                      "linkId": {
                        "type": "string",
                        "pattern": "^[a-f\\d]{24}$/i"
                      },
                      "title": {
                        "type": "string",
                        "maxLength": 120
                      },
                      "image": {
                        "anyOf": [
                          {
                            "type": "string",
                            "maxLength": 2048,
                            "format": "uri"
                          },
                          {
                            "type": "string",
                            "enum": [
                              ""
                            ]
                          }
                        ],
                        "default": ""
                      },
                      "highlight": {
                        "type": "boolean",
                        "default": false
                      }
                    },
                    "required": [
                      "id",
                      "type",
                      "linkId",
                      "title"
                    ]
                  },
                  {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 100
                      },
                      "visible": {
                        "type": "boolean",
                        "default": true
                      },
                      "schedule": {
                        "type": "object",
                        "properties": {
                          "from": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "to": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      },
                      "type": {
                        "type": "string",
                        "enum": [
                          "links"
                        ]
                      },
                      "title": {
                        "type": "string",
                        "maxLength": 120,
                        "default": ""
                      },
                      "layout": {
                        "type": "string",
                        "enum": [
                          "vertical",
                          "paginated"
                        ],
                        "default": "vertical"
                      },
                      "pageSize": {
                        "type": "integer",
                        "minimum": 4,
                        "maximum": 50,
                        "default": 10
                      },
                      "cardStyle": {
                        "type": "string",
                        "enum": [
                          "list",
                          "small-grid",
                          "large-grid",
                          "image-grid"
                        ],
                        "default": "list"
                      },
                      "cardContent": {
                        "type": "string",
                        "enum": [
                          "image-text",
                          "image-only",
                          "text"
                        ],
                        "default": "image-text"
                      },
                      "cardRadius": {
                        "type": "string",
                        "enum": [
                          "theme",
                          "square",
                          "rounded"
                        ],
                        "default": "theme"
                      },
                      "imageFit": {
                        "type": "string",
                        "enum": [
                          "cover",
                          "contain"
                        ],
                        "default": "cover"
                      },
                      "linkSurface": {
                        "type": "string",
                        "enum": [
                          "theme",
                          "minimal",
                          "soft",
                          "filled",
                          "outline"
                        ],
                        "default": "theme"
                      },
                      "linkBorder": {
                        "type": "string",
                        "enum": [
                          "theme",
                          "none",
                          "subtle",
                          "solid",
                          "divider"
                        ],
                        "default": "theme"
                      },
                      "linkWeight": {
                        "type": "string",
                        "enum": [
                          "theme",
                          "regular",
                          "medium",
                          "bold"
                        ],
                        "default": "theme"
                      },
                      "linkSpacing": {
                        "type": "string",
                        "enum": [
                          "compact",
                          "comfortable",
                          "spacious"
                        ],
                        "default": "comfortable"
                      },
                      "showLinkArrow": {
                        "type": "boolean",
                        "default": true
                      },
                      "paginationAppearance": {
                        "type": "string",
                        "enum": [
                          "muted",
                          "minimal",
                          "outline",
                          "filled"
                        ],
                        "default": "muted"
                      },
                      "paginationSize": {
                        "type": "string",
                        "enum": [
                          "small",
                          "comfortable"
                        ],
                        "default": "small"
                      },
                      "paginationStyle": {
                        "type": "string",
                        "enum": [
                          "arrows",
                          "labels"
                        ],
                        "default": "arrows"
                      },
                      "showPageCount": {
                        "type": "boolean",
                        "default": true
                      },
                      "showLinkCount": {
                        "type": "boolean",
                        "default": false
                      },
                      "items": {
                        "type": "array",
                        "items": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "string",
                              "minLength": 1,
                              "maxLength": 100
                            },
                            "visible": {
                              "type": "boolean",
                              "default": true
                            },
                            "schedule": {
                              "type": "object",
                              "properties": {
                                "from": {
                                  "type": "string",
                                  "format": "date-time"
                                },
                                "to": {
                                  "type": "string",
                                  "format": "date-time"
                                }
                              }
                            },
                            "linkId": {
                              "type": "string",
                              "pattern": "^[a-f\\d]{24}$/i"
                            },
                            "title": {
                              "type": "string",
                              "maxLength": 120
                            },
                            "image": {
                              "anyOf": [
                                {
                                  "type": "string",
                                  "maxLength": 2048,
                                  "format": "uri"
                                },
                                {
                                  "type": "string",
                                  "enum": [
                                    ""
                                  ]
                                }
                              ],
                              "default": ""
                            },
                            "highlight": {
                              "type": "boolean",
                              "default": false
                            }
                          },
                          "required": [
                            "id",
                            "linkId",
                            "title"
                          ]
                        },
                        "maxItems": 1000
                      }
                    },
                    "required": [
                      "id",
                      "type",
                      "items"
                    ]
                  },
                  {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 100
                      },
                      "visible": {
                        "type": "boolean",
                        "default": true
                      },
                      "schedule": {
                        "type": "object",
                        "properties": {
                          "from": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "to": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      },
                      "type": {
                        "type": "string",
                        "enum": [
                          "socials"
                        ]
                      },
                      "items": {
                        "type": "array",
                        "items": {
                          "type": "object",
                          "properties": {
                            "platform": {
                              "type": "string",
                              "minLength": 1,
                              "maxLength": 32
                            },
                            "url": {
                              "type": "string",
                              "maxLength": 2048,
                              "format": "uri"
                            }
                          },
                          "required": [
                            "platform",
                            "url"
                          ]
                        },
                        "maxItems": 20
                      }
                    },
                    "required": [
                      "id",
                      "type",
                      "items"
                    ]
                  },
                  {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 100
                      },
                      "visible": {
                        "type": "boolean",
                        "default": true
                      },
                      "schedule": {
                        "type": "object",
                        "properties": {
                          "from": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "to": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      },
                      "type": {
                        "type": "string",
                        "enum": [
                          "header"
                        ]
                      },
                      "text": {
                        "type": "string",
                        "maxLength": 120
                      }
                    },
                    "required": [
                      "id",
                      "type",
                      "text"
                    ]
                  },
                  {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 100
                      },
                      "visible": {
                        "type": "boolean",
                        "default": true
                      },
                      "schedule": {
                        "type": "object",
                        "properties": {
                          "from": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "to": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      },
                      "type": {
                        "type": "string",
                        "enum": [
                          "text"
                        ]
                      },
                      "text": {
                        "type": "string",
                        "maxLength": 2000
                      }
                    },
                    "required": [
                      "id",
                      "type",
                      "text"
                    ]
                  },
                  {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 100
                      },
                      "visible": {
                        "type": "boolean",
                        "default": true
                      },
                      "schedule": {
                        "type": "object",
                        "properties": {
                          "from": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "to": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      },
                      "type": {
                        "type": "string",
                        "enum": [
                          "image"
                        ]
                      },
                      "url": {
                        "anyOf": [
                          {
                            "type": "string",
                            "maxLength": 2048,
                            "format": "uri"
                          },
                          {
                            "type": "string",
                            "enum": [
                              ""
                            ]
                          }
                        ]
                      },
                      "alt": {
                        "type": "string",
                        "maxLength": 300
                      },
                      "link": {
                        "type": "string",
                        "maxLength": 2048,
                        "format": "uri"
                      }
                    },
                    "required": [
                      "id",
                      "type",
                      "url",
                      "alt"
                    ]
                  },
                  {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 100
                      },
                      "visible": {
                        "type": "boolean",
                        "default": true
                      },
                      "schedule": {
                        "type": "object",
                        "properties": {
                          "from": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "to": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      },
                      "type": {
                        "type": "string",
                        "enum": [
                          "embed"
                        ]
                      },
                      "provider": {
                        "type": "string",
                        "enum": [
                          "youtube",
                          "spotify",
                          "soundcloud",
                          "vimeo",
                          "apple-music"
                        ]
                      },
                      "url": {
                        "anyOf": [
                          {
                            "type": "string",
                            "maxLength": 2048,
                            "format": "uri"
                          },
                          {
                            "type": "string",
                            "enum": [
                              ""
                            ]
                          }
                        ]
                      }
                    },
                    "required": [
                      "id",
                      "type",
                      "provider",
                      "url"
                    ]
                  },
                  {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 100
                      },
                      "visible": {
                        "type": "boolean",
                        "default": true
                      },
                      "schedule": {
                        "type": "object",
                        "properties": {
                          "from": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "to": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      },
                      "type": {
                        "type": "string",
                        "enum": [
                          "qr"
                        ]
                      },
                      "linkId": {
                        "type": "string",
                        "pattern": "^[a-f\\d]{24}$/i"
                      },
                      "title": {
                        "type": "string",
                        "maxLength": 120,
                        "default": ""
                      }
                    },
                    "required": [
                      "id",
                      "type",
                      "linkId"
                    ]
                  }
                ]
              },
              "maxItems": 150
            },
            "theme": {
              "type": "object",
              "properties": {
                "background": {
                  "type": "string",
                  "pattern": "^#[a-f\\d]{6}$/i"
                },
                "surface": {
                  "type": "string",
                  "pattern": "^#[a-f\\d]{6}$/i"
                },
                "text": {
                  "type": "string",
                  "pattern": "^#[a-f\\d]{6}$/i"
                },
                "accent": {
                  "type": "string",
                  "pattern": "^#[a-f\\d]{6}$/i"
                },
                "buttonStyle": {
                  "type": "string",
                  "enum": [
                    "fill",
                    "outline",
                    "soft",
                    "shadow-less"
                  ]
                },
                "radius": {
                  "anyOf": [
                    {
                      "type": "number",
                      "enum": [
                        0
                      ]
                    },
                    {
                      "type": "number",
                      "enum": [
                        6
                      ]
                    },
                    {
                      "type": "number",
                      "enum": [
                        12
                      ]
                    },
                    {
                      "type": "number",
                      "enum": [
                        999
                      ]
                    }
                  ]
                },
                "font": {
                  "type": "string",
                  "enum": [
                    "Geist",
                    "Inter",
                    "Roboto",
                    "Open Sans",
                    "Lato",
                    "Montserrat",
                    "Poppins",
                    "DM Sans",
                    "Space Grotesk",
                    "Nunito",
                    "Raleway",
                    "Playfair Display",
                    "Merriweather",
                    "Lora",
                    "Oswald",
                    "Source Serif 4"
                  ]
                },
                "density": {
                  "type": "string",
                  "enum": [
                    "comfortable",
                    "compact"
                  ]
                },
                "backgroundImage": {
                  "type": "string",
                  "maxLength": 2048,
                  "format": "uri"
                },
                "legacyFont": {
                  "type": "string",
                  "pattern": "^[a-zA-Z][a-zA-Z0-9 -]{0,63}$"
                }
              },
              "required": [
                "background",
                "surface",
                "text",
                "accent",
                "buttonStyle",
                "radius",
                "font",
                "density"
              ]
            },
            "seo": {
              "type": "object",
              "properties": {
                "favicon": {
                  "type": "string",
                  "maxLength": 2048,
                  "format": "uri"
                },
                "title": {
                  "type": "string",
                  "maxLength": 120,
                  "default": ""
                },
                "description": {
                  "type": "string",
                  "maxLength": 500,
                  "default": ""
                },
                "noindex": {
                  "type": "boolean",
                  "default": false
                }
              }
            },
            "hideBranding": {
              "type": "boolean",
              "default": false
            }
          },
          "required": [
            "blocks",
            "theme",
            "seo"
          ]
        }
      },
      "required": [
        "id",
        "handle",
        "revision",
        "publishedRevision",
        "published",
        "content"
      ]
    },
    "Webhook": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string"
        },
        "name": {
          "type": "string"
        },
        "url": {
          "type": "string"
        },
        "events": {
          "type": "array",
          "items": {
            "type": "string",
            "enum": [
              "link.created",
              "link.updated",
              "link.deleted",
              "link.clicked",
              "qr.scanned",
              "bio.published",
              "domain.verified",
              "import.completed"
            ]
          }
        },
        "enabled": {
          "type": "boolean"
        },
        "createdAt": {
          "type": "string",
          "format": "date-time"
        },
        "updatedAt": {
          "type": "string",
          "format": "date-time"
        },
        "failureSince": {
          "type": [
            "string",
            "null"
          ],
          "format": "date-time"
        },
        "disabledAt": {
          "type": [
            "string",
            "null"
          ],
          "format": "date-time"
        }
      },
      "required": [
        "id",
        "name",
        "url",
        "events",
        "enabled",
        "createdAt",
        "updatedAt",
        "failureSince",
        "disabledAt"
      ]
    }
  },
  "parameters": {}
}
```
