Developer API

API Documentation

Create protected links, read privacy-conscious analytics, and record conversions from your own product.

01

Make your first request

The API is server-to-server. Never expose a token in browser JavaScript or a public mobile bundle.

  1. 1Start Pro or BusinessAPI access is enabled on supported paid plans and during the Pro trial.
  2. 2Create a scoped tokenChoose only the permissions your integration needs in the Developer Console.
  3. 3Send it over HTTPSAuthorization: Bearer 4ul_live_…
  4. 4Store it like a passwordThe full token is shown once. Revoke it immediately if it may be exposed.
curl "https://4ul.ink/api/v1/links?per_page=1" \
  -H "Authorization: Bearer YOUR_TOKEN"
02

Endpoint reference

All object access is restricted to the workspace assigned to the token.

MethodPathScopeResult
GET/api/v1/linkslinks:readPaginated links
POST/api/v1/linkslinks:writeCreates a protected link
PATCH/api/v1/links/{id}links:writeUpdates supplied fields
DELETE/api/v1/links/{id}links:writeDisables without deleting analytics
GET/api/v1/links/{id}/analyticsanalytics:readAggregated analytics
POST/api/v1/conversionslinks:writeRecords an attributed event
GET/api/v1/domainsdomains:readCustom domains
POST/api/v1/domainsdomains:writeConnect a domain
POST/api/v1/domains/{id}/verifydomains:writeVerify DNS
DELETE/api/v1/domains/{id}domains:writeRemove
GET/api/v1/webhookswebhooks:readWebhook endpoints
POST/api/v1/webhookswebhooks:writeAdd endpoint
PATCH/api/v1/webhooks/{id}webhooks:writeUpdates supplied fields
POST/api/v1/webhooks/{id}/testwebhooks:writeSend test
POST/api/v1/webhooks/{id}/rotate-secretwebhooks:writeRotate secret
DELETE/api/v1/webhooks/{id}webhooks:writeDelete
03

Requests and responses

Request and response field names below match the live v1 API.

POST

Create a link

links:write
{
    "destination_url": "https://example.com",
    "slug": "launch",
    "title": "Launch article"
}
destination_url
Required public HTTP or HTTPS URL.
title
Optional, up to 190 characters.
slug
Optional custom alias, 3 to 80 characters.
201

Link response

application/json
{
  "data": {
    "id": 123,
    "slug": "launch",
    "short_url": "https://4ul.ink/launch",
    "destination_url": "https://example.com/article",
    "title": "Launch article",
    "click_count": 0,
    "is_active": true,
    "created_at": "2026-07-25 12:00:00"
  }
}
GET

List links

?page=1&per_page=25
{
  "data": [{ "id": 123, "slug": "launch", "is_active": true }],
  "meta": {
    "page": 1,
    "per_page": 25,
    "total": 1,
    "total_pages": 1
  }
}

per_page accepts 1 to 100. Link objects use the complete response shape shown above.

GET

Read analytics

analytics:read
/api/v1/links/123/analytics?period=custom
  &from=2026-07-01
  &to=2026-07-20

Periods are 7, 30, 90, or custom. The response includes overview, comparisons, traffic quality, conversions, daily series, audience breakdowns, and hourly patterns—never raw visitor IP addresses.

PATCH

Update a link

/api/v1/links/123
{
  "destination_url": "https://example.com/new",
  "title": "New title",
  "slug": "new-alias",
  "is_active": true
}

Only supplied fields change. A destination change automatically queues a new Nova Shield analysis.

POST

Record a conversion

/api/v1/conversions
{
  "click_token": "4ul_click_…",
  "event": "sale",
  "external_id": "order_1042",
  "amount": 49.00,
  "currency": "USD"
}

Events are lead, sale, or refund. A duplicate external event returns HTTP 409 and is never counted twice.

Token scopes

links:read
lists links.
links:write
creates, updates, disables links and records conversions.
analytics:read
reads link analytics.
domains:read / domains:write
Custom domains
webhooks:read / webhooks:write
Signed webhooks

Rate limits

Tokens allow 120 requests per minute plus the workspace monthly API allowance. Analytics has an additional 60 requests per minute limit. HTTP 429 includes Retry-After and RateLimit headers. Ordinary redirects keep working.

Idempotency

Send an Idempotency-Key of 8 to 128 printable characters when creating links. Keys are workspace-scoped for 24 hours. An identical retry replays the stored response; different input with the same key returns HTTP 409.

HTTP

Errors

Errors use one predictable JSON shape. Common statuses are 400 invalid JSON, 401 invalid token, 403 missing scope or plan access, 404 not found in this workspace, 409 conflict, 413 body too large, 415 wrong content type, 422 validation error, and 429 rate or usage limit.

{
  "error": "destination_url must be a valid public HTTP or HTTPS URL.",
  "code": "invalid_destination",
  "field": "destination_url",
  "request_id": "req_7fd9c0c8d821e84b3d69d171",
  "error_details": {
    "code": "invalid_destination",
    "message": "destination_url must be a valid public HTTP or HTTPS URL.",
    "field": "destination_url",
    "request_id": "req_7fd9c0c8d821e84b3d69d171"
  }
}
HMAC

Verify every webhook

Events include link.created, link.updated, link.disabled, link.clicked, conversion.created, shield.status_changed, and endpoint.test. Automatic delivery retries up to five times.

Availability webhooks use shield.availability_changed and contain status transitions plus bounded HTTP diagnostics without destination URLs.

Delivery headers

X-4ulink-Event: conversion.created
X-4ulink-Delivery: EVENT_UUID
X-4ulink-Timestamp: UNIX_TIMESTAMP
X-4ulink-Signature: sha256=HEX_HMAC

PHP verification

$raw = file_get_contents('php://input');
$signed = $timestamp . '.' . $raw;
$expected = 'sha256=' . hash_hmac('sha256', $signed, $secret);

if (!hash_equals($expected, $signature)) {
    http_response_code(401);
    exit;
}

Use the raw request body, compare signatures in constant time, reject timestamps older than five minutes, and store X-4ulink-Delivery so retries remain idempotent. Webhook endpoints must use public HTTPS on port 443.