Links

BASE  https://api.rii.link/v1AUTH: BEARER → AUTHENTICATION

A link is a mapping: a slug on a domain, a fallback url, and an ordered list of routing rules that decide per visitor.

POST/v1/links

Create a link

The one required field is url. Everything else — slug, domain, routing, utm — has a working default, so the minimum viable create is one field.

PARAMETERS
urlstringREQUIRED

Fallback destination — where visitors go when no rule matches.

slugstring

Path on the domain. Omitted → a short random slug, collision-checked.

domainstring

Defaults to rii.link. Otherwise a host connected through POST /v1/domains — active, or still pending_dns, in which case the link is staged (see below). Dashboard-connected domains are refused with 400.

routingarray

Ordered if/then rules, up to 50 — see PATCH below for the shape. Needs Premium.

expires_attimestamp

After this moment the link answers 410 instead of routing.

utmobject

Campaign tags written onto url as utm_* parameters: source, medium, campaign, term, content (all optional strings, up to 255 characters). Values are trimmed; source and medium are lowercased; a utm_* already on url is replaced, never duplicated. Routing rule destinations (to) are left as they are. The response returns the tagged url plus a utm object.

REQUEST
curl -X POST https://api.rii.link/v1/links \
-H "Authorization: Bearer $RII_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/summer",
"slug": "summer",
"utm": { "source": "Newsletter", "medium": "Email", "campaign": "summer-2026" },
"routing": [
{ "if": { "country": "DE" }, "to": "https://example.de/sommer" }
]
}'
RESPONSE · 201
{
"id": "lnk_8h2f0qZ3Tk1mR4vXw9aBcQ",
"slug": "summer",
"domain": "rii.link",
"short_url": "https://rii.link/summer",
"url": "https://example.com/summer?utm_source=newsletter&utm_medium=email&utm_campaign=summer-2026",
"utm": {
"source": "newsletter",
"medium": "email",
"campaign": "summer-2026",
"term": null,
"content": null
},
"routing": [
{ "if": { "country": "DE" }, "to": "https://example.de/sommer" }
],
"expires_at": null,
"domain_status": "active",
"staged": false,
"created_at": "2026-08-12T09:14:03Z",
"updated_at": "2026-08-12T09:14:03Z"
}
GET/v1/links

List links

Links created through the API or MCP, cursor-paginated, newest first. Filter to one domain to audit a host before disconnecting it.

PARAMETERS
limitinteger

Default 25, max 100.

cursorstring

next_cursor from the previous page.

domainstring

Only links on this host.

REQUEST
curl "https://api.rii.link/v1/links?limit=25" \
-H "Authorization: Bearer $RII_KEY"
RESPONSE · 200
{
"data": [
{
"id": "lnk_8h2f0qZ3Tk1mR4vXw9aBcQ",
"slug": "summer",
"domain": "rii.link",
"short_url": "https://rii.link/summer",
"url": "https://example.com/summer?utm_source=newsletter&utm_medium=email&utm_campaign=summer-2026",
"utm": {
"source": "newsletter",
"medium": "email",
"campaign": "summer-2026",
"term": null,
"content": null
},
"routing": [],
"expires_at": null,
"domain_status": "active",
"staged": false,
"created_at": "2026-08-12T09:14:03Z",
"updated_at": "2026-08-12T09:14:03Z"
}
],
"next_cursor": "c_MjAyNi0wOC0xMlQwOToxNDow…"
}
GET/v1/links/{id}

Retrieve a link

The full object, including the routing array in evaluation order.

REQUEST
curl https://api.rii.link/v1/links/lnk_8h2f0qZ3Tk1mR4vXw9aBcQ \
-H "Authorization: Bearer $RII_KEY"
RESPONSE · 200
{
"id": "lnk_8h2f0qZ3Tk1mR4vXw9aBcQ",
"slug": "summer",
"domain": "rii.link",
"short_url": "https://rii.link/summer",
"url": "https://example.com/summer?utm_source=newsletter&utm_medium=email&utm_campaign=summer-2026",
"utm": {
"source": "newsletter",
"medium": "email",
"campaign": "summer-2026",
"term": null,
"content": null
},
"routing": [
{ "if": { "country": "DE" }, "to": "https://example.de/sommer" }
],
"expires_at": null,
"domain_status": "active",
"staged": false,
"created_at": "2026-08-12T09:14:03Z",
"updated_at": "2026-08-12T09:14:03Z"
}
PATCH/v1/links/{id}

Update a link

PATCH semantics per field — but routing replaces the whole array on write. Send it in the order you want it evaluated; first match wins.

PARAMETERS
routingarray

The full rule list, one condition per rule: country (ISO code), device (ios, android, mobile, tablet, desktop), language (e.g. de), or time — { "days": ["mon", …], "start": "09:00", "end": "17:00", "timezone": "Europe/Berlin" }. First match wins.

urlstring

New fallback destination.

slugstring

Renames the path — the old slug 301s for 30 days.

expires_attimestamp

Set, move, or null it while the link is live — once expired, a link answers 410.

utmobject

Changes only the keys you send: a value sets that utm_* on the destination, null (or "") removes it, omitted keys stay. "utm": null strips every UTM parameter. If url is sent too, the tags go onto the new url. Rule destinations are untouched.

REQUEST
curl -X PATCH https://api.rii.link/v1/links/lnk_8h2f0qZ3Tk1mR4vXw9aBcQ \
-H "Authorization: Bearer $RII_KEY" \
-H "Content-Type: application/json" \
-d '{
"routing": [
{ "if": { "device": "ios" }, "to": "https://apps.apple.com/app/id0000000" },
{ "if": { "country": "DE" }, "to": "https://example.de/sommer" }
]
}'
RESPONSE · 200
{
"id": "lnk_8h2f0qZ3Tk1mR4vXw9aBcQ",
"slug": "summer",
"domain": "rii.link",
"short_url": "https://rii.link/summer",
"url": "https://example.com/summer?utm_source=newsletter&utm_medium=email&utm_campaign=summer-2026",
"utm": {
"source": "newsletter",
"medium": "email",
"campaign": "summer-2026",
"term": null,
"content": null
},
"routing": [
{ "if": { "device": "ios" }, "to": "https://apps.apple.com/app/id0000000" },
{ "if": { "country": "DE" }, "to": "https://example.de/sommer" }
],
"expires_at": null,
"domain_status": "active",
"staged": false,
"created_at": "2026-08-12T09:14:03Z",
"updated_at": "2026-08-12T09:15:21Z"
}

Campaign tags (UTM)

Every link response carries utm — the five UTM parameters on url, exactly as analytics records them, null for any that are absent. Send utm on create or update instead of editing the query string by hand: rii.link writes the tags onto url with one set of rules (the same as the dashboard builder and the MCP tools), so "Email" and "email" never split one channel into two rows. When a visitor clicks, analytics records the tags on the destination actually served — after rules and splits — alongside any utm_* on the short URL itself.

SEND "utm": null TO STRIP EVERY UTM PARAMETER FROM THE DESTINATION

REQUEST
curl -X PATCH https://api.rii.link/v1/links/lnk_8h2f0qZ3Tk1mR4vXw9aBcQ \
-H "Authorization: Bearer $RII_KEY" \
-H "Content-Type: application/json" \
-d '{ "utm": { "campaign": "summer-sale", "medium": null } }'
RESPONSE · 200 (EXCERPT)
{
"id": "lnk_8h2f0qZ3Tk1mR4vXw9aBcQ",
"url": "https://example.com/summer?utm_source=newsletter&utm_campaign=summer-sale",
"utm": {
"source": "newsletter",
"medium": null,
"campaign": "summer-sale",
"term": null,
"content": null
},
…
}

Staged links: import before DNS switches

A link can go onto a domain that is still pending_dns. It is stored and loaded into routing straight away, and comes back with "staged": true and "domain_status": "pending_dns". Once the domain's DNS points at rii.link and its certificate is issued, the domain turns active and the link answers — no second call, nothing to re-send. Plan limits, destination safety checks and validation apply exactly as on an active domain. A domain whose setup failed, or one you disconnected, is refused with 400 validation_failed.

MIGRATING? CONNECT THE DOMAIN, IMPORT EVERY LINK, THEN SWITCH DNS — NO 404 WINDOW

REQUEST
curl -X POST https://api.rii.link/v1/links \
-H "Authorization: Bearer $RII_KEY" \
-H "Content-Type: application/json" \
-d '{ "url": "https://example.com/spring", "slug": "spring", "domain": "go.example.com" }'
RESPONSE · 201 (DOMAIN STILL PENDING_DNS)
{
"id": "lnk_2Rk9sV0mQx7tLb4wNc1yHA",
"slug": "spring",
"domain": "go.example.com",
"short_url": "https://go.example.com/spring",
"url": "https://example.com/spring",
"utm": {
"source": null,
"medium": null,
"campaign": null,
"term": null,
"content": null
},
"routing": [],
"expires_at": null,
"domain_status": "pending_dns",
"staged": true,
"created_at": "2026-08-12T09:16:40Z",
"updated_at": "2026-08-12T09:16:40Z"
}
DELETE/v1/links/{id}

Delete a link

Deletes the mapping and quarantines the slug for 30 days before it can be re-claimed — a reprinted poster should never route somewhere new silently.

REQUEST
curl -X DELETE https://api.rii.link/v1/links/lnk_8h2f0qZ3Tk1mR4vXw9aBcQ \
-H "Authorization: Bearer $RII_KEY"
RESPONSE
HTTP/2 204