API docs

Links API

mount /links · base https://zpe.vc/links · v2.0.0 · 38 operations · 22 write · 0 need no account · stable

archived: committed 2026-11-01, from 69440287. Contract: https://zpe.vc/links/_/api/v1/openapi.json

GET /_/api/v1/links/{id} Get a link with click stats getLinksById

Get a link with click stats

Parameters

nameinrequiredtypenotes
idpathyesinteger
rangequeryno"24h" | "7d" | "30d" | "90d" | "1y" | "all"Time range for analytics. Defaults are documented per endpoint.

Request body

None: this operation takes its arguments in the path and the query.

Responses

statusmeaningbody
200OK.object
400Validation error.object
401Missing or invalid bearer token.object
403Scope insufficient.object
404Not found.object
500Server error.object

Example

Get a link with click stats, with a token from the door.

GET /_/api/v1/links/{id}
curl -X GET 'https://zpe.vc/links/_/api/v1/links/42?range=24h' \
  -H 'Authorization: Bearer $ZPE_TOKEN'
200 application/json
{
  "id": 42,
  "url": "https://example.com/long-url",
  "label": "Marketing landing",
  "created_at": 0,
  "expires_at": null,
  "created_via": null,
  "created_by": "string",
  "slugs": [
    {
      "link_id": 0,
      "slug": "string",
      "is_custom": 0,
      "is_primary": 0,
      "click_count": 0,
      "created_at": 0,
      "disabled_at": null
    }
  ],
  "total_clicks": 137,
  "delta_pct": 0
}

The example is generated from the schema in the contract: the shape is the document's, the values are placeholders.

PUT /_/api/v1/links/{id} Update a link's URL, label, or expiry putLinksById

Owner only. The identity behind the API key must be the link's `created_by`, otherwise the call is refused with 403.

Parameters

nameinrequiredtypenotes
idpathyesinteger

Request body application/json · optional

fieldtyperequirednotes
urlstring (uri)no
labelstring | nullno
expires_atinteger | nullno

Responses

statusmeaningbody
200Updated.object
400Validation error.object
401Missing or invalid bearer token.object
403Refused. Either the key lacks the `create` scope, or the caller is not the link owner. Ownership is fixed at creation and is not transferable: only the identity that created a link can change it. Reading is open to every authenticated key.object
404Not found.object

Example

Update a link's URL, label, or expiry, with a token from the door.

PUT /_/api/v1/links/{id}
curl -X PUT 'https://zpe.vc/links/_/api/v1/links/42' \
  -H 'Authorization: Bearer $ZPE_TOKEN' \
  -H 'content-type: application/json' \
  -d '{
  "url": "https://example.com",
  "label": null,
  "expires_at": null
}'
200 application/json
{
  "id": 42,
  "url": "https://example.com/long-url",
  "label": "Marketing landing",
  "created_at": 0,
  "expires_at": null,
  "created_via": null,
  "created_by": "string",
  "slugs": [
    {
      "link_id": 0,
      "slug": "string",
      "is_custom": 0,
      "is_primary": 0,
      "click_count": 0,
      "created_at": 0,
      "disabled_at": null
    }
  ],
  "total_clicks": 137,
  "delta_pct": 0
}

The example is generated from the schema in the contract: the shape is the document's, the values are placeholders.

DELETE /_/api/v1/links/{id} Delete a link permanently deleteLinksById

Owner only, and only while the link has recorded zero clicks. Once any click exists the link is permanent and this call returns 400; disable it instead. Deleting removes the link, its slugs, and frees those slugs for reuse by anyone.

Parameters

nameinrequiredtypenotes
idpathyesinteger

Request body

None: this operation takes its arguments in the path and the query.

Responses

statusmeaningbody
200Deleted.object
400Refused. The link has recorded at least one click, so it cannot be deleted: `Cannot delete a link with clicks, disable it instead`. A clicked short link is already in circulation, in email, in print and in QR codes, and deleting it frees the slug for reuse. Call `POST /links/{id}/disable` instead, which stops the redirect and keeps the history. The lifetime count is unfiltered, so a single bot click is enough. Also returned for a malformed request.object
401Missing or invalid bearer token.object
403Refused. Either the key lacks the `create` scope, or the caller is not the link owner. Ownership is fixed at creation and is not transferable: only the identity that created a link can change it. Reading is open to every authenticated key.object
404Not found.object

Example

Delete a link permanently, with a token from the door.

DELETE /_/api/v1/links/{id}
curl -X DELETE 'https://zpe.vc/links/_/api/v1/links/42' \
  -H 'Authorization: Bearer $ZPE_TOKEN'
200 application/json
{
  "deleted": false
}

The example is generated from the schema in the contract: the shape is the document's, the values are placeholders.

POST /_/api/v1/links/{id}/disable Disable a link postLinksByIdDisable

Owner only. Stops every slug on the link from redirecting while keeping the link and its click history. This is the correct operation for a link that is already in circulation, and the only one available once the link has recorded a click.

Parameters

nameinrequiredtypenotes
idpathyesinteger

Request body

None: this operation takes its arguments in the path and the query.

Responses

statusmeaningbody
200Disabled.object
401Missing or invalid bearer token.object
403Refused. Either the key lacks the `create` scope, or the caller is not the link owner. Ownership is fixed at creation and is not transferable: only the identity that created a link can change it. Reading is open to every authenticated key.object
404Not found.object

Example

Disable a link, with a token from the door.

POST /_/api/v1/links/{id}/disable
curl -X POST 'https://zpe.vc/links/_/api/v1/links/42/disable' \
  -H 'Authorization: Bearer $ZPE_TOKEN'
200 application/json
{
  "id": 42,
  "url": "https://example.com/long-url",
  "label": "Marketing landing",
  "created_at": 0,
  "expires_at": null,
  "created_via": null,
  "created_by": "string",
  "slugs": [
    {
      "link_id": 0,
      "slug": "string",
      "is_custom": 0,
      "is_primary": 0,
      "click_count": 0,
      "created_at": 0,
      "disabled_at": null
    }
  ],
  "total_clicks": 137,
  "delta_pct": 0
}

The example is generated from the schema in the contract: the shape is the document's, the values are placeholders.

POST /_/api/v1/links/{id}/enable Re-enable a disabled link postLinksByIdEnable

Owner only. Clears `expires_at` outright, including an expiry set at create or update, so the link resolves again and no longer expires.

Parameters

nameinrequiredtypenotes
idpathyesinteger

Request body

None: this operation takes its arguments in the path and the query.

Responses

statusmeaningbody
200Enabled.object
401Missing or invalid bearer token.object
403Refused. Either the key lacks the `create` scope, or the caller is not the link owner. Ownership is fixed at creation and is not transferable: only the identity that created a link can change it. Reading is open to every authenticated key.object
404Not found.object

Example

Re-enable a disabled link, with a token from the door.

POST /_/api/v1/links/{id}/enable
curl -X POST 'https://zpe.vc/links/_/api/v1/links/42/enable' \
  -H 'Authorization: Bearer $ZPE_TOKEN'
200 application/json
{
  "id": 42,
  "url": "https://example.com/long-url",
  "label": "Marketing landing",
  "created_at": 0,
  "expires_at": null,
  "created_via": null,
  "created_by": "string",
  "slugs": [
    {
      "link_id": 0,
      "slug": "string",
      "is_custom": 0,
      "is_primary": 0,
      "click_count": 0,
      "created_at": 0,
      "disabled_at": null
    }
  ],
  "total_clicks": 137,
  "delta_pct": 0
}

The example is generated from the schema in the contract: the shape is the document's, the values are placeholders.

POST /_/api/v1/links/{id}/slugs Add a custom slug to a link postLinksByIdSlugs

Open to any authenticated caller with the `create` scope, including on a link owned by someone else. Adding a slug creates an additional address for the existing destination; it cannot change, disable or delete anything. Removing or disabling that slug afterwards is owner-gated.

Parameters

nameinrequiredtypenotes
idpathyesinteger

Request body application/json · optional

fieldtyperequirednotes
slugstringyes

Responses

statusmeaningbody
201Slug added.object
400Validation error.object
401Missing or invalid bearer token.object
403Scope insufficient.object
404Not found.object
409Conflict.object

Example

Add a custom slug to a link, with a token from the door.

POST /_/api/v1/links/{id}/slugs
curl -X POST 'https://zpe.vc/links/_/api/v1/links/42/slugs' \
  -H 'Authorization: Bearer $ZPE_TOKEN' \
  -H 'content-type: application/json' \
  -d '{
  "slug": "string"
}'
201 application/json
{
  "link_id": 0,
  "slug": "string",
  "is_custom": 0,
  "is_primary": 0,
  "click_count": 0,
  "created_at": 0,
  "disabled_at": null
}

The example is generated from the schema in the contract: the shape is the document's, the values are placeholders.

POST /_/api/v1/links/{id}/slugs/{slug}/disable Disable a specific slug on a link postLinksByIdSlugsBySlugDisable

Owner of the parent link only. Custom slugs only: the system-generated slug cannot be disabled. The slug stops resolving and keeps its click history.

Parameters

nameinrequiredtypenotes
idpathyesinteger
slugpathyesstring

Request body

None: this operation takes its arguments in the path and the query.

Responses

statusmeaningbody
200Disabled.object
400Refused. The system-generated slug cannot be disabled, only custom slugs can. Disable the whole link instead. Also returned for a malformed request.object
401Missing or invalid bearer token.object
403Refused. Either the key lacks the `create` scope, or the caller is not the link owner. Ownership is fixed at creation and is not transferable: only the identity that created a link can change it. Reading is open to every authenticated key.object
404Not found.object

Example

Disable a specific slug on a link, with a token from the door.

POST /_/api/v1/links/{id}/slugs/{slug}/disable
curl -X POST 'https://zpe.vc/links/_/api/v1/links/42/slugs/string/disable' \
  -H 'Authorization: Bearer $ZPE_TOKEN'
200 application/json
{
  "link_id": 0,
  "slug": "string",
  "is_custom": 0,
  "is_primary": 0,
  "click_count": 0,
  "created_at": 0,
  "disabled_at": null
}

The example is generated from the schema in the contract: the shape is the document's, the values are placeholders.

POST /_/api/v1/links/{id}/slugs/{slug}/enable Re-enable a disabled slug on a link postLinksByIdSlugsBySlugEnable

Owner of the parent link only.

Parameters

nameinrequiredtypenotes
idpathyesinteger
slugpathyesstring

Request body

None: this operation takes its arguments in the path and the query.

Responses

statusmeaningbody
200Enabled.object
400Validation error.object
401Missing or invalid bearer token.object
403Refused. Either the key lacks the `create` scope, or the caller is not the link owner. Ownership is fixed at creation and is not transferable: only the identity that created a link can change it. Reading is open to every authenticated key.object
404Not found.object

Example

Re-enable a disabled slug on a link, with a token from the door.

POST /_/api/v1/links/{id}/slugs/{slug}/enable
curl -X POST 'https://zpe.vc/links/_/api/v1/links/42/slugs/string/enable' \
  -H 'Authorization: Bearer $ZPE_TOKEN'
200 application/json
{
  "link_id": 0,
  "slug": "string",
  "is_custom": 0,
  "is_primary": 0,
  "click_count": 0,
  "created_at": 0,
  "disabled_at": null
}

The example is generated from the schema in the contract: the shape is the document's, the values are placeholders.

DELETE /_/api/v1/links/{id}/slugs/{slug} Remove a custom slug from a link deleteLinksByIdSlugsBySlug

Owner of the parent link only, custom slugs only, and only while the slug has recorded zero clicks. Once any click exists the slug is permanent and this call returns 400; disable it instead.

Parameters

nameinrequiredtypenotes
idpathyesinteger
slugpathyesstring

Request body

None: this operation takes its arguments in the path and the query.

Responses

statusmeaningbody
200Removed.object
400Refused. Either the slug has recorded at least one click (`Cannot remove a slug with clicks, disable it instead`), or it is the link's system-generated slug, which can never be removed. For a clicked custom slug, call `POST /links/{id}/slugs/{slug}/disable` instead: it stops that slug resolving and keeps its click history. For the system-generated slug, disable the whole link with `POST /links/{id}/disable`. Also returned for a malformed request.object
401Missing or invalid bearer token.object
403Refused. Either the key lacks the `create` scope, or the caller is not the link owner. Ownership is fixed at creation and is not transferable: only the identity that created a link can change it. Reading is open to every authenticated key.object
404Not found.object

Example

Remove a custom slug from a link, with a token from the door.

DELETE /_/api/v1/links/{id}/slugs/{slug}
curl -X DELETE 'https://zpe.vc/links/_/api/v1/links/42/slugs/string' \
  -H 'Authorization: Bearer $ZPE_TOKEN'
200 application/json
{
  "removed": false
}

The example is generated from the schema in the contract: the shape is the document's, the values are placeholders.

GET /_/api/v1/links/{id}/qr Get a QR code SVG for a link getLinksByIdQr

Get a QR code SVG for a link

Parameters

nameinrequiredtypenotes
idpathyesinteger
sizequerynointegerQR pixel dimensions (square). Integer 1-2048; defaults to 220.
slugquerynostringOptional specific slug. Defaults to the link's primary slug.

Request body

None: this operation takes its arguments in the path and the query.

Responses

statusmeaningbody
200SVG imagestring
400Validation error.object
401Missing or invalid bearer token.object
403Scope insufficient.object
404Not found.object

Example

Get a QR code SVG for a link, with a token from the door.

GET /_/api/v1/links/{id}/qr
curl -X GET 'https://zpe.vc/links/_/api/v1/links/42/qr?size=0&slug=string' \
  -H 'Authorization: Bearer $ZPE_TOKEN'
200 image/svg+xml
"string"

The example is generated from the schema in the contract: the shape is the document's, the values are placeholders.

GET /_/api/v1/links/{id}/bundles List bundles a link belongs to getLinksByIdBundles

List bundles a link belongs to

Parameters

nameinrequiredtypenotes
idpathyesinteger

Request body

None: this operation takes its arguments in the path and the query.

Responses

statusmeaningbody
200OK.array of object
401Missing or invalid bearer token.object
403Scope insufficient.object
404Not found.object

Example

List bundles a link belongs to, with a token from the door.

GET /_/api/v1/links/{id}/bundles
curl -X GET 'https://zpe.vc/links/_/api/v1/links/42/bundles' \
  -H 'Authorization: Bearer $ZPE_TOKEN'
200 application/json
[
  {
    "id": 0,
    "name": "string",
    "description": null,
    "icon": null,
    "accent": "orange",
    "archived_at": null,
    "created_via": null,
    "created_by": "string",
    "created_at": 0,
    "updated_at": 0
  }
]

The example is generated from the schema in the contract: the shape is the document's, the values are placeholders.

GET /_/api/v1/links/{id}/analytics Get analytics for a link (defaults to all-time) getLinksByIdAnalytics

Get analytics for a link (defaults to all-time)

Parameters

nameinrequiredtypenotes
idpathyesinteger
rangequeryno"24h" | "7d" | "30d" | "90d" | "1y" | "all"Time range for analytics. Defaults are documented per endpoint.

Request body

None: this operation takes its arguments in the path and the query.

Responses

statusmeaningbody
200OKobject
400Validation error.object
401Missing or invalid bearer token.object
403Scope insufficient.object
404Not found.object
500Server error.object

Example

Get analytics for a link (defaults to all-time), with a token from the door.

GET /_/api/v1/links/{id}/analytics
curl -X GET 'https://zpe.vc/links/_/api/v1/links/42/analytics?range=24h' \
  -H 'Authorization: Bearer $ZPE_TOKEN'
200 application/json
{
  "total_clicks": 0,
  "countries": [
    {
      "name": "string",
      "count": 0
    }
  ],
  "referrers": [
    {
      "name": "string",
      "count": 0
    }
  ],
  "referrer_hosts": [
    {
      "name": "string",
      "count": 0
    }
  ],
  "devices": [
    {
      "name": "string",
      "count": 0
    }
  ],
  "os": [
    {
      "name": "string",
      "count": 0
    }
  ],
  "browsers": [
    {
      "name": "string",
      "count": 0
    }
  ],
  "link_modes": [
    {
      "name": "string",
      "count": 0
    }
  ],
  "channels": [
    {
      "name": "string",
      "count": 0
    }
  ],
  "clicks_over_time": [
    {
      "date": "string",
      "count": 0
    }
  ],
  "slug_clicks": [
    {
      "slug": "string",
      "count": 0
    }
  ],
  "num_countries": 0,
  "num_referrers": 0,
  "num_referrer_hosts": 0,
  "num_os": 0,
  "num_browsers": 0
}

The example is generated from the schema in the contract: the shape is the document's, the values are placeholders.

GET /_/api/v1/links/{id}/timeline Get click timeline for a link (defaults to all-time) getLinksByIdTimeline

Get click timeline for a link (defaults to all-time)

Parameters

nameinrequiredtypenotes
idpathyesinteger
rangequeryno"24h" | "7d" | "30d" | "90d" | "1y" | "all"Time range for analytics. Defaults are documented per endpoint.

Request body

None: this operation takes its arguments in the path and the query.

Responses

statusmeaningbody
200OKobject
400Validation error.object
401Missing or invalid bearer token.object
403Scope insufficient.object
404Not found.object
500Server error.object

Example

Get click timeline for a link (defaults to all-time), with a token from the door.

GET /_/api/v1/links/{id}/timeline
curl -X GET 'https://zpe.vc/links/_/api/v1/links/42/timeline?range=24h' \
  -H 'Authorization: Bearer $ZPE_TOKEN'
200 application/json
{
  "range": "24h",
  "buckets": [
    {
      "label": "string",
      "count": 0
    }
  ],
  "summary": {
    "last_24h": 0,
    "last_7d": 0,
    "last_30d": 0,
    "last_90d": 0,
    "last_1y": 0
  }
}

The example is generated from the schema in the contract: the shape is the document's, the values are placeholders.

GET /_/api/v1/links/{id}/breakdown Page through a link's countries, sources, or domains breakdown getLinksByIdBreakdown

Page through a link's countries, sources, or domains breakdown

Parameters

nameinrequiredtypenotes
idpathyesinteger
dimensionqueryyes"countries" | "referrers" | "referrer_hosts"Which panel to page through: countries, referrers (sources), or referrer_hosts (domains).
limitquerynointegerPage size, 1-100. Defaults to 10.
offsetquerynointeger | nullRow offset into the ranked list. Defaults to 0.
rangequeryno"24h" | "7d" | "30d" | "90d" | "1y" | "all"Time range. Defaults to all-time.

Request body

None: this operation takes its arguments in the path and the query.

Responses

statusmeaningbody
200OKobject
400Validation error.object
401Missing or invalid bearer token.object
403Scope insufficient.object
404Not found.object
500Server error.object

Example

Page through a link's countries, sources, or domains breakdown, with a token from the door.

GET /_/api/v1/links/{id}/breakdown
curl -X GET 'https://zpe.vc/links/_/api/v1/links/42/breakdown?dimension=countries&limit=0&offset=<offset>&range=24h' \
  -H 'Authorization: Bearer $ZPE_TOKEN'
200 application/json
{
  "items": [
    {
      "name": "string",
      "count": 0
    }
  ],
  "total": 0
}

The example is generated from the schema in the contract: the shape is the document's, the values are placeholders.

GET /_/api/v1/slugs/{slug} Look up a link by its slug getSlugsBySlug

Open to every authenticated key, whoever owns the link. Reads are not owner-scoped.

Parameters

nameinrequiredtypenotes
slugpathyesstring

Request body

None: this operation takes its arguments in the path and the query.

Responses

statusmeaningbody
200OKobject
401Missing or invalid bearer token.object
403Scope insufficient.object
404Slug not found.object
500Server error.object

Example

Look up a link by its slug, with a token from the door.

GET /_/api/v1/slugs/{slug}
curl -X GET 'https://zpe.vc/links/_/api/v1/slugs/string' \
  -H 'Authorization: Bearer $ZPE_TOKEN'
200 application/json
{
  "id": 42,
  "url": "https://example.com/long-url",
  "label": "Marketing landing",
  "created_at": 0,
  "expires_at": null,
  "created_via": null,
  "created_by": "string",
  "slugs": [
    {
      "link_id": 0,
      "slug": "string",
      "is_custom": 0,
      "is_primary": 0,
      "click_count": 0,
      "created_at": 0,
      "disabled_at": null
    }
  ],
  "total_clicks": 137,
  "delta_pct": 0
}

The example is generated from the schema in the contract: the shape is the document's, the values are placeholders.

GET /_/api/v1/bundles List bundles getBundles

Every bundle on the deployment, not only the caller's. Reads are open across owners.

Parameters

nameinrequiredtypenotes
archivedqueryno"1" | "true" | "only" | "all"true|1|only -> archived only. all -> include archived. Omit -> active only.
rangequeryno"24h" | "7d" | "30d" | "90d" | "1y" | "all"Time range for analytics. Defaults are documented per endpoint.

Request body

None: this operation takes its arguments in the path and the query.

Responses

statusmeaningbody
200OK.array of object
400Validation error.object
401Missing or invalid bearer token.object
403Scope insufficient.object

Example

List bundles, with a token from the door.

GET /_/api/v1/bundles
curl -X GET 'https://zpe.vc/links/_/api/v1/bundles?archived=1&range=24h' \
  -H 'Authorization: Bearer $ZPE_TOKEN'
200 application/json
[
  {
    "id": 0,
    "name": "string",
    "description": null,
    "icon": null,
    "accent": "orange",
    "archived_at": null,
    "created_via": null,
    "created_by": "string",
    "created_at": 0,
    "updated_at": 0,
    "link_count": 0,
    "total_clicks": 0,
    "delta_pct": 0,
    "sparkline": [
      0
    ],
    "top_links": [
      {
        "slug": "string",
        "click_count": 0
      }
    ]
  }
]

The example is generated from the schema in the contract: the shape is the document's, the values are placeholders.

POST /_/api/v1/bundles Create a bundle postBundles

The identity behind the API key becomes the bundle's owner and is the only one who can later change it.

Parameters

No parameters.

Request body application/json · optional

fieldtyperequirednotes
namestringyes
descriptionstring | nullno
iconstring | nullno
accent"orange" | "red" | "green" | "blue" | "purple"no

Responses

statusmeaningbody
201Created.object
400Validation error.object
401Missing or invalid bearer token.object
403Scope insufficient.object

Example

Create a bundle, with a token from the door.

POST /_/api/v1/bundles
curl -X POST 'https://zpe.vc/links/_/api/v1/bundles' \
  -H 'Authorization: Bearer $ZPE_TOKEN' \
  -H 'content-type: application/json' \
  -d '{
  "name": "string",
  "description": null,
  "icon": null,
  "accent": "orange"
}'
201 application/json
{
  "id": 0,
  "name": "string",
  "description": null,
  "icon": null,
  "accent": "orange",
  "archived_at": null,
  "created_via": null,
  "created_by": "string",
  "created_at": 0,
  "updated_at": 0
}

The example is generated from the schema in the contract: the shape is the document's, the values are placeholders.

GET /_/api/v1/bundles/{id} Get a bundle getBundlesById

Open to every authenticated key, whoever owns the bundle.

Parameters

nameinrequiredtypenotes
idpathyesinteger
rangequeryno"24h" | "7d" | "30d" | "90d" | "1y" | "all"Time range for analytics. Defaults are documented per endpoint.

Request body

None: this operation takes its arguments in the path and the query.

Responses

statusmeaningbody
200OK.object
400Validation error.object
401Missing or invalid bearer token.object
403Scope insufficient.object
404Not found.object

Example

Get a bundle, with a token from the door.

GET /_/api/v1/bundles/{id}
curl -X GET 'https://zpe.vc/links/_/api/v1/bundles/42?range=24h' \
  -H 'Authorization: Bearer $ZPE_TOKEN'
200 application/json
{
  "id": 0,
  "name": "string",
  "description": null,
  "icon": null,
  "accent": "orange",
  "archived_at": null,
  "created_via": null,
  "created_by": "string",
  "created_at": 0,
  "updated_at": 0,
  "link_count": 0,
  "total_clicks": 0,
  "delta_pct": 0,
  "sparkline": [
    0
  ],
  "top_links": [
    {
      "slug": "string",
      "click_count": 0
    }
  ]
}

The example is generated from the schema in the contract: the shape is the document's, the values are placeholders.

PUT /_/api/v1/bundles/{id} Update a bundle putBundlesById

Owner only. Refused with 403 for any other identity.

Parameters

nameinrequiredtypenotes
idpathyesinteger

Request body application/json · optional

fieldtyperequirednotes
namestringno
descriptionstring | nullno
iconstring | nullno
accent"orange" | "red" | "green" | "blue" | "purple"no

Responses

statusmeaningbody
200Updated.object
400Validation error.object
401Missing or invalid bearer token.object
403Refused. Either the key lacks the `create` scope, or the caller is not the bundle owner. Ownership is fixed at creation and is not transferable: only the identity that created a bundle can change it. Reading a bundle, its links and its analytics is open to every authenticated key.object
404Not found.object

Example

Update a bundle, with a token from the door.

PUT /_/api/v1/bundles/{id}
curl -X PUT 'https://zpe.vc/links/_/api/v1/bundles/42' \
  -H 'Authorization: Bearer $ZPE_TOKEN' \
  -H 'content-type: application/json' \
  -d '{
  "name": "string",
  "description": null,
  "icon": null,
  "accent": "orange"
}'
200 application/json
{
  "id": 0,
  "name": "string",
  "description": null,
  "icon": null,
  "accent": "orange",
  "archived_at": null,
  "created_via": null,
  "created_by": "string",
  "created_at": 0,
  "updated_at": 0
}

The example is generated from the schema in the contract: the shape is the document's, the values are placeholders.

DELETE /_/api/v1/bundles/{id} Delete a bundle permanently deleteBundlesById

Owner only. Unlike a link, a bundle carries no zero-click rule: a bundle is a grouping, not an address, so deleting one drops the memberships and leaves every member link and its click history in place. Archive instead when you only want it out of the default listing.

Parameters

nameinrequiredtypenotes
idpathyesinteger

Request body

None: this operation takes its arguments in the path and the query.

Responses

statusmeaningbody
200Deleted.object
401Missing or invalid bearer token.object
403Refused. Either the key lacks the `create` scope, or the caller is not the bundle owner. Ownership is fixed at creation and is not transferable: only the identity that created a bundle can change it. Reading a bundle, its links and its analytics is open to every authenticated key.object
404Not found.object

Example

Delete a bundle permanently, with a token from the door.

DELETE /_/api/v1/bundles/{id}
curl -X DELETE 'https://zpe.vc/links/_/api/v1/bundles/42' \
  -H 'Authorization: Bearer $ZPE_TOKEN'
200 application/json
{
  "deleted": false
}

The example is generated from the schema in the contract: the shape is the document's, the values are placeholders.

POST /_/api/v1/bundles/{id}/archive Archive a bundle postBundlesByIdArchive

Owner only. Hides the bundle from the default listing without deleting anything. Reversible through unarchive.

Parameters

nameinrequiredtypenotes
idpathyesinteger

Request body

None: this operation takes its arguments in the path and the query.

Responses

statusmeaningbody
200Archived.object
401Missing or invalid bearer token.object
403Refused. Either the key lacks the `create` scope, or the caller is not the bundle owner. Ownership is fixed at creation and is not transferable: only the identity that created a bundle can change it. Reading a bundle, its links and its analytics is open to every authenticated key.object
404Not found.object

Example

Archive a bundle, with a token from the door.

POST /_/api/v1/bundles/{id}/archive
curl -X POST 'https://zpe.vc/links/_/api/v1/bundles/42/archive' \
  -H 'Authorization: Bearer $ZPE_TOKEN'
200 application/json
{
  "id": 0,
  "name": "string",
  "description": null,
  "icon": null,
  "accent": "orange",
  "archived_at": null,
  "created_via": null,
  "created_by": "string",
  "created_at": 0,
  "updated_at": 0
}

The example is generated from the schema in the contract: the shape is the document's, the values are placeholders.

POST /_/api/v1/bundles/{id}/unarchive Unarchive a bundle postBundlesByIdUnarchive

Owner only. Returns an archived bundle to the default listing.

Parameters

nameinrequiredtypenotes
idpathyesinteger

Request body

None: this operation takes its arguments in the path and the query.

Responses

statusmeaningbody
200Unarchived.object
401Missing or invalid bearer token.object
403Refused. Either the key lacks the `create` scope, or the caller is not the bundle owner. Ownership is fixed at creation and is not transferable: only the identity that created a bundle can change it. Reading a bundle, its links and its analytics is open to every authenticated key.object
404Not found.object

Example

Unarchive a bundle, with a token from the door.

POST /_/api/v1/bundles/{id}/unarchive
curl -X POST 'https://zpe.vc/links/_/api/v1/bundles/42/unarchive' \
  -H 'Authorization: Bearer $ZPE_TOKEN'
200 application/json
{
  "id": 0,
  "name": "string",
  "description": null,
  "icon": null,
  "accent": "orange",
  "archived_at": null,
  "created_via": null,
  "created_by": "string",
  "created_at": 0,
  "updated_at": 0
}

The example is generated from the schema in the contract: the shape is the document's, the values are placeholders.

GET /_/api/v1/bundles/{id}/analytics Get bundle analytics. Defaults to all-time if no range given. getBundlesByIdAnalytics

Get bundle analytics. Defaults to all-time if no range given.

Parameters

nameinrequiredtypenotes
idpathyesinteger
rangequeryno"24h" | "7d" | "30d" | "90d" | "1y" | "all"Time range for analytics. Defaults are documented per endpoint.

Request body

None: this operation takes its arguments in the path and the query.

Responses

statusmeaningbody
200OK.object
400Validation error.object
401Missing or invalid bearer token.object
403Scope insufficient.object
404Not found.object

Example

Get bundle analytics. Defaults to all-time if no range given., with a token from the door.

GET /_/api/v1/bundles/{id}/analytics
curl -X GET 'https://zpe.vc/links/_/api/v1/bundles/42/analytics?range=24h' \
  -H 'Authorization: Bearer $ZPE_TOKEN'
200 application/json
{
  "total_clicks": 0,
  "countries": [
    {
      "name": "string",
      "count": 0
    }
  ],
  "referrers": [
    {
      "name": "string",
      "count": 0
    }
  ],
  "referrer_hosts": [
    {
      "name": "string",
      "count": 0
    }
  ],
  "devices": [
    {
      "name": "string",
      "count": 0
    }
  ],
  "os": [
    {
      "name": "string",
      "count": 0
    }
  ],
  "browsers": [
    {
      "name": "string",
      "count": 0
    }
  ],
  "link_modes": [
    {
      "name": "string",
      "count": 0
    }
  ],
  "channels": [
    {
      "name": "string",
      "count": 0
    }
  ],
  "clicks_over_time": [
    {
      "date": "string",
      "count": 0
    }
  ],
  "slug_clicks": [
    {
      "slug": "string",
      "count": 0
    }
  ],
  "num_countries": 0,
  "num_referrers": 0,
  "num_referrer_hosts": 0,
  "num_os": 0,
  "num_browsers": 0
}

The example is generated from the schema in the contract: the shape is the document's, the values are placeholders.

GET /_/api/v1/bundles/{id}/breakdown Page through a bundle's countries, sources, or domains breakdown getBundlesByIdBreakdown

Page through a bundle's countries, sources, or domains breakdown

Parameters

nameinrequiredtypenotes
idpathyesinteger
dimensionqueryyes"countries" | "referrers" | "referrer_hosts"Which panel to page through: countries, referrers (sources), or referrer_hosts (domains).
limitquerynointegerPage size, 1-100. Defaults to 10.
offsetquerynointeger | nullRow offset into the ranked list. Defaults to 0.
rangequeryno"24h" | "7d" | "30d" | "90d" | "1y" | "all"Time range. Defaults to all-time.

Request body

None: this operation takes its arguments in the path and the query.

Responses

statusmeaningbody
200OK.object
400Validation error.object
401Missing or invalid bearer token.object
403Scope insufficient.object
404Not found.object

Example

Page through a bundle's countries, sources, or domains breakdown, with a token from the door.

GET /_/api/v1/bundles/{id}/breakdown
curl -X GET 'https://zpe.vc/links/_/api/v1/bundles/42/breakdown?dimension=countries&limit=0&offset=<offset>&range=24h' \
  -H 'Authorization: Bearer $ZPE_TOKEN'
200 application/json
{
  "items": [
    {
      "name": "string",
      "count": 0
    }
  ],
  "total": 0
}

The example is generated from the schema in the contract: the shape is the document's, the values are placeholders.

DELETE /_/api/v1/bundles/{id}/links/{linkId} Remove a link from a bundle deleteBundlesByIdLinksByLinkId

Bundle owner only, whoever owns the link. Owning the link does not grant removal from someone else's bundle. The link itself is not deleted.

Parameters

nameinrequiredtypenotes
idpathyesinteger
linkIdpathyesinteger

Request body

None: this operation takes its arguments in the path and the query.

Responses

statusmeaningbody
200Removed.object
401Missing or invalid bearer token.object
403Refused. Either the key lacks the `create` scope, or the caller is not the bundle owner. Ownership is fixed at creation and is not transferable: only the identity that created a bundle can change it. Reading a bundle, its links and its analytics is open to every authenticated key.object
404Not found.object

Example

Remove a link from a bundle, with a token from the door.

DELETE /_/api/v1/bundles/{id}/links/{linkId}
curl -X DELETE 'https://zpe.vc/links/_/api/v1/bundles/42/links/17' \
  -H 'Authorization: Bearer $ZPE_TOKEN'
200 application/json
{
  "removed": false
}

The example is generated from the schema in the contract: the shape is the document's, the values are placeholders.

GET /_/api/v1/pages List the pages this key can write getPages

Every name the key's owner holds that carries a page, with its blocks. An account holds at most five pages, so this is one page of results and takes no paging parameters.

Parameters

No parameters.

Request body

None: this operation takes its arguments in the path and the query.

Responses

statusmeaningbody
200The owner's pages.array of object
401Missing or invalid bearer token.object
403The key's scope does not allow this verb.object
404No such page for this key's owner.object
409The name is retired.object
422The request is well formed and the page cannot honour it.object

Example

List the pages this key can write, with a token from the door.

GET /_/api/v1/pages
curl -X GET 'https://zpe.vc/links/_/api/v1/pages' \
  -H 'Authorization: Bearer $ZPE_TOKEN'
200 application/json
[
  {
    "name": "string",
    "headline": "string",
    "bio": "string",
    "email": null,
    "theme": "ink",
    "accent": "string",
    "created_at": 0,
    "updated_at": 0,
    "blocks": [
      {
        "id": 0,
        "name": "string",
        "position": 0,
        "kind": "link",
        "title": "string",
        "subtitle": null,
        "href": null,
        "app_id": null,
        "r2_key": null,
        "alt": null,
        "body": null,
        "is_first": false,
        "visible": false,
        "created_at": 0,
        "updated_at": 0
      }
    ]
  }
]

The example is generated from the schema in the contract: the shape is the document's, the values are placeholders.

GET /_/api/v1/pages/{name} Read one page, and its numbers getPagesByName

The page's fields and every block, in the order a visitor would see them, plus the report the editor draws: views, taps per block, the answers each question holds, the acts the page performs, and the countries and hosts the visits came from. Both doors read the report from one grouped read, so the JSON and the tab cannot disagree. Bots are stored and excluded from every count.

Parameters

nameinrequiredtypenotes
namepathyesstring

Request body

None: this operation takes its arguments in the path and the query.

Responses

statusmeaningbody
200The page, with its report.object
401Missing or invalid bearer token.object
403The key's scope does not allow this verb.object
404No such page for this key's owner.object
409The name is retired.object
422The request is well formed and the page cannot honour it.object

Example

Read one page, and its numbers, with a token from the door.

GET /_/api/v1/pages/{name}
curl -X GET 'https://zpe.vc/links/_/api/v1/pages/string' \
  -H 'Authorization: Bearer $ZPE_TOKEN'
200 application/json
{
  "name": "string",
  "headline": "string",
  "bio": "string",
  "email": null,
  "theme": "ink",
  "accent": "string",
  "created_at": 0,
  "updated_at": 0,
  "blocks": [
    {
      "id": 0,
      "name": "string",
      "position": 0,
      "kind": "link",
      "title": "string",
      "subtitle": null,
      "href": null,
      "app_id": null,
      "r2_key": null,
      "alt": null,
      "body": null,
      "is_first": false,
      "visible": false,
      "created_at": 0,
      "updated_at": 0
    }
  ],
  "report": {
    "views": 0,
    "taps": 0,
    "intents": 3,
    "acts": {
      "card": 0,
      "email": 0,
      "share": 0,
      "submit": 0
    },
    "blocks": [
      {
        "id": 7,
        "title": "Book a call",
        "taps": 4,
        "answers": 3,
        "on_page": false
      }
    ],
    "rules": [
      {
        "id": 3,
        "kind": "host",
        "value": "instagram.com",
        "taps": 5,
        "on_page": false
      }
    ],
    "countries": [
      {
        "value": "ID",
        "count": 12
      }
    ],
    "hosts": [
      {
        "value": "ID",
        "count": 12
      }
    ],
    "bot_views": 0
  }
}

The example is generated from the schema in the contract: the shape is the document's, the values are placeholders.

PUT /_/api/v1/pages/{name} Write a page's fields putPagesByName

The one line, the longer note, the address and the look. The first save creates the page row, which is what publishing requires.

Parameters

nameinrequiredtypenotes
namepathyesstring

Request body application/json · optional

fieldtyperequirednotes
headlinestringno
biostringno
emailstring | nullno
theme"ink" | "paper" | "midnight" | "forest" | "cocoa" | "sand" | "mono" | "rosewater" | "neon"no
accent"ember" | "lime" | "mint" | "sky" | "violet" | "rose" | "gold" | "slate"no

Responses

statusmeaningbody
200The page as written.object
401Missing or invalid bearer token.object
403The key's scope does not allow this verb.object
404No such page for this key's owner.object
409The name is retired.object
422The request is well formed and the page cannot honour it.object

Example

Write a page's fields, with a token from the door.

PUT /_/api/v1/pages/{name}
curl -X PUT 'https://zpe.vc/links/_/api/v1/pages/string' \
  -H 'Authorization: Bearer $ZPE_TOKEN' \
  -H 'content-type: application/json' \
  -d '{
  "headline": "string",
  "bio": "string",
  "email": null,
  "theme": "ink",
  "accent": "ember"
}'
200 application/json
{
  "name": "string",
  "headline": "string",
  "bio": "string",
  "email": null,
  "theme": "ink",
  "accent": "string",
  "created_at": 0,
  "updated_at": 0,
  "blocks": [
    {
      "id": 0,
      "name": "string",
      "position": 0,
      "kind": "link",
      "title": "string",
      "subtitle": null,
      "href": null,
      "app_id": null,
      "r2_key": null,
      "alt": null,
      "body": null,
      "is_first": false,
      "visible": false,
      "created_at": 0,
      "updated_at": 0
    }
  ]
}

The example is generated from the schema in the contract: the shape is the document's, the values are placeholders.

GET /_/api/v1/pages/{name}/submissions Read a page's answers getPagesByNameSubmissions

Every answer the page's questions collected in the last 90 days, newest first, each with the block it answers and that block's title as it reads now. The window and the bot rule are the report's own, so a block's `answers` figure and this list count the same rows. An answer whose block was since removed is still listed, with a null title: the answer outlives its question by design.

Parameters

nameinrequiredtypenotes
namepathyesstring

Request body

None: this operation takes its arguments in the path and the query.

Responses

statusmeaningbody
200The answers in the window, newest first.object
401Missing or invalid bearer token.object
403The key's scope does not allow this verb.object
404No such page for this key's owner.object
409The name is retired.object
422The request is well formed and the page cannot honour it.object

Example

Read a page's answers, with a token from the door.

GET /_/api/v1/pages/{name}/submissions
curl -X GET 'https://zpe.vc/links/_/api/v1/pages/string/submissions' \
  -H 'Authorization: Bearer $ZPE_TOKEN'
200 application/json
{
  "answers": [
    {
      "id": 12,
      "block_id": 7,
      "block_title": "Ask me anything",
      "value": "hello@example.test",
      "at": 0,
      "country": "TH"
    }
  ]
}

The example is generated from the schema in the contract: the shape is the document's, the values are placeholders.

POST /_/api/v1/pages/{name}/blocks Add a block postPagesByNameBlocks

A page holds 12 blocks. The first ask is exclusive: adding a block with is_first clears whichever one held it.

Parameters

nameinrequiredtypenotes
namepathyesstring

Request body application/json · optional

fieldtyperequirednotes
kind"link" | "app" | "form" | "image" | "text"no
titlestringno
subtitlestringno
hrefstringno
app_idstringno
field"email" | "text" | "tel"no
labelstringno
consentstringno
r2_keystringno
altstringno
bodystringno
is_firstbooleanno
visiblebooleanno

Responses

statusmeaningbody
201The block as written.object
401Missing or invalid bearer token.object
403The key's scope does not allow this verb.object
404No such page for this key's owner.object
409The name is retired.object
422The request is well formed and the page cannot honour it.object

Example

Add a block, with a token from the door.

POST /_/api/v1/pages/{name}/blocks
curl -X POST 'https://zpe.vc/links/_/api/v1/pages/string/blocks' \
  -H 'Authorization: Bearer $ZPE_TOKEN' \
  -H 'content-type: application/json' \
  -d '{
  "kind": "link",
  "title": "string",
  "subtitle": "string",
  "href": "string",
  "app_id": "string",
  "field": "email",
  "label": "string",
  "consent": "string",
  "r2_key": "string",
  "alt": "string",
  "body": "string",
  "is_first": false,
  "visible": false
}'
201 application/json
{
  "id": 0,
  "name": "string",
  "position": 0,
  "kind": "link",
  "title": "string",
  "subtitle": null,
  "href": null,
  "app_id": null,
  "r2_key": null,
  "alt": null,
  "body": null,
  "is_first": false,
  "visible": false,
  "created_at": 0,
  "updated_at": 0
}

The example is generated from the schema in the contract: the shape is the document's, the values are placeholders.

PATCH /_/api/v1/pages/{name}/blocks/{id} Change one block patchPagesByNameBlocksById

Only the keys sent are written. The kind cannot change. Hiding the first ask clears it, because an ask nobody can see is not an ask.

Parameters

nameinrequiredtypenotes
idpathyesinteger | null
namepathyesstring

Request body application/json · optional

fieldtyperequirednotes
kind"link" | "app" | "form" | "image" | "text"no
titlestringno
subtitlestringno
hrefstringno
app_idstringno
field"email" | "text" | "tel"no
labelstringno
consentstringno
r2_keystringno
altstringno
bodystringno
is_firstbooleanno
visiblebooleanno

Responses

statusmeaningbody
200The block as written.object
401Missing or invalid bearer token.object
403The key's scope does not allow this verb.object
404No such page for this key's owner.object
409The name is retired.object
422The request is well formed and the page cannot honour it.object

Example

Change one block, with a token from the door.

PATCH /_/api/v1/pages/{name}/blocks/{id}
curl -X PATCH 'https://zpe.vc/links/_/api/v1/pages/string/blocks/{id}' \
  -H 'Authorization: Bearer $ZPE_TOKEN' \
  -H 'content-type: application/json' \
  -d '{
  "kind": "link",
  "title": "string",
  "subtitle": "string",
  "href": "string",
  "app_id": "string",
  "field": "email",
  "label": "string",
  "consent": "string",
  "r2_key": "string",
  "alt": "string",
  "body": "string",
  "is_first": false,
  "visible": false
}'
200 application/json
{
  "id": 0,
  "name": "string",
  "position": 0,
  "kind": "link",
  "title": "string",
  "subtitle": null,
  "href": null,
  "app_id": null,
  "r2_key": null,
  "alt": null,
  "body": null,
  "is_first": false,
  "visible": false,
  "created_at": 0,
  "updated_at": 0
}

The example is generated from the schema in the contract: the shape is the document's, the values are placeholders.

DELETE /_/api/v1/pages/{name}/blocks/{id} Remove one block deletePagesByNameBlocksById

The taps a block earned are kept: the report says what happened, and a removed block does not un-happen it.

Parameters

nameinrequiredtypenotes
idpathyesinteger | null
namepathyesstring

Request body

None: this operation takes its arguments in the path and the query.

Responses

statusmeaningbody
204Removed.
401Missing or invalid bearer token.object
403The key's scope does not allow this verb.object
404No such page for this key's owner.object
409The name is retired.object
422The request is well formed and the page cannot honour it.object

Example

Remove one block, with a token from the door.

DELETE /_/api/v1/pages/{name}/blocks/{id}
curl -X DELETE 'https://zpe.vc/links/_/api/v1/pages/string/blocks/{id}' \
  -H 'Authorization: Bearer $ZPE_TOKEN'

The example is generated from the schema in the contract: the shape is the document's, the values are placeholders.

POST /_/api/v1/pages/{name}/publish Publish a page postPagesByNamePublish

The name starts answering at its address with no destination beside it. Refused when the page was never written or has nothing visible.

Parameters

nameinrequiredtypenotes
namepathyesstring

Request body

None: this operation takes its arguments in the path and the query.

Responses

statusmeaningbody
200The name now answers.object
401Missing or invalid bearer token.object
403The key's scope does not allow this verb.object
404No such page for this key's owner.object
409The name is retired.object
422The request is well formed and the page cannot honour it.object

Example

Publish a page, with a token from the door.

POST /_/api/v1/pages/{name}/publish
curl -X POST 'https://zpe.vc/links/_/api/v1/pages/string/publish' \
  -H 'Authorization: Bearer $ZPE_TOKEN'
200 application/json
{
  "name": "string",
  "state": "claimed",
  "published": false
}

The example is generated from the schema in the contract: the shape is the document's, the values are placeholders.

POST /_/api/v1/pages/{name}/unpublish Unpublish a page postPagesByNameUnpublish

The address stops answering and the page is left whole, so publishing again brings the same page back.

Parameters

nameinrequiredtypenotes
namepathyesstring

Request body

None: this operation takes its arguments in the path and the query.

Responses

statusmeaningbody
200The name stopped answering.object
401Missing or invalid bearer token.object
403The key's scope does not allow this verb.object
404No such page for this key's owner.object
409The name is retired.object
422The request is well formed and the page cannot honour it.object

Example

Unpublish a page, with a token from the door.

POST /_/api/v1/pages/{name}/unpublish
curl -X POST 'https://zpe.vc/links/_/api/v1/pages/string/unpublish' \
  -H 'Authorization: Bearer $ZPE_TOKEN'
200 application/json
{
  "name": "string",
  "state": "claimed",
  "published": false
}

The example is generated from the schema in the contract: the shape is the document's, the values are placeholders.