API documentation
Create short links and batches of personal codes from any platform: email and SMS senders, push networks, CRMs, n8n / Make / Zapier or your own script.
Overview
Base URL:
https://createlink.link/api/v1
The API works with two things:
- Link — a short link on your domain that leads to your target page.
- Personal code — a separate short address for one recipient of a link. Each code
carries an optional
ref: any label you choose (an email, a subscriber ID, a phone number, a click ID). The API never interprets it — it just gives it back with the code.
Codes are served by the agent on your VPS and pass through the Anti-Bot filter exactly like codes created in the dashboard. They are paid from the same balance: one code = one link from your package.
Requests and responses are JSON (UTF-8). Results can also be downloaded as CSV, which every mailing platform can import.
Authentication
Create an integration key in your dashboard: account menu → API. The key is shown once — store it in your platform's settings. You can have several keys (one per platform) and revoke any of them without affecting the others or your agent.
Authorization: Bearer cl_live_xxxxxxxxxxxxxxxx
The header X-API-Key: cl_live_… works too. Never put the key
in a URL or in code that runs in a browser.
curl https://createlink.link/api/v1/account -H "Authorization: Bearer $KEY"
{"user_number": "123456", "links_available": 4870, "billing_enforced": true, "key_name": "Mumara"}
Connecting a platform
Mailing and messaging platforms send at full speed and do not call external services for each recipient. So codes are created before sending, and the platform simply inserts them:
- Create a link once:
POST /links(or use one from the dashboard). - Before the campaign, send your recipient list:
POST /links/{id}/codeswithrecipients— emails or subscriber IDs. - Get back pairs
ref → url(JSON or CSV). - Write each URL into a custom field of the subscriber in your platform — via its API or a CSV import matched by email / ID.
- Put that field's merge tag into the letter or message. The platform substitutes each recipient's own link while sending.
Step-by-step recipes with ready commands:
Your platform exports a CSV? Send the file as it is — no need to build JSON:
curl -X POST "https://createlink.link/api/v1/links/512/codes?column=1&skip_header=1" \ -H "Authorization: Bearer $KEY" -H "Content-Type: text/csv" --data-binary @export.csv
And download the result with the column names your platform imports:
curl "https://createlink.link/api/v1/jobs/JOB_ID?format=csv&columns=Email:ref,CL_LINK:url,Name:" \ -H "Authorization: Bearer $KEY" -o links.csv
Remember: a code lives 60 days and opens the page up to 3 times — create fresh codes for every campaign instead of storing one link in a subscriber field forever.
No list at hand? Send "count": 5000 instead of
recipients and distribute the codes in any order.
Tagged links — no lists at all
The simplest way to connect any platform. Turn on tagged for a link (in the
dashboard: Link with tag on the link card; via the API: "tagged": true). Then put one address
into the message and let your platform append its subscriber tag after r=:
https://go.example.com/aB3dE9?r=%%subscriber_id%%
On the subscriber's first click the platform creates their personal code (paid like any code: 1 link) and every next click of that subscriber goes through the same code with the usual 3-click limit. No export, no import, no fresh batch before every campaign.
- An untouched tag (
%%…%%,[…],{…}) or an empty balance does not lose the visitor: they reach the page as from a regular link, just without a personal code. - New codes per link are capped per hour, so a bot iterating
?r=1,2,3…cannot drain the balance. - Requires agent version 2026.10.18-1 or newer; an older agent ignores
r=and works as a regular link.
PATCH/links/{id} with {"tagged": true}
switches the mode; the link object then has tagged_url, e.g.
https://go.example.com/aB3dE9?r={ref}.
Links
GET/domains — domains ready for links
(verified and connected to Cloudflare).
{"domains": [{"id": 9, "domain": "go.example.com"}]}
POST/links — create a link.
| Field | Type | Description |
|---|---|---|
url | string, required | Target page. It is checked against threat databases first, as in the dashboard. |
domain | string or id | A domain from /domains. "all" (default) spreads links over all your domains. |
name | string | Your label for the link. |
campaign | string | Campaign name. |
tagged | boolean | Turn on tagged mode. |
curl -X POST https://createlink.link/api/v1/links -H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/offer", "name": "October newsletter"}'
201 Created
{"id": 512, "short_url": "https://go.example.com/aB3dE9", "target_url": "https://example.com/offer",
"domain": "go.example.com", "all_domains": true, "name": "October newsletter", "campaign": "",
"archived": false, "created_at": "2026-10-18T09:00:00Z",
"stats": {"codes": 0, "opens": 0, "clicks": 0, "bots_blocked": 0}}
GET/links — your links, newest first.
Query: limit (≤ 200), offset, archived=1 to include archived ones.
GET/links/{id} — one link with its counters.
GET/links/{id}/codes — every code of the
link (from the dashboard and the API) with status, opens and clicks. Query: limit
(≤ 10 000), offset, format=csv.
Personal codes
POST/links/{id}/codes — create a batch.
| Field | Type | Description |
|---|---|---|
recipients | array of strings | One code per item; the item is returned as ref (≤ 255 chars). Up to 100,000 per request. |
count | integer | Instead of recipients: that many codes without a ref. |
mode | string | auto (default): up to 1,000 codes are returned right away, bigger batches go to a job. sync / async force one way. |
webhook_url | https URL | Called when the job finishes (see below). |
With Content-Type: text/csv (or text/plain) the body is the recipient file itself; query parameters: column — the column with the ref, from 1 (default 1); skip_header=1 — skip the header row; mode, webhook_url as above. Comma, semicolon and tab separators are detected automatically. | ||
Header Idempotency-Key (recommended): any unique string per batch, e.g. your
campaign ID. If the connection drops and you repeat the request with the same key, you get the same batch
back — nothing is created or charged twice.
curl -X POST https://createlink.link/api/v1/links/512/codes -H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" -H "Idempotency-Key: campaign-2026-10-18" \
-d '{"recipients": ["anna@example.com", "subscriber-1042"]}'
201 Created
{"job_id": "job_4f1c…", "status": "done", "requested": 2, "generated": 2,
"codes": [
{"ref": "anna@example.com", "url": "https://go.example.com/aB3dE9_x7Kq…", "code": "aB3dE9_x7Kq…"},
{"ref": "subscriber-1042", "url": "https://go2.example.com/aB3dE9_Pz0w…", "code": "aB3dE9_Pz0w…"}
], …}
A big batch answers 202 Accepted with "status": "queued"
and a status_url. The balance is charged when the batch is accepted; if fewer codes are made,
the difference is returned automatically. Not enough links on the balance → 402, nothing is charged.
Jobs & webhooks
GET/jobs/{job_id} — status of a batch:
queued → running → done or failed, with
generated showing progress. When finished, the response includes codes
(pages of up to 10 000: offset, limit). ?format=csv downloads all of
them as ref,url,code. Add columns to shape the file for your platform's import:
columns=Email:ref,CL_LINK:url,Name: — each item is header:source, where the source is
ref, url, code or empty (an empty column). The same works for
GET /links/{id}/codes?format=csv.
curl "https://createlink.link/api/v1/jobs/job_4f1c…?format=csv" -H "Authorization: Bearer $KEY" -o codes.csv
Batches survive server restarts: an interrupted job continues where it stopped, without duplicates. Jobs and their results are kept for 30 days.
Webhook: when a job finishes we send POST to your webhook_url
(up to 3 attempts):
{"event": "job.finished", "job_id": "job_4f1c…", "status": "done",
"link_id": 512, "requested": 50000, "generated": 50000}
The webhook carries no codes and no secrets — fetch the result with your key from
/jobs/{job_id}. That is why it needs no signature: a forged call cannot give anyone your codes.
Errors & limits
Every error has the same shape:
{"error": {"code": "insufficient_credits", "message": "Not enough links on the balance: …",
"needed": 50000, "available": 4870}}
| HTTP | code | Meaning |
|---|---|---|
| 400 | invalid_json, missing_field, invalid_field, unknown_domain | Fix the request; field names the culprit. |
| 401 | missing_api_key, invalid_api_key | No key, a wrong key or a revoked one. |
| 402 | insufficient_credits | Top up in the dashboard. Nothing was charged. |
| 404 | not_found | No such link or job in your account. |
| 409 | no_domain | No domain is ready for links yet. |
| 413 | batch_too_large | Split the list into several requests. |
| 422 | target_flagged, target_invalid | The target URL failed the threat-database check. |
| 429 | — | Too many requests; wait for Retry-After. |
| 503 | target_check_unavailable | The URL check is temporarily unavailable; retry in a minute. |
Rate limit: 120 requests per minute and 3 000 per hour per key. One batch request can carry up to 100,000 codes, so the limits are about requests, not volume.