Build on Linktain.
Three endpoints. Make a locked link, list what you have, read what each one earned. JSON goes in, JSON comes out, and one bearer key opens all of it.
Make a key in Settings, paste it into this, and you have a live locked link in one request. Everything else on this page is detail.
curl -X POST https://linktain.com/api/v1/links \
-H "Authorization: Bearer lt_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{"title":"My download","destinationUrl":"https://example.com/file"}'The shortUrl that comes back is the thing you share. It starts counting the moment somebody opens it.
One header on every request. Keys begin with lt_ and are made in Dashboard, Settings.
Authorization: Bearer lt_your_api_key_here
We store a hash, never the key itself. Lose it and there is nothing to look up: revoke it and make another.
The next request with a revoked key gets a 401. There is no grace period and nothing is cached.
A key can see and change everything you can. Anything done with it is done by you, so keep it off the client side.
A few requests a second and you will never hear from us. We would rather tell you the truth than invent a number.
Every amount in this API is an integer number of mills. A mill is a thousandth of a dollar, so 1000 mills is $1.00 and 6420 mills is $6.42.
Integers, because money in a float is money that eventually does not add up. Divide by 1000 at the moment you print it and never before.
const usd = (mills) => (mills / 1000).toFixed(2); usd(6420); // "6.42"
Recent days can still move. A partner reversing a conversion takes it back off the balance it landed on, sometimes weeks later, so treat the last few days as provisional rather than final.
Wrap any URL in a gate and get a short link back.
The link is live the moment this returns. Share the shortUrl and it starts counting views.
The gate a visitor sees is the one configured on your account, not something you pass here. Change it in the dashboard and every link you have already made changes with it.
titlestringrequired- What you call it in your dashboard. 1 to 120 characters. Visitors never see it.
destinationUrlstringrequired- Where the visitor lands once the gate is finished. Must start with http:// or https://. Up to 2000 characters.
curl -X POST https://linktain.com/api/v1/links \
-H "Authorization: Bearer lt_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{"title":"My download","destinationUrl":"https://example.com/file"}'{
"id": "cmd7x1abc0001xyz",
"slug": "x7k2m9a",
"shortUrl": "https://linktain.com/l/x7k2m9a",
"title": "My download",
"destinationUrl": "https://example.com/file",
"createdAt": "2026-07-27T12:00:00.000Z"
}idstring- Use it for the stats endpoint.
slugstring- The short code on the end of the URL.
shortUrlstring- The link to share. This is the one you want.
titlestring- What you sent.
destinationUrlstring- What you sent.
createdAtstring- ISO 8601, UTC.
Every link on your account, newest first, with its lifetime totals.
There is no paging and no limit. You get all of them in one response, which is the right shape while an account has hundreds of links and the wrong shape if it ever has a hundred thousand. If you are near that, tell us and we will add a cursor.
curl https://linktain.com/api/v1/links \ -H "Authorization: Bearer lt_your_api_key_here"
{
"links": [
{
"id": "cmd7x1abc0001xyz",
"slug": "x7k2m9a",
"shortUrl": "https://linktain.com/l/x7k2m9a",
"title": "My download",
"destinationUrl": "https://example.com/file",
"active": true,
"views": 1523,
"unlocks": 1284,
"earnedMills": 6420,
"createdAt": "2026-07-27T12:00:00.000Z"
}
]
}linksarray- The list. Newest first.
links[].idstring- Pass this to the stats endpoint.
links[].slugstring- The short code.
links[].shortUrlstring- The full link to share.
links[].titlestring- Your name for it.
links[].destinationUrlstring- Where it sends people.
links[].activeboolean- False if you paused it or we disabled it. A disabled link serves nothing.
links[].viewsinteger- Times the gate was opened, lifetime.
links[].unlocksinteger- Times a visitor finished the gate, lifetime.
links[].earnedMillsinteger- Everything this link has earned you, in mills. Both money paths added together.
links[].createdAtstring- ISO 8601, UTC.
Lifetime totals, the earnings split, and a day by day series.
Ask for a link that is not yours and you get a 404, the same answer as a link that does not exist. We do not confirm that somebody else's id is real.
unlocks counts every finished gate. paidUnlocks counts the ones that earned, and the two differ because the same visitor coming back inside 24 hours unlocks again and earns nothing. Dividing earnings by unlocks gives you a rate that looks lower than the one you are on; divide by paidUnlocks instead.
curl https://linktain.com/api/v1/links/cmd7x1abc0001xyz/stats \ -H "Authorization: Bearer lt_your_api_key_here"
{
"views": 1523,
"unlocks": 1284,
"paidUnlocks": 1180,
"conversions": 47,
"unlockMills": 1180,
"offerMills": 5240,
"earnedMills": 6420,
"last14d": [
{ "date": "2026-07-14", "views": 96, "unlocks": 80 },
{ "date": "2026-07-15", "views": 112, "unlocks": 95 }
]
}viewsinteger- Times the gate was opened.
unlocksinteger- Times a visitor finished the gate.
paidUnlocksinteger- Unlocks that earned. One visitor pays once per 24 hours.
conversionsinteger- Sponsored steps a visitor actually completed.
unlockMillsinteger- Your share from the flat per-unlock rate, in mills.
offerMillsinteger- Your share of what advertisers paid for completed steps, in mills. On most accounts this is nearly all of it.
earnedMillsinteger- unlockMills plus offerMills. The number your balance moved by.
last14darray- Fourteen UTC days, oldest first, zero filled. Days with nothing still appear.
last14d[].datestring- UTC calendar date, YYYY-MM-DD.
last14d[].viewsinteger- Gate opens that day.
last14d[].unlocksinteger- Finished gates that day.
One tag on a site you own turns every outbound link on it into a locker. You do not rewrite any hrefs and you do not create the links: the first time somebody clicks one we make it, and every click after that uses the same one.
<script src="https://linktain.com/e.js" data-key="pk_your_site_key"></script>
Register the site in Dashboard, Links to get the key. It is public on purpose, because it sits in your page source where anybody can read it. What stops somebody else using it is the hostname you registered against it: a request from any other site is refused.
Links to your own pages, anchors, mailto: and tel:, downloads, middle clicks and ctrl-clicks.
If we are slow or down, the click goes straight through to where it was already going. Your site does not break because ours did.
<a href="https://example.com/sponsor" data-linktain="off">never locked</a>
<script src="https://linktain.com/e.js"
data-key="pk_your_site_key"
data-selector="a.paid"></script>Every failure is the same shape, { "error": "..." }, with the status carrying the category and the string saying what to fix.
- 400
- The body was not JSON, or a field failed validation.The message names the field, e.g. "destinationUrl: must use http:// or https://". Fix it and send again.
- 401
- No Authorization header, or the key is wrong, or you revoked it.Check the header reads Bearer lt_… and that the key is still listed in Settings.
- 403
- The account cannot use the API: awaiting approval, suspended, or banned.The message says which. Approval is the usual one and it is a person, not a queue you can retry past.
- 404
- No link with that id on your account.Check the id came from your own list. Somebody else's id answers 404 too.
- 500
- We broke.Retry once. If it keeps happening, tell us on Telegram and include roughly when.
- 503
- Could not allocate a short code, which takes an unlucky run of collisions.Retry. It is transient and it clears immediately.
Get a key and make the first call.
Keys live in Settings. If something here is wrong or missing, say so on Telegram at @LinksWithAndrew and it gets fixed, usually the same day.
Create an API key