REST API — free on every plan

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.

POST /api/v1/links
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.

Truthylink Assistant

Ask about pricing & features

Hi! Ask me anything about Truthylink's plans and features.