Links
Create, list, read, update and delete short links.
Everything a link integration needs lives under one resource. Create a link, read it back, change where it points, switch it off or delete it. Create, get and update return the same link object, and a listing returns a page of them.
The examples on this page are real calls, captured against our own API and reprinted with the public host and a placeholder key. Responses are shown pretty-printed for reading; on the wire they are compact.
Create a link
/api/v1/links
Shorten one URL. This is the endpoint most integrations only ever need.
| Field | Type | Required | Description |
|---|---|---|---|
url |
string | required | The destination. Must be a well-formed http:// or https:// address. Some destinations are refused with a 400 validation error: a host on our denylist, and any address that is already a short link (one of ours, a custom domain, or another known shortener). Use the final destination instead. |
alias |
string | optional, Pro and above | Your own code instead of a generated one, e.g. "spring-sale". At most 40 characters, using only letters, digits, - and _. On the shared domain the shortest alias allowed depends on the plan, and a shorter one answers 400 with the minimum in the message; on your own domain any length works. Anything else answers 400. Already in use on that domain answers 409. |
expiresOn |
integer | optional, Pro and above | Unix seconds. The link stops resolving after this moment. A time in the past is accepted and creates a link that has already expired. |
maxClicks |
integer | optional, Pro and above | Stop resolving after this many clicks, from 1 to 1,000,000,000. Clicks from people count; bots and link previews do not. |
startsOn |
integer | optional, Pro and above | Unix seconds. The link does not resolve before this moment. It must be before expiresOn. |
fallbackUrl |
string | optional, Pro and above | A full http:// or https:// address where visitors go while the link is not live, instead of our page. It is checked like any destination. |
iosAppLink |
string | optional, Team and above | A link into your iOS app: your app's own scheme, e.g. "myapp://product/42", or an https:// universal link. iPhone and iPad visitors get a page that tries to open it and continues to url if the app does not open; with a universal link the page waits for a tap. At most 2,000 characters. http://, javascript:, tel: and similar schemes are refused. |
androidAppLink |
string | optional, Team and above | A link into your Android app, e.g. "myapp://product/42". It cannot contain #. Needs androidPackage. |
androidPackage |
string | optional, Team and above | The Android app's package name, e.g. "com.example.app". Required with androidAppLink. |
password |
string | optional, Pro and above | Visitors must enter this before being forwarded. |
domain |
string | optional, Pro and above | A custom domain of yours, by host, e.g. "go.example.com". It must belong to this workspace and be verified and active. Otherwise the request answers 400 validation and nothing is created. Send "default" to use whichever domain the workspace has set as its default, or, when you also name a folder, that folder's default domain; if there is none, the request answers 400 rather than quietly using ivi.to. Leaving the field out keeps using ivi.to, as before. |
folder |
string | optional, Pro and above | The folder to file the link in, as a path: "Campaigns/Q4". Levels that do not exist are created. Leave it out, or send null, to keep the link out of any folder. |
title |
string | optional | Your own name for the link, shown in the app instead of the code. Free on every plan. |
notes |
string | optional | Notes kept with the link, searchable in the app. Free on every plan. |
curl -X POST "https://urlicer.com/api/v1/links" \
-H "Authorization: Bearer url_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/blog/2026/the-long-article-title-goes-here"}'
{
"short": "https://ivi.to/mDpk",
"code": "mDpk",
"domain": null,
"url": "https://example.com/blog/2026/the-long-article-title-goes-here",
"clicks": 0,
"active": true,
"expiresOn": null,
"maxClicks": null,
"startsOn": null,
"fallbackUrl": null,
"iosAppLink": null,
"androidAppLink": null,
"androidPackage": null,
"createdOn": 1787314771,
"folder": null,
"title": null,
"notes": null,
"warnings": []
}
| Field | Type | Description |
|---|---|---|
short |
string | The finished short link, absolute and ready to share. |
code |
string | Just the code part, without the domain. |
domain |
string or null | The custom domain the link lives on, or null when it uses the default ivi.to. |
url |
string | The destination the link currently points at. |
clicks |
integer | Clicks counted so far. Read live from the click counter, not a nightly total. |
active |
boolean | False for a link you have switched off. Visitors see the "link disabled" page instead of the destination. |
expiresOn |
integer or null | Unix seconds. After this moment the link stops resolving. Null means it never expires. |
maxClicks |
integer or null | The link stops resolving after this many clicks since it was created. Null means no limit. |
startsOn |
integer or null | Unix seconds. Before this moment the link does not resolve yet. Null means it is live from creation. |
fallbackUrl |
string or null | Where visitors go while the link is not live (before startsOn, after expiresOn, past maxClicks) instead of our page. Null means our page. |
iosAppLink |
string or null | The app link iPhone and iPad visitors are offered instead of going straight to url. Null when the link has none. |
androidAppLink |
string or null | The same, for Android visitors. |
androidPackage |
string or null | The Android app's package name, set together with androidAppLink. |
createdOn |
integer | Unix seconds, UTC. |
folder |
string or null | The folder the link is filed in, as a path from the top: "Campaigns/Q4". Null when the link is in no folder. |
title |
string or null | Your workspace's own name for the link, shown in the app instead of the bare code. Null when it has none. |
notes |
string or null | Free-text notes kept with the link. Null when it has none. |
warnings |
array of strings | Empty unless this create or update broke your workspace's UTM naming rules while they are set to warn. Then it says what, and the link was saved anyway. Always empty on get and list. |
- Codes are unique per domain. The same alias can exist once on ivi.to and once on each of your custom domains.
- Every created link counts against your plan's monthly link allowance, and the count does not go back down when you delete one.
- Omit "alias" and you get a generated code: shorter, and it can never collide.
- Folders are addressed by path, "Campaigns/Q4", and any level that does not exist yet is created, so an import needs no set-up call first. Folders are a paid feature; on a plan without them a request that names one answers 403 feature. See the folders reference for listing, renaming and deleting them.
- A folder can carry a default domain. With "domain": "default" and a folder named, the link is created on that folder's domain, or the nearest one above it, before falling back to the workspace default.
- If your workspace has UTM naming rules (Pro and above), the utm_source, utm_medium and utm_campaign in "url" are checked against them, and lowercased first when the rules say so. A value that is not on a list answers 400 validation when the rules block, or saves the link and fills "warnings" when they warn. Which rules apply depends on the folder the link ends up in: a folder can carry its own, which replace the ones it would otherwise follow, and a folder without any follows the nearest folder above it, then the workspace.
Using every option at once
A create request can carry a custom alias, an expiry, a password and one of your own domains together. Each of those is a paid feature, and each is checked separately, so a request that asks for two features you have and one you do not is refused for the one you do not, and creates nothing.
curl -X POST "https://urlicer.com/api/v1/links" \
-H "Authorization: Bearer url_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/spring-sale",
"alias": "spring-sale",
"expiresOn": 1790000000,
"domain": "go.example.com",
"folder": "Campaigns/Q4",
"title": "Spring sale"
}'
{
"short": "https://go.example.com/spring-sale",
"code": "spring-sale",
"domain": "go.example.com",
"url": "https://example.com/spring-sale",
"clicks": 0,
"active": true,
"expiresOn": 1790000000,
"maxClicks": null,
"startsOn": null,
"fallbackUrl": null,
"iosAppLink": null,
"androidAppLink": null,
"androidPackage": null,
"createdOn": 1787314771,
"folder": "Campaigns/Q4",
"title": "Spring sale",
"notes": null,
"warnings": []
}
List links
/api/v1/links
Every link in the workspace the key belongs to, newest first.
| Field | Type | Required | Description |
|---|---|---|---|
page |
integer | optional | 1-based page number. Defaults to 1; anything lower is treated as 1. |
size |
integer | optional | Links per page. Defaults to 20, capped at 100. |
folder |
string | optional | Only links in this folder and everything beneath it, by path: "Campaigns" also returns the links in "Campaigns/Q4". Unlike create, nothing is created here: a path that does not resolve answers 400 rather than listing the whole workspace. |
unfiled |
boolean | optional | Send 1 for only the links in no folder at all. Ignored when folder is given. |
curl -X GET "https://urlicer.com/api/v1/links?page=1&size=3" \
-H "Authorization: Bearer url_live_YOUR_KEY"
{
"links": [
{
"short": "https://ivi.to/mDpk",
"code": "mDpk",
"domain": null,
"url": "https://example.com/blog/2026/the-long-article-title-goes-here",
"clicks": 0,
"active": true,
"expiresOn": null,
"maxClicks": null,
"startsOn": null,
"fallbackUrl": null,
"iosAppLink": null,
"androidAppLink": null,
"androidPackage": null,
"createdOn": 1787314771,
"folder": null,
"title": null,
"notes": null,
"warnings": []
},
{
"short": "https://go.example.com/spring-sale",
"code": "spring-sale",
"domain": "go.example.com",
"url": "https://example.com/spring-sale",
"clicks": 128,
"active": true,
"expiresOn": 1790000000,
"maxClicks": null,
"startsOn": null,
"fallbackUrl": null,
"iosAppLink": null,
"androidAppLink": null,
"androidPackage": null,
"createdOn": 1787314771,
"folder": "Campaigns/Q4",
"title": "Spring sale",
"notes": null,
"warnings": []
}
],
"total": 20077,
"page": 1,
"size": 3
}
| Field | Type | Description |
|---|---|---|
links |
array | The page of links. Each entry has the same shape as a create response. |
total |
integer | How many links the workspace holds in total, across all pages. |
page |
integer | The page you asked for, after clamping. |
size |
integer | The page size actually used, after clamping to 100. |
- Ordering is newest first and stable, so paging through a workspace that is not being written to will not repeat or skip links.
- Click totals on a listing are read live for the whole page in one lookup, so listing 100 links costs no more than listing one.
- A folder filter always includes subfolders, which is what a folder means to whoever filed the links. There is no way to ask for one level only; if you need that, filter the results on the folder field.
Get one link
/api/v1/links/{code}
Look a single link up by its code, including its current click total.
| Field | Type | Required | Description |
|---|---|---|---|
code |
string | required | The code alone, never the whole short URL: "spring-sale", not "https://ivi.to/spring-sale". |
| Field | Type | Required | Description |
|---|---|---|---|
domain |
string | optional | Which domain the code is on: one of your custom domains by host, e.g. "go.example.com", "ivi.to" for the default domain, or "default" for the workspace's default domain. Needed only when the same code exists on more than one of your domains. A domain that is not yours answers 400. The domain does not need to be verified or active, so you can still manage links on a domain you have lost or no longer pay for. |
curl -X GET "https://urlicer.com/api/v1/links/spring-sale" \
-H "Authorization: Bearer url_live_YOUR_KEY"
{
"short": "https://go.example.com/spring-sale",
"code": "spring-sale",
"domain": "go.example.com",
"url": "https://example.com/spring-sale",
"clicks": 128,
"active": true,
"expiresOn": 1790000000,
"maxClicks": null,
"startsOn": null,
"fallbackUrl": null,
"iosAppLink": null,
"androidAppLink": null,
"androidPackage": null,
"createdOn": 1787314771,
"folder": null,
"title": null,
"notes": null,
"warnings": []
}
| Field | Type | Description |
|---|---|---|
short |
string | The finished short link, absolute and ready to share. |
code |
string | Just the code part, without the domain. |
domain |
string or null | The custom domain the link lives on, or null when it uses the default ivi.to. |
url |
string | The destination the link currently points at. |
clicks |
integer | Clicks counted so far. Read live from the click counter, not a nightly total. |
active |
boolean | False for a link you have switched off. Visitors see the "link disabled" page instead of the destination. |
expiresOn |
integer or null | Unix seconds. After this moment the link stops resolving. Null means it never expires. |
maxClicks |
integer or null | The link stops resolving after this many clicks since it was created. Null means no limit. |
startsOn |
integer or null | Unix seconds. Before this moment the link does not resolve yet. Null means it is live from creation. |
fallbackUrl |
string or null | Where visitors go while the link is not live (before startsOn, after expiresOn, past maxClicks) instead of our page. Null means our page. |
iosAppLink |
string or null | The app link iPhone and iPad visitors are offered instead of going straight to url. Null when the link has none. |
androidAppLink |
string or null | The same, for Android visitors. |
androidPackage |
string or null | The Android app's package name, set together with androidAppLink. |
createdOn |
integer | Unix seconds, UTC. |
folder |
string or null | The folder the link is filed in, as a path from the top: "Campaigns/Q4". Null when the link is in no folder. |
title |
string or null | Your workspace's own name for the link, shown in the app instead of the bare code. Null when it has none. |
notes |
string or null | Free-text notes kept with the link. Null when it has none. |
warnings |
array of strings | Empty unless this create or update broke your workspace's UTM naming rules while they are set to warn. Then it says what, and the link was saved anyway. Always empty on get and list. |
- A code belonging to another workspace answers 404, exactly like one that does not exist. The API never confirms that someone else's link is real.
- If the same code exists on more than one of your domains and you leave out domain, the request answers 409 ambiguous_code, naming the domains that hold it. We never pick one for you. The same applies to PATCH and DELETE, so a script cannot change or delete a link you did not mean.
Update a link
/api/v1/links/{code}
Change where a link points, switch it off, or set an expiry or password. Only the fields you send change.
| Field | Type | Required | Description |
|---|---|---|---|
code |
string | required | The code of the link to update. |
| Field | Type | Required | Description |
|---|---|---|---|
domain |
string | optional | Which domain the code is on: one of your custom domains by host, e.g. "go.example.com", "ivi.to" for the default domain, or "default" for the workspace's default domain. Needed only when the same code exists on more than one of your domains. A domain that is not yours answers 400. The domain does not need to be verified or active, so you can still manage links on a domain you have lost or no longer pay for. |
| Field | Type | Required | Description |
|---|---|---|---|
url |
string | optional | A new destination. Re-points the link everywhere it has already been shared. The same rules as create apply, so a denylisted host or an address that is already a short link answers 400. |
active |
boolean | optional | False switches the link off; visitors get the "link disabled" page. True switches it back on. |
expiresOn |
integer or null | optional | Unix seconds to set an expiry, or null to remove one. A time in the past is accepted and expires the link straight away. |
maxClicks |
integer or null | optional, Pro and above | A click limit, or null to remove it. A limit at or below the clicks so far stops the link at once. |
startsOn |
integer or null | optional, Pro and above | Unix seconds for a start, or null to remove it. |
fallbackUrl |
string or null | optional, Pro and above | Where visitors go while the link is not live, or null to show our page. |
iosAppLink |
string or null | optional, Team and above | A link into your iOS app, or null to remove it. |
androidAppLink |
string or null | optional, Team and above | A link into your Android app, or null to remove it. Setting one needs androidPackage, in this request or already on the link. |
androidPackage |
string or null | optional, Team and above | The Android app's package name. Remove it together with androidAppLink. |
password |
string | optional | A new password, or an empty string to remove the one it has. |
folder |
string or null | optional | Moves the link to this folder, by path, creating levels that do not exist. Null takes it out of every folder. Sending the field is what moves the link; leaving it out leaves it where it is. |
title |
string or null | optional | A new name for the link, or null to clear it. |
notes |
string or null | optional | New notes, or null to clear them. |
curl -X PATCH "https://urlicer.com/api/v1/links/spring-sale" \
-H "Authorization: Bearer url_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/spring-sale-2026"}'
{
"short": "https://go.example.com/spring-sale",
"code": "spring-sale",
"domain": "go.example.com",
"url": "https://example.com/spring-sale-2026",
"clicks": 128,
"active": true,
"expiresOn": 1790000000,
"maxClicks": null,
"startsOn": null,
"fallbackUrl": null,
"iosAppLink": null,
"androidAppLink": null,
"androidPackage": null,
"createdOn": 1787314771,
"folder": null,
"title": null,
"notes": null,
"warnings": []
}
| Field | Type | Description |
|---|---|---|
short |
string | The finished short link, absolute and ready to share. |
code |
string | Just the code part, without the domain. |
domain |
string or null | The custom domain the link lives on, or null when it uses the default ivi.to. |
url |
string | The destination the link currently points at. |
clicks |
integer | Clicks counted so far. Read live from the click counter, not a nightly total. |
active |
boolean | False for a link you have switched off. Visitors see the "link disabled" page instead of the destination. |
expiresOn |
integer or null | Unix seconds. After this moment the link stops resolving. Null means it never expires. |
maxClicks |
integer or null | The link stops resolving after this many clicks since it was created. Null means no limit. |
startsOn |
integer or null | Unix seconds. Before this moment the link does not resolve yet. Null means it is live from creation. |
fallbackUrl |
string or null | Where visitors go while the link is not live (before startsOn, after expiresOn, past maxClicks) instead of our page. Null means our page. |
iosAppLink |
string or null | The app link iPhone and iPad visitors are offered instead of going straight to url. Null when the link has none. |
androidAppLink |
string or null | The same, for Android visitors. |
androidPackage |
string or null | The Android app's package name, set together with androidAppLink. |
createdOn |
integer | Unix seconds, UTC. |
folder |
string or null | The folder the link is filed in, as a path from the top: "Campaigns/Q4". Null when the link is in no folder. |
title |
string or null | Your workspace's own name for the link, shown in the app instead of the bare code. Null when it has none. |
notes |
string or null | Free-text notes kept with the link. Null when it has none. |
warnings |
array of strings | Empty unless this create or update broke your workspace's UTM naming rules while they are set to warn. Then it says what, and the link was saved anyway. Always empty on get and list. |
- The code itself cannot be changed. It is already out in the world on posters, emails and chat messages, so a new code means a new link.
- An empty body, {}, answers 400. A body with only field names we do not recognise, such as {"name": "x"}, answers 200 and changes nothing, so check the spelling of the fields you send.
- Changing the destination re-runs our safety check on the new address. A link we had merely flagged is cleared; one we had blocked outright stays blocked.
- If your plan no longer includes a feature, you can still edit links that already use it. You just cannot add it to a link that does not.
- Moving a link between folders is recorded in your workspace's audit trail, naming the key that did it, exactly as a move in the app names the person.
- If your workspace has UTM naming rules (Pro and above), the utm_source, utm_medium and utm_campaign in "url" are checked against them, and lowercased first when the rules say so. A value that is not on a list answers 400 validation when the rules block, or saves the link and fills "warnings" when they warn. Which rules apply depends on the folder the link ends up in: a folder can carry its own, which replace the ones it would otherwise follow, and a folder without any follows the nearest folder above it, then the workspace. On an update only the tags you changed are checked, so a link made before the rules can still be edited.
Delete a link
/api/v1/links/{code}
Remove a link for good. Anyone following it afterwards is redirected (301) to the Urlicer homepage.
| Field | Type | Required | Description |
|---|---|---|---|
code |
string | required | The code of the link to delete. |
| Field | Type | Required | Description |
|---|---|---|---|
domain |
string | optional | Which domain the code is on: one of your custom domains by host, e.g. "go.example.com", "ivi.to" for the default domain, or "default" for the workspace's default domain. Needed only when the same code exists on more than one of your domains. A domain that is not yours answers 400. The domain does not need to be verified or active, so you can still manage links on a domain you have lost or no longer pay for. |
curl -X DELETE "https://urlicer.com/api/v1/links/spring-sale" \
-H "Authorization: Bearer url_live_YOUR_KEY"
{"success": true}
- Deleting is immediate and cannot be undone. To take a link out of circulation while keeping the option to bring it back, PATCH it to "active": false instead.
- Deleting does not give back monthly link allowance: the allowance counts links created, not links held.
- A custom alias becomes free again once the link is deleted. On the default ivi.to domain anyone can then create a link with that code, including another workspace. On your own custom domain only your workspace can. If the code is printed somewhere, switch the link off instead of deleting it.
- The generator never hands out a generated code twice.
- Click history already recorded stays in your analytics, shown against the deleted link.
Times and click counts
Every time in a response is unix seconds in UTC, including
createdOn and expiresOn, and including the expiry you send when
creating a link. Milliseconds will be read as a date tens of thousands of years from now,
which is a valid expiry and will not be rejected. A time in the past is not rejected
either: it creates a link that has already expired. If an expiry behaves oddly, check the
conversion first.
clicks is read live at the moment you ask, from the same counter the redirect
itself increments, so it includes clicks from seconds ago. A create response always shows
0; get, list and update show the current total. It counts redirects served, which
is not the same as people: one person following a link twice is two clicks, and a link
opened from a preview-generating chat app may be counted without anyone having read it.