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.

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.