API docs

Links API

mount /links · base https://zpe.vc/links · v3.0.0 · 56 operations · 31 write · 0 need no account · stable

the document this app serves now. Contract: https://zpe.vc/links/_/api/v1/openapi.json · sha256 5853a3d6f253

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.

GET /new The intake page getNew

**Public.** The form a stranger opens at `zpe.vc/links/new`: the markup, the caps and the script that posts the drop. No key and no session — this is the one address a person who has never heard of the door is sent to. Any other method on this path is the intake's own 404 page, not a second operation.

Parameters

No parameters.

Request body

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

Responses

statusmeaningbody
200The intake page.string

Example

The intake page, with a token from the door.

GET /new
curl -X GET 'https://zpe.vc/links/new' \
  -H 'Authorization: Bearer $ZPE_TOKEN'
200 text/html
"string"

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

GET /new/api/form The form was opened (the dwell anchor's row) getNewApiForm

**Public.** The page calls this once when the form first paints. The call writes the dwell anchor's row (a salted hash of the caller, and the first time it was seen) and answers the caps the page shows: the dwell floor, the per-hour send cap and the text cap. It reads nothing back and refuses nothing.

Parameters

No parameters.

Request body

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

Responses

statusmeaningbody
200The caps, and when the anchor was first seen.object

Example

The form was opened (the dwell anchor's row), with a token from the door.

GET /new/api/form
curl -X GET 'https://zpe.vc/links/new/api/form' \
  -H 'Authorization: Bearer $ZPE_TOKEN'
200 application/json
{}

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

POST /new/api/submit One drop: the answers and the files in one body postNewApiSubmit

**Public.** The only write a stranger makes. The browser posts the form (multipart) and a script posts JSON; both reach the same fields and the same file walk. A browser is answered `303` to its receipt page; a script is answered `201` with `{receipt, status}`. A replayed drop under the dedupe window answers `201` with the SAME receipt and writes no second row. Every refusal (`413` cap, `429` per-address, `422` fields) is the app's `{ok:false, code, errors}` body.

Parameters

No parameters.

Request body multipart/form-data · required

The document declares no properties for this body.

Responses

statusmeaningbody
201The drop was stored. The body carries the receipt and the build's status.object
303A browser's drop: the receipt page, which is where the person waits.
413The body, one file, or the store's whole submission cap was exceeded.object
422A field was missing or over its limit; the body names which.object
429The caller's address is over the per-hour send cap.object

Example

One drop: the answers and the files in one body, with a token from the door.

POST /new/api/submit
curl -X POST 'https://zpe.vc/links/new/api/submit' \
  -H 'Authorization: Bearer $ZPE_TOKEN' \
  -H 'content-type: application/json' \
  -d '{}'
201 application/json
{}

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

POST /new/api/{receipt}/files The receipt holder adds files after the drop postNewApiByReceiptFiles

**Public, with the receipt as the credential.** The receipt page's file picker posts here. The receipt names the build and nothing else authorises the write: a person holding the eight characters is the person the drop belongs to. A file already stored under the same digest is a duplicate and is not written twice.

Parameters

nameinrequiredtypenotes
receiptpathyesstringThe eight-character receipt the submit answered with. It is the only key a stranger holds.

Request body multipart/form-data · required

The document declares no properties for this body.

Responses

statusmeaningbody
200What was stored, what was a duplicate, and what was skipped and why.object
404No build carries that receipt.object
413The file, the part count, or the submission's byte cap was exceeded.object
422No file in the batch could be read.object

Example

The receipt holder adds files after the drop, with a token from the door.

POST /new/api/{receipt}/files
curl -X POST 'https://zpe.vc/links/new/api/string/files' \
  -H 'Authorization: Bearer $ZPE_TOKEN' \
  -H 'content-type: application/json' \
  -d '{}'
200 application/json
{}

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

GET /new/api/{receipt}/content The assembled object (key) getNewApiByReceiptContent

**Key.** The runner reads what the assembler wrote: the extracted content, the build's status and when it moved. Refused with `401` without an `Authorization: Bearer` account key granted `zpe-links`, and with `403` when the grant is read-only.

Parameters

nameinrequiredtypenotes
receiptpathyesstringThe eight-character receipt the submit answered with. It is the only key a stranger holds.

Request body

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

Responses

statusmeaningbody
200The assembled object and the build's status.object
401No `Authorization` header, or one the door refuses. The path needs the app's own account key.object
403The key is valid and its grant does not allow this verb.object
404No build carries that receipt.object

Example

The assembled object (key), with a token from the door.

GET /new/api/{receipt}/content
curl -X GET 'https://zpe.vc/links/new/api/string/content' \
  -H 'Authorization: Bearer $ZPE_TOKEN'
200 application/json
{}

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

POST /new/api/{receipt}/content The assembler writes the object, once (key) postNewApiByReceiptContent

**Key.** The one write that puts the assembled object on the build. It is written once: a second write to a build that already holds content is refused rather than merged.

Parameters

nameinrequiredtypenotes
receiptpathyesstringThe eight-character receipt the submit answered with. It is the only key a stranger holds.

Request body application/json · required

The document declares no properties for this body.

Responses

statusmeaningbody
200Written. The body carries the build's new status.object
401No `Authorization` header, or one the door refuses. The path needs the app's own account key.object
403The key is valid and its grant does not allow this verb.object
404No build carries that receipt.object
409The build is past the state where content can be written.object
413The object was over the record's byte cap.object

Example

The assembler writes the object, once (key), with a token from the door.

POST /new/api/{receipt}/content
curl -X POST 'https://zpe.vc/links/new/api/string/content' \
  -H 'Authorization: Bearer $ZPE_TOKEN' \
  -H 'content-type: application/json' \
  -d '{}'
200 application/json
{}

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

GET /new/api/{receipt}/message The held message, its sign-off and its attempts (key) getNewApiByReceiptMessage

**Key.** Reads the draft that is held for this build: the subject and body, the recipient, the latest sign-off and every send attempt so far. A caller that only wants to see the words can stop here — nothing is sent by a read.

Parameters

nameinrequiredtypenotes
receiptpathyesstringThe eight-character receipt the submit answered with. It is the only key a stranger holds.

Request body

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

Responses

statusmeaningbody
200The held message, its sign-off and its attempts.object
401No `Authorization` header, or one the door refuses. The path needs the app's own account key.object
403The key is valid and its grant does not allow this verb.object
404This build holds no message yet.object

Example

The held message, its sign-off and its attempts (key), with a token from the door.

GET /new/api/{receipt}/message
curl -X GET 'https://zpe.vc/links/new/api/string/message' \
  -H 'Authorization: Bearer $ZPE_TOKEN'
200 application/json
{}

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

POST /new/api/{receipt}/message Write the held message for a row already read (key) postNewApiByReceiptMessage

**Key.** Writes the draft the sender will read. A second post to a build that already holds a message is refused: the draft is written once, and a correction is a new build rather than a quiet rewrite of words somebody may have signed.

Parameters

nameinrequiredtypenotes
receiptpathyesstringThe eight-character receipt the submit answered with. It is the only key a stranger holds.

Request body application/json · required

The document declares no properties for this body.

Responses

statusmeaningbody
200The message the build now holds.object
401No `Authorization` header, or one the door refuses. The path needs the app's own account key.object
403The key is valid and its grant does not allow this verb.object
404No build carries that receipt.object
409The build already holds a message, or its recipient is not settled.object
422The message was missing a field the composer needs.object

Example

Write the held message for a row already read (key), with a token from the door.

POST /new/api/{receipt}/message
curl -X POST 'https://zpe.vc/links/new/api/string/message' \
  -H 'Authorization: Bearer $ZPE_TOKEN' \
  -H 'content-type: application/json' \
  -d '{}'
200 application/json
{}

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

POST /new/api/{receipt}/message/sign Record a sign-off naming the body and the recipient (key) postNewApiByReceiptMessageSign

**Key.** Records that a person read this exact draft, addressed to this exact recipient: the sign-off names the draft's hash, so a body that moved after the signature no longer matches it. `release` refuses without one.

Parameters

nameinrequiredtypenotes
receiptpathyesstringThe eight-character receipt the submit answered with. It is the only key a stranger holds.

Request body application/json · required

The document declares no properties for this body.

Responses

statusmeaningbody
200The sign-off that is now the newest for this message.object
400The sign-off named nobody as the reader.object
401No `Authorization` header, or one the door refuses. The path needs the app's own account key.object
403The key is valid and its grant does not allow this verb.object
404This build holds no message to sign.object
409The message has no recipient, so there is nothing to sign for.object

Example

Record a sign-off naming the body and the recipient (key), with a token from the door.

POST /new/api/{receipt}/message/sign
curl -X POST 'https://zpe.vc/links/new/api/string/message/sign' \
  -H 'Authorization: Bearer $ZPE_TOKEN' \
  -H 'content-type: application/json' \
  -d '{}'
200 application/json
{}

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

POST /new/api/{receipt}/release The app's only send (key, and refused without a sign-off) postNewApiByReceiptRelease

**Key.** The one operation in this app that sends mail. It refuses unless the held message carries a sign-off whose draft hash still matches, so a caller cannot post words nobody read. The refusal codes are the contract `tools/mail-release.mjs` turns into exit codes — `409` is 'nobody signed this', not 'the mail service refused it'.

Parameters

nameinrequiredtypenotes
receiptpathyesstringThe eight-character receipt the submit answered with. It is the only key a stranger holds.

Request body

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

Responses

statusmeaningbody
200The send was attempted; the body carries its outcome.object
401No `Authorization` header, or one the door refuses. The path needs the app's own account key.object
403The key is valid and its grant does not allow this verb.object
404This build holds no message to send.object
409The message is unsigned, or its sign-off names a body that has since moved.object
503The mail service or the store could not be reached; nothing was sent.object

Example

The app's only send (key, and refused without a sign-off), with a token from the door.

POST /new/api/{receipt}/release
curl -X POST 'https://zpe.vc/links/new/api/string/release' \
  -H 'Authorization: Bearer $ZPE_TOKEN'
200 application/json
{}

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

GET /new/r/{receipt} The page the submitter waits on getNewRByReceipt

**Public, with the receipt as the credential.** The receipt page: what was received, the files, the held message's state, the address box when the record holds no address, and the confirm link. A receipt nobody holds answers the same page at `404`, so the answer never says whether a receipt exists.

Parameters

nameinrequiredtypenotes
receiptpathyesstringThe eight-character receipt the submit answered with. It is the only key a stranger holds.

Request body

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

Responses

statusmeaningbody
200The receipt page.string
404The same page, saying it is not on this server.string

Example

The page the submitter waits on, with a token from the door.

GET /new/r/{receipt}
curl -X GET 'https://zpe.vc/links/new/r/string' \
  -H 'Authorization: Bearer $ZPE_TOKEN'
200 text/html
"string"

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

POST /new/r/{receipt}/claim Keep the published page, once, for the door's principal postNewRByReceiptClaim

**Door.** Unreachable without the door's session: the receipt opens the record, and only a signed-in principal can become its owner — answered `401` with no session, and `503` when the door itself cannot be reached (which is not a refusal, because the person may well be signed in). A browser is sent back to the receipt page with `303`; a script is told `{ok:true, receipt, owner_principal}`. Only a published row nobody has kept can be claimed, once.

Parameters

nameinrequiredtypenotes
receiptpathyesstringThe eight-character receipt the submit answered with. It is the only key a stranger holds.

Request body

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

Responses

statusmeaningbody
200The row now belongs to the caller's principal.object
303A browser's claim: back to the receipt page.
401No door session.object
404No build carries that receipt.object
409The row is not published, or somebody already owns it.object
503The door could not be reached, so the claim was not decided.object

Example

Keep the published page, once, for the door's principal, with a token from the door.

POST /new/r/{receipt}/claim
curl -X POST 'https://zpe.vc/links/new/r/string/claim' \
  -H 'Authorization: Bearer $ZPE_TOKEN'
200 application/json
{}

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

POST /new/r/{receipt}/email The address the page asks for when the record holds none postNewRByReceiptEmail

**Public, with the receipt as the credential.** The person says where their link should go. This recomposes the held draft and writes nothing anybody can send: the message still waits for a read and a sign-off. A browser is sent back to the receipt page with `303`.

Parameters

nameinrequiredtypenotes
receiptpathyesstringThe eight-character receipt the submit answered with. It is the only key a stranger holds.

Request body application/x-www-form-urlencoded · required

The document declares no properties for this body.

Responses

statusmeaningbody
303The address was recorded; back to the receipt page.
404No build carries that receipt.string
409The message is already signed, or the row is past the ask.string
422That is not an address the app can write to.string

Example

The address the page asks for when the record holds none, with a token from the door.

POST /new/r/{receipt}/email
curl -X POST 'https://zpe.vc/links/new/r/string/email' \
  -H 'Authorization: Bearer $ZPE_TOKEN' \
  -H 'content-type: application/json' \
  -d '{}'

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

GET /new/r/{receipt}/confirm The correction page getNewRByReceiptConfirm

**Public, with the receipt as the credential.** The page that lets the person fix a field before the press. It is a read: nothing is confirmed until the POST below.

Parameters

nameinrequiredtypenotes
receiptpathyesstringThe eight-character receipt the submit answered with. It is the only key a stranger holds.

Request body

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

Responses

statusmeaningbody
200The correction page.string
404No build carries that receipt.string
409The build is closed; the page says so.string

Example

The correction page, with a token from the door.

GET /new/r/{receipt}/confirm
curl -X GET 'https://zpe.vc/links/new/r/string/confirm' \
  -H 'Authorization: Bearer $ZPE_TOKEN'
200 text/html
"string"

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

POST /new/r/{receipt}/confirm The person's press, which is what confirms postNewRByReceiptConfirm

**Public, with the receipt as the credential.** The only act that moves a build to `confirmed`: the page's press, with the corrected fields. It answers `303` back to the receipt page, which is where the published address appears.

Parameters

nameinrequiredtypenotes
receiptpathyesstringThe eight-character receipt the submit answered with. It is the only key a stranger holds.

Request body application/x-www-form-urlencoded · required

The document declares no properties for this body.

Responses

statusmeaningbody
303Confirmed; back to the receipt page.
404No build carries that receipt.string
409The build is closed, or somebody pressed first.string
422A corrected field was refused; the page says which.string

Example

The person's press, which is what confirms, with a token from the door.

POST /new/r/{receipt}/confirm
curl -X POST 'https://zpe.vc/links/new/r/string/confirm' \
  -H 'Authorization: Bearer $ZPE_TOKEN' \
  -H 'content-type: application/json' \
  -d '{}'

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

GET /new/r/{receipt}/f/{id} One stored file, streamed through the worker getNewRByReceiptFById

**Public, with the receipt as the credential.** The bytes of one file on this build, streamed from R2 through the worker so the bucket is never public. The receipt and the file id must belong together: a file id from somebody else's build is not a row and answers `404`.

Parameters

nameinrequiredtypenotes
idpathyesstring (uuid)The file row's UUID, as the receipt page links it.
receiptpathyesstringThe eight-character receipt the submit answered with. It is the only key a stranger holds.

Request body

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

Responses

statusmeaningbody
200The file's own bytes, at the type the row stored.string (binary)
404That file is not on this server.object

Example

One stored file, streamed through the worker, with a token from the door.

GET /new/r/{receipt}/f/{id}
curl -X GET 'https://zpe.vc/links/new/r/string/f/00000000-0000-4000-8000-000000000000' \
  -H 'Authorization: Bearer $ZPE_TOKEN'
200 application/octet-stream
"string"

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

GET /u/{slug} The published page getPublishedPage

**Public.** The product the intake exists for: the page a drop becomes after the press and the publish, at `zpe.vc/u/<name>`. No key and no session — this is the address the business prints. It answers at that mount only: measured 2026-11-06, `zpe.vc/u/<name>` is 200 and `zpe.vc/links/u/<name>` is 404, which is why this path item carries its own `servers`. A name that holds no live page answers the signpost's own `404`, never somebody else's page.

Parameters

nameinrequiredtypenotes
slugpathyesstringThe page's published name, which is also its address: `zpe.vc/u/<name>`.

Request body

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

Responses

statusmeaningbody
200The published page.string
404No live page holds that name.string

Example

The published page, with a token from the door.

GET /u/{slug}
curl -X GET 'https://zpe.vc/u/string' \
  -H 'Authorization: Bearer $ZPE_TOKEN'
200 text/html
"string"

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

GET /u/{slug}/card.vcf The published page's card, as a vCard getPublishedPageCard

**Public.** The page's 'save my card' act at `zpe.vc/u/<name>/card.vcf`: the page's own name, headline, address and contact as `text/vcard`, built from the same row the page reads. The read records its row before it answers, so the operator can see the card was taken.

Parameters

nameinrequiredtypenotes
slugpathyesstringThe page's published name, which is also its address: `zpe.vc/u/<name>`.

Request body

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

Responses

statusmeaningbody
200The card.string
404No live page holds that name.string

Example

The published page's card, as a vCard, with a token from the door.

GET /u/{slug}/card.vcf
curl -X GET 'https://zpe.vc/u/string/card.vcf' \
  -H 'Authorization: Bearer $ZPE_TOKEN'
200 text/vcard
"string"

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