API

Manage links, tags and collections and read click statistics over HTTP and JSON. The machine-readable description is openapi.yaml (OpenAPI 3.1).

Authentication

Create a key in the app under workspace settings, then API keys. The key is shown once. Send it on every request.

Authorization: Bearer wp_<prefix>_<secret>

A key belongs to one workspace and acts as a member of it. It can use the routes on this page and nothing else, so it cannot manage keys, members or domains. A revoked or malformed key answers 401 invalid_api_key.

Base URL and workspaces

Every request goes to https://dev.wayp.one. Routes are scoped by the workspace slug, which you see in the app's address bar: /api/v1/workspaces/{slug}/.... The slug must be that of the key's workspace. Any other slug answers 404 not_found.

The examples below assume these variables.

export WP=https://dev.wayp.one/api/v1/workspaces/acme
export KEY=wp_abcd2345_...

Errors

Every error has the same shape. Branch on code, show message to people.

{"error": {"code": "invalid_slug", "message": "A slug is 1 to 64 characters."}}
StatusMeaning
400The request is not valid, for example invalid_url, invalid_slug or invalid_cursor. An address the abuse checks refuse adds a reason.
401The key is missing or not valid.
402plan_limit. The workspace's plan does not allow more. The error also carries resource, limit and used.
403Only the creator of an item or a workspace admin can change it, or the workspace is suspended.
404Not found, or it belongs to another workspace.
409A conflict such as slug_taken.
429rate_limited. Wait the seconds in the Retry-After header.

Rate limits

Past a limit the response is 429 rate_limited with a Retry-After header. Plan limits, such as the number of links, are separate and answer 402 plan_limit.

Pagination

Lists of links take limit (1 to 100, default 50) and cursor. Each response has next_cursor, which is null on the last page. Pass it back unchanged as cursor. The cursor is opaque. Tags and collections come in one piece.

curl -H "Authorization: Bearer $KEY" "$WP/links?limit=2&sort=clicks"
curl -H "Authorization: Bearer $KEY" "$WP/links?limit=2&sort=clicks&cursor=NDI6MTA"

Links

GET /links takes q (slug, title, address or tag), tag, sort (created or clicks), limit and cursor. The response is {"links": [...], "next_cursor": ..., "usage": {"used", "limit"}}.

curl -H "Authorization: Bearer $KEY" "$WP/links?tag=docs&q=pricing"

Create

Only url is required. Without a slug one is generated. visibility is private, workspace (default) or public. domain places the link on one of the workspace's verified custom domains.

curl -X POST "$WP/links" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/pricing", "slug": "pricing", "title": "Pricing",
       "tags": ["marketing"], "visibility": "public"}'

The answer is 201 with the link: id, slug, short_url, url, title, description, visibility, status, expires_at, max_clicks, forward_query, tags, domain, clicks, created_by, created_at and updated_at.

Read, update, delete

PATCH changes only the fields you send. Send null for expires_at or max_clicks to clear them. Sending tags replaces the whole list.

curl -H "Authorization: Bearer $KEY" "$WP/links/42"
curl -X PATCH "$WP/links/42" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"title": "New title", "expires_at": "2027-01-01T00:00:00Z", "max_clicks": null}'
curl -X DELETE -H "Authorization: Bearer $KEY" "$WP/links/42"

Delete answers 204. GET /links/check-slug?slug=...&domain=... says whether a slug is free, and GET /links/search?q=... returns up to 25 ranked matches.

Tags

curl -H "Authorization: Bearer $KEY" "$WP/tags"
curl -X PATCH "$WP/tags/7" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"name": "launch"}'
curl -X DELETE -H "Authorization: Bearer $KEY" "$WP/tags/7"

The list is {"tags": [{"id", "name", "links"}]}. Deleting a tag removes it from its links and keeps the links.

Collections

A collection groups links in an order you choose. Create one, then add links by id. Every call that changes the links answers with the collection and its ordered links.

curl -X POST "$WP/collections" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"title": "Onboarding", "visibility": "workspace"}'

curl -X POST "$WP/collections/3/links" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"link_id": 42}'

Also available are GET /collections, GET, PATCH and DELETE /collections/{id}, PUT /collections/{id}/links with {"link_ids": [...]} to reorder, and DELETE /collections/{id}/links/{linkID}.

Statistics

Reports cover a window of days. from and to are YYYY-MM-DD (default: the last 30 days, at most 400), tz is an IANA zone such as Europe/Paris (default UTC) and include_bots=true counts bot clicks too.

curl -H "Authorization: Bearer $KEY" "$WP/stats?from=2026-10-01&to=2026-10-21"
curl -H "Authorization: Bearer $KEY" "$WP/links/42/stats?tz=Europe/Paris"

The response has totals, a zero-filled daily series, breakdowns by country, device, browser, os and referrer (top 10 each), and, for the workspace report, top_links.

CSV

Clicks

Raw clicks, oldest first, with the columns at,link,country,device,browser,os,referrer_host,bot. They take the same window parameters as the reports.

curl -H "Authorization: Bearer $KEY" -o clicks.csv "$WP/clicks.csv?from=2026-10-01"
curl -H "Authorization: Bearer $KEY" -o link-42.csv "$WP/links/42/clicks.csv"

Export links

One row per link the key can see. The header is slug,url,title,tags,visibility,domain,expires,max_clicks,created.

curl -H "Authorization: Bearer $KEY" -o links.csv "$WP/links.csv"

Import links

Import is two calls. Without commit=true the file is only checked and nothing is created. Send the CSV as the raw body, or as a multipart form field named file. format is auto (default), waypoint (the export above), shlink or slash.

# preview
curl -X POST "$WP/links/import?format=auto" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: text/csv" --data-binary @links.csv

# commit
curl -X POST "$WP/links/import?format=auto&commit=true" \
  -H "Authorization: Bearer $KEY" -F [email protected]

The answer is {"format", "total", "valid", "invalid", "imported", "rows": [...]}. imported appears after a commit. Each row has its line, slug, url, a status of ok, error or imported, and for an error {"code", "message", "reason"}. A file may have 5000 rows at most (400 too_many_rows) and is also capped in bytes (413).

Webhooks

Admins and owners add webhooks in workspace settings. Waypoint then posts JSON to your endpoint on link.created, link.updated, link.deleted and link.clicked. Clicks come batched: one delivery holds the clicks of one write, up to a second of them. The payloads are described under webhooks in the OpenAPI document.

Every delivery carries Waypoint-Event, Waypoint-Delivery and Waypoint-Signature: t=<unix>,v1=<hex>. To check it, compute HMAC-SHA256 with the signing secret over the timestamp, a dot and the raw body, compare it with v1 in constant time, and refuse a timestamp more than 5 minutes old.

// Node
const want = createHmac("sha256", secret).update(`${t}.`).update(rawBody).digest("hex");

Answer with any 2xx within 10 seconds. Anything else is retried 8 times over about a day, and 20 deliveries in a row that fail every try turn the webhook off and email the owners. Retries keep the event id, so use it to drop duplicates.

OpenAPI

The full description with every field, parameter and response is at https://dev.wayp.one/api/v1/openapi.yaml. It needs no key. Import it into a client generator or an API tool.