Create, list, update and delete short links, and pull click statistics, from your own code. Available on Bolt and above.
Every request must carry a bearer token in the Authorization header. Create a key from your dashboard's API keys page — the raw key is shown once, at creation, and cannot be retrieved again.
Authorization: Bearer z4ps_3a9f1c...
An unknown, revoked or expired key returns 401 Unauthorized. Revoking a key from the dashboard takes effect immediately — the very next request fails. If your account is not on Bolt or above (including if a subscription lapses), every key on the account returns 403 Forbidden until you upgrade again; the keys themselves are not deleted.
https://z4ps.uk/api-v1.php
There is a single endpoint file; the resource and action are selected by HTTP method and the id query parameter, as shown below. All request and response bodies are JSON.
Generous by design — a real integration will not hit these. Limits apply per key and per account (across all of an account's keys), whichever is hit first: 600 requests/hour per key, 1,200 requests/hour per account. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers. Exceeding the limit returns 429 Too Many Requests.
Errors are always {"error": "human-readable message"} with a matching HTTP status. Messages never leak internal details (stack traces, queries, file paths) — only what went wrong and, where useful, how to fix it.
Every endpoint enforces the exact same plan rules as the dashboard: monthly link allowance, custom/short-slug quotas, editing rights, and analytics retention. There is no separate "API tier" — your plan behaves identically whether you use the website or the API. See the pricing page for current per-plan limits.
Create a short link.
| destination | string, required — the http(s) URL to redirect to |
| slug | string, optional — a custom slug (subject to your plan's length/quota rules); omit for a random slug |
| campaign_id | integer, optional — assign the link to one of your own campaigns at creation (Surge and above); omit or send null for no campaign |
| expires_at | string, optional — an ISO-8601 or YYYY-MM-DD HH:MM:SS timestamp in the future; the link stops redirecting once this passes. Free on every plan. Omit for no time-based expiry. |
| max_clicks | integer, optional — the link stops redirecting once it has been granted this many clicks. Free on every plan. Omit for no click-count limit. |
| utm_source, utm_medium, utm_campaign, utm_term, utm_content | string, optional, free on every plan — if any is present, destination is tagged with the given UTM parameters before shortening. Any existing query string on destination is preserved; existing utm_* values are replaced, never duplicated. Requires at least one non-empty value. |
curl -X POST https://z4ps.uk/api-v1.php \
-H "Authorization: Bearer z4ps_3a9f1c..." \
-H "Content-Type: application/json" \
-d '{"destination":"https://example.com/landing","slug":"launch","campaign_id":7,"max_clicks":500}'
{
"link": {
"id": 42,
"slug": "launch",
"destination": "https://example.com/landing",
"short_url": "https://z4ps.uk/launch",
"status": "active",
"protected": false,
"campaign_id": 7,
"expires_at": null,
"max_clicks": 500,
"granted_clicks": 0,
"expired": false,
"expired_at": null,
"expired_reason": null,
"created_at": "2026-08-01 10:15:02",
"updated_at": null
}
}
List your links, newest first.
| page | integer, optional, default 1 |
| per_page | integer, optional, default 20, max 100 |
| format | string, optional — json (default), csv, tsv, txt or md. A non-JSON value returns a file download of the current page of links (page/per_page still apply, so page through or raise per_page for everything). The same serialiser the dashboard uses: csv/tsv are a spreadsheet table (UTF-8 BOM included), txt is one short URL per line, md is a Markdown link list. An unrecognised value falls back to JSON. |
{
"links": [ { "id": 42, "slug": "launch", "...": "..." } ],
"pagination": { "page": 1, "per_page": 20, "total": 1, "total_pages": 1 }
}
Example CSV export: curl -H "Authorization: Bearer z4ps_..." "https://z4ps.uk/api-v1.php?format=csv&per_page=100" -o links.csv
Fetch one of your links by id.
{ "link": { "id": 42, "slug": "launch", "...": "..." } }
Returns 404 Not Found if the id does not exist or belongs to a different account — the two cases look identical, so no ownership is ever leaked.
Update a link's destination. Editing destinations is a Bolt+ feature and is refused on a link that is frozen (over your plan's slug limit) — the link keeps redirecting either way, only editing is blocked.
| destination | string, required — the new http(s) URL |
| campaign_id | integer or null, optional — reassign the link to one of your own campaigns (Surge and above), or send null to unassign; omit this key entirely to leave the campaign untouched |
| expires_at | string or null, optional — a future timestamp, or null to remove the time-based expiry; omit this key entirely to leave it untouched |
| max_clicks | integer or null, optional — a new click limit, or null to remove it; omit this key entirely to leave it untouched. Lowering it below the clicks already granted expires the link immediately; raising it (or removing the limit) un-expires a link that only expired by clicks. |
{ "link": { "id": 42, "destination": "https://example.com/new-page", "campaign_id": 7, "...": "..." } }
Delete a link and its click history. This cannot be undone.
{ "deleted": true, "id": 42 }
Click statistics for one of your links: all-time totals plus a 30-day daily series. Retention (how far back detail is kept) matches your plan, exactly like the dashboard.
| format | string, optional — json (default), csv or tsv. A non-JSON value downloads the daily series as a table (the scalar totals stay JSON-only, so text/Markdown do not apply here). |
{
"id": 42,
"slug": "launch",
"totals": { "total": 128, "humans": 110, "bots": 18, "unique_ish": 74 },
"daily": [ { "date": "2026-07-02", "total": 4, "humans": 4, "bots": 0 } ]
}
422 Unprocessable Entity with the reason.Unlike every other feature on this page, expiry is free on every plan — it's a safety/hygiene feature, not a premium one. Set an expires_at timestamp and/or a max_clicks count on create or update; either or both may be set, and either can be cleared independently by sending null for that key on a PATCH. An expired link returns 410 Gone with a branded page and never reveals its destination — a deliberate, owner-initiated exception to the "a link never breaks" rule. granted_clicks in the response is the number of clicks actually let through the redirect (counted atomically, so a burst of concurrent requests against the last remaining click cannot overshoot max_clicks); the expired boolean and expired_at/expired_reason fields reflect current state.
Free on every plan, like expiry above. Pass any of utm_source, utm_medium, utm_campaign, utm_term or utm_content on create and destination is tagged before shortening: any existing query string is kept, any existing utm_* values are overwritten (never duplicated), and values are percent-encoded. Omit all five to shorten destination unmodified, exactly as before.
Available on Surge and above. A link's rules run in order, first match wins: country and device rules match or don't; a split rule always matches once reached and picks a weighted alternative (sticky per visitor). Rules sharing the same position form one split group — create two rules with the same position to build an A/B test. If nothing matches, the link's own destination is used, exactly as if it had no rules.
List a link's rules, ordered for evaluation.
{
"rules": [
{ "id": 5, "position": 0, "condition_type": "country", "condition_value": "FR,DE,BE",
"destination": "https://example.com/eu", "weight": 100, "created_at": "2026-08-01 10:00:00" },
{ "id": 6, "position": 1, "condition_type": "split", "condition_value": null,
"destination": "https://example.com/a", "weight": 50, "created_at": "2026-08-01 10:01:00" },
{ "id": 7, "position": 1, "condition_type": "split", "condition_value": null,
"destination": "https://example.com/b", "weight": 50, "created_at": "2026-08-01 10:01:00" }
]
}
Create one rule (or one alternative of a split group).
| condition_type | string, required — one of country, device, split |
| condition_value | string — required for country (comma-separated 2-letter codes, e.g. FR,DE,BE) and device (one of desktop, mobile, tablet); omitted for split |
| destination | string, required — the http(s) URL for this condition/alternative, checked through the same safety gate as a normal link |
| weight | integer 1-1000, split only — relative share of traffic for this alternative |
| position | integer, optional — omit for a new rule/group (appended last); pass an existing group's position to add another weighted alternative to that split test |
{ "rule": { "id": 6, "position": 1, "condition_type": "split", "...": "..." } }
Reorder a link's rule groups, by sending reorder instead of a new rule's fields.
| reorder | array of integers, required — rule ids in the desired new order; ids you own but omit keep their relative order, appended after the given ones |
curl -X POST "https://z4ps.uk/api-v1.php?id=42&rules=1" \
-H "Authorization: Bearer z4ps_3a9f1c..." \
-H "Content-Type: application/json" \
-d '{"reorder":[6,7,5]}'
Update one rule's condition, destination, or (for a split alternative) weight. Body parameters are the same as create.
{ "rule": { "id": 6, "destination": "https://example.com/a2", "...": "..." } }
Delete one rule (or one alternative of a split group).
{ "deleted": true, "id": 6 }