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.
curl -X POST https://truthylink.com/api/v1/links \
-H "Authorization: Bearer $TOKEN" \
-d '{"original_url":"https://example.com/campaign"}'
{
"slug": "campaign",
"short_url": "truthylink.com/campaign",
"safety_status": "safe"
}
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 }
}
}
MCP Server
Connect Claude Desktop, Claude Code, or any MCP client to your Truthylink account and manage links in natural language. Same token, same rate limits as the REST API above.
Server URL
https://app.truthylink.com/api/mcp
Claude Desktop config
{
"mcpServers": {
"truthylink": {
"url": "https://app.truthylink.com/api/mcp",
"headers": { "Authorization": "Bearer <token>" }
}
}
}
Tools
list_links
— List your links, newest first.
get_link
— Get full details for one link by slug.
get_link_stats
— Click analytics for one link.
create_link
— Create a new short link.
update_link
— Update an existing link.
delete_link
— Permanently delete a link.
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.