API
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."}}
| Status | Meaning |
|---|---|
| 400 | The request is not valid, for example invalid_url, invalid_slug or invalid_cursor. An address the abuse checks refuse adds a reason. |
| 401 | The key is missing or not valid. |
| 402 | plan_limit. The workspace's plan does not allow more. The error also carries resource, limit and used. |
| 403 | Only the creator of an item or a workspace admin can change it, or the workspace is suspended. |
| 404 | Not found, or it belongs to another workspace. |
| 409 | A conflict such as slug_taken. |
| 429 | rate_limited. Wait the seconds in the Retry-After header. |
Rate limits
- A key may make 600 requests a minute.
- Creating links is limited to 60 a minute per key.
- A workspace in its first 24 hours may create 50 links an hour.
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.