Build on Truthylink
with a token, not a sales call.
Full CRUD on links plus click stats — create, update, and monitor links from your own scripts, CI pipelines, or product. Every plan gets API access, rate-limited by tier.
Quick Reference
Base URL
https://app.truthylink.com/api/v1
Authentication
Authorization: Bearer <token>
Get a token from Developer Settings after signing up — no credit card, no sales call.
Endpoints
Full CRUD on links, plus click stats. Every response is JSON. Fields marked Pro+ require a paid plan.
Returns your links, newest first. Supports pagination.
| Param | In | Type | Required | Notes |
|---|---|---|---|---|
| page | query | integer | Optional | Default 1. |
| per_page | query | integer | Optional | Default 25. Max 100. |
Request
curl https://app.truthylink.com//api/v1/links?page=1&per_page=25 \ -H "Authorization: Bearer <token>"
import requests
response = requests.get(
"https://app.truthylink.com//api/v1/links",
headers={"Authorization": "Bearer <token>"},
params={"page": 1, "per_page": 25}
)
print(response.json())
const res = await fetch("https://app.truthylink.com//api/v1/links?page=1&per_page=25", {
headers: { Authorization: "Bearer <token>" }
});
const data = await res.json();
require "net/http"
require "json"
uri = URI("https://app.truthylink.com//api/v1/links")
uri.query = URI.encode_www_form(page: 1, per_page: 25)
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer <token>"
res = Net::HTTP.start(uri.host, uri.port) { |http| http.request(req) }
JSON.parse(res.body)
$ch = curl_init("https://app.truthylink.com//api/v1/links?page=1&per_page=25");
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer <token>"]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true);
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://app.truthylink.com//api/v1/links?page=1&per_page=25"))
.header("Authorization", "Bearer <token>")
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
Response
HTTP/1.1 200 OK
{
"data": [
{
"slug": "summer-sale",
"short_url": "https://app.truthylink.com//summer-sale",
"original_url": "https://example.com/products/summer-collection",
"title": "Summer Sale Campaign",
"clicks_count": 482,
"password_protected": false,
"expires_at": null,
"max_clicks": null,
"status": "active",
"created_at": "2026-06-12T09:14:22Z",
"updated_at": "2026-07-01T16:40:05Z"
}
],
"meta": { "total": 1, "page": 1, "per_page": 25, "pages": 1 }
}
Creates a link. Omit slug to auto-generate one.
| Param | In | Type | Required | Notes |
|---|---|---|---|---|
| original_url | body | string | Required | Must be a valid http(s) URL. Max 2000 chars. |
| slug | body | string | Optional | 3–50 chars, letters/numbers/-/_. Auto-generated if omitted. |
| title | body | string | Optional | Max 200 chars. |
| password_protected | body | boolean | Optional Pro+ | Requires Pro plan or higher. |
| password | body | string | Optional Pro+ | Set when password_protected is true. Requires Pro plan or higher. |
| expires_at | body | datetime (ISO 8601) | Optional Pro+ | Requires Pro plan or higher. |
| max_clicks | body | integer | Optional Pro+ | Must be greater than 0. Requires Pro plan or higher. |
Request
curl -X POST https://app.truthylink.com//api/v1/links \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"link":{"original_url":"https://example.com","title":"My link","slug":"my-link"}}'
import requests
response = requests.post(
"https://app.truthylink.com//api/v1/links",
headers={"Authorization": "Bearer <token>"},
json={
"link": {
"original_url": "https://example.com",
"title": "My link",
"slug": "my-link"
}
}
)
print(response.json())
const res = await fetch("https://app.truthylink.com//api/v1/links", {
method: "POST",
headers: {
Authorization: "Bearer <token>",
"Content-Type": "application/json"
},
body: JSON.stringify({
link: {
original_url: "https://example.com",
title: "My link",
slug: "my-link"
}
})
});
const data = await res.json();
require "net/http"
require "json"
uri = URI("https://app.truthylink.com//api/v1/links")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer <token>"
req["Content-Type"] = "application/json"
req.body = {
link: {
original_url: "https://example.com",
title: "My link",
slug: "my-link"
}
}.to_json
res = Net::HTTP.start(uri.host, uri.port) { |http| http.request(req) }
JSON.parse(res.body)
$ch = curl_init("https://app.truthylink.com//api/v1/links");
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer <token>",
"Content-Type: application/json"
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
"link" => [
"original_url" => "https://example.com",
"title" => "My link",
"slug" => "my-link"
]
]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true);
String body = "{\"link\":{\"original_url\":\"https://example.com\",\"title\":\"My link\",\"slug\":\"my-link\"}}";
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://app.truthylink.com//api/v1/links"))
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
Response
HTTP/1.1 201 Created
{
"data": {
"slug": "my-link",
"short_url": "https://app.truthylink.com//my-link",
"original_url": "https://example.com",
"title": "My link",
"clicks_count": 0,
"password_protected": false,
"expires_at": null,
"max_clicks": null,
"status": "active",
"created_at": "2026-07-03T10:02:11Z",
"updated_at": "2026-07-03T10:02:11Z"
}
}
Fetches a single link by its slug.
| Param | In | Type | Required | Notes |
|---|---|---|---|---|
| slug | path | string | Required | The link's slug, e.g. my-link. |
Request
curl https://app.truthylink.com//api/v1/links/my-link \ -H "Authorization: Bearer <token>"
import requests
response = requests.get(
"https://app.truthylink.com//api/v1/links/my-link",
headers={"Authorization": "Bearer <token>"}
)
print(response.json())
const res = await fetch("https://app.truthylink.com//api/v1/links/my-link", {
headers: { Authorization: "Bearer <token>" }
});
const data = await res.json();
require "net/http"
require "json"
uri = URI("https://app.truthylink.com//api/v1/links/my-link")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer <token>"
res = Net::HTTP.start(uri.host, uri.port) { |http| http.request(req) }
JSON.parse(res.body)
$ch = curl_init("https://app.truthylink.com//api/v1/links/my-link");
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer <token>"]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true);
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://app.truthylink.com//api/v1/links/my-link"))
.header("Authorization", "Bearer <token>")
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
Response
HTTP/1.1 200 OK
{
"data": {
"slug": "my-link",
"short_url": "https://app.truthylink.com//my-link",
"original_url": "https://example.com",
"title": "My link",
"clicks_count": 128,
"password_protected": false,
"expires_at": null,
"max_clicks": null,
"status": "active",
"created_at": "2026-07-03T10:02:11Z",
"updated_at": "2026-07-03T10:02:11Z"
}
}
Updates any subset of a link's fields.
| Param | In | Type | Required | Notes |
|---|---|---|---|---|
| slug | path | string | Required | The link's current slug, e.g. my-link. |
| original_url | body | string | Optional | Must be a valid http(s) URL. Max 2000 chars. |
| slug | body | string | Optional | Renames the link's slug. Requires uniqueness. |
| title | body | string | Optional | Max 200 chars. |
| password_protected | body | boolean | Optional Pro+ | Requires Pro plan or higher. |
| password | body | string | Optional Pro+ | Requires Pro plan or higher. |
| expires_at | body | datetime (ISO 8601) | Optional Pro+ | Requires Pro plan or higher. |
| max_clicks | body | integer | Optional Pro+ | Must be greater than 0. Requires Pro plan or higher. |
Request
curl -X PATCH https://app.truthylink.com//api/v1/links/my-link \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"link":{"title":"Updated title","max_clicks":1000}}'
import requests
response = requests.patch(
"https://app.truthylink.com//api/v1/links/my-link",
headers={"Authorization": "Bearer <token>"},
json={
"link": {
"title": "Updated title",
"max_clicks": 1000
}
}
)
print(response.json())
const res = await fetch("https://app.truthylink.com//api/v1/links/my-link", {
method: "PATCH",
headers: {
Authorization: "Bearer <token>",
"Content-Type": "application/json"
},
body: JSON.stringify({
link: {
title: "Updated title",
max_clicks: 1000
}
})
});
const data = await res.json();
require "net/http"
require "json"
uri = URI("https://app.truthylink.com//api/v1/links/my-link")
req = Net::HTTP::Patch.new(uri)
req["Authorization"] = "Bearer <token>"
req["Content-Type"] = "application/json"
req.body = {
link: {
title: "Updated title",
max_clicks: 1000
}
}.to_json
res = Net::HTTP.start(uri.host, uri.port) { |http| http.request(req) }
JSON.parse(res.body)
$ch = curl_init("https://app.truthylink.com//api/v1/links/my-link");
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PATCH");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer <token>",
"Content-Type: application/json"
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
"link" => [
"title" => "Updated title",
"max_clicks" => 1000
]
]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true);
String body = "{\"link\":{\"title\":\"Updated title\",\"max_clicks\":1000}}";
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://app.truthylink.com//api/v1/links/my-link"))
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.method("PATCH", HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
Response
HTTP/1.1 200 OK
{
"data": {
"slug": "my-link",
"short_url": "https://app.truthylink.com//my-link",
"original_url": "https://example.com",
"title": "Updated title",
"clicks_count": 128,
"password_protected": false,
"expires_at": null,
"max_clicks": 1000,
"status": "active",
"created_at": "2026-07-03T10:02:11Z",
"updated_at": "2026-07-03T11:47:52Z"
}
}
Deletes a link permanently. This cannot be undone.
| Param | In | Type | Required | Notes |
|---|---|---|---|---|
| slug | path | string | Required | The link's slug, e.g. my-link. |
Request
curl -X DELETE https://app.truthylink.com//api/v1/links/my-link \ -H "Authorization: Bearer <token>"
import requests
response = requests.delete(
"https://app.truthylink.com//api/v1/links/my-link",
headers={"Authorization": "Bearer <token>"}
)
print(response.json())
const res = await fetch("https://app.truthylink.com//api/v1/links/my-link", {
method: "DELETE",
headers: { Authorization: "Bearer <token>" }
});
const data = await res.json();
require "net/http"
require "json"
uri = URI("https://app.truthylink.com//api/v1/links/my-link")
req = Net::HTTP::Delete.new(uri)
req["Authorization"] = "Bearer <token>"
res = Net::HTTP.start(uri.host, uri.port) { |http| http.request(req) }
JSON.parse(res.body)
$ch = curl_init("https://app.truthylink.com//api/v1/links/my-link");
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "DELETE");
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer <token>"]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true);
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://app.truthylink.com//api/v1/links/my-link"))
.header("Authorization", "Bearer <token>")
.DELETE()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
Response
HTTP/1.1 200 OK
{ "message": "Link deleted" }
Click stats for a link. Defaults to your plan's full analytics retention window.
| Param | In | Type | Required | Notes |
|---|---|---|---|---|
| slug | path | string | Required | The link's slug, e.g. my-link. |
| days | query | integer | Optional | Defaults to your plan's analytics retention (7/30/90 days). Clamped to that max. |
Request
curl https://app.truthylink.com//api/v1/links/my-link/stats?days=30 \ -H "Authorization: Bearer <token>"
import requests
response = requests.get(
"https://app.truthylink.com//api/v1/links/my-link/stats",
headers={"Authorization": "Bearer <token>"},
params={"days": 30}
)
print(response.json())
const res = await fetch("https://app.truthylink.com//api/v1/links/my-link/stats?days=30", {
headers: { Authorization: "Bearer <token>" }
});
const data = await res.json();
require "net/http"
require "json"
uri = URI("https://app.truthylink.com//api/v1/links/my-link/stats")
uri.query = URI.encode_www_form(days: 30)
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer <token>"
res = Net::HTTP.start(uri.host, uri.port) { |http| http.request(req) }
JSON.parse(res.body)
$ch = curl_init("https://app.truthylink.com//api/v1/links/my-link/stats?days=30");
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer <token>"]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true);
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://app.truthylink.com//api/v1/links/my-link/stats?days=30"))
.header("Authorization", "Bearer <token>")
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
Response
HTTP/1.1 200 OK
{
"data": {
"slug": "my-link",
"total_clicks": 428,
"bot_clicks": 54,
"clicks_by_date": { "2026-07-03": 12, "2026-07-02": 20 },
"top_countries": { "IN": 210, "US": 88 },
"top_referrers": { "direct": 120, "https://twitter.com/": 40 },
"top_devices": { "mobile": 260, "desktop": 168 },
"top_browsers": { "Chrome": 300, "Safari": 90 }
}
}
Rate limits by plan
Exceeding your limit returns a 429. Limits are per token, per minute.
| Plan | Requests / minute |
|---|---|
| free | 20 |
| pro | 60 |
| business | 100 |
| enterprise | 300 |
Errors
| Status | When | Example body |
|---|---|---|
| 401 | Missing or invalid token | { "error": "Invalid API token" } |
| 403 | Plan limit hit (e.g. link cap, feature gate) | { "error": "Link limit reached for the Free plan. Upgrade to create more links." } |
| 404 | Slug doesn't exist or isn't yours | { "error": "Not found" } |
| 422 | Validation failed | { "errors": ["Original url can't be blank"] } |
| 429 | Rate limit exceeded | { "error": "Rate limit exceeded. Max 20 requests per minute on the free plan." } |
Ready to build?
Sign up free, generate a token in Developer Settings, and start making requests in minutes.