T.LY API

Create and update short links, customize QR codes, and track short links and OneLinks from your own code. Send requests over HTTPS with an API token from your account. Responses are JSON unless you request plain text or a QR code image.

Base URL https://api.t.ly
Create your first short link
curl -X POST https://api.t.ly/api/v1/link/shorten \
  -H 'Authorization: Bearer YOUR_API_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{"long_url": "https://example.com/"}'
const response = await fetch("https://api.t.ly/api/v1/link/shorten", {
    method: "POST",
    headers: {
        "Authorization": "Bearer YOUR_API_TOKEN",
        "Content-Type": "application/json",
    },
    body: JSON.stringify({ long_url: "https://example.com/" }),
});
const link = await response.json();
console.log(link.short_url);
$client = new \GuzzleHttp\Client();
$response = $client->post('https://api.t.ly/api/v1/link/shorten', [
    'headers' => ['Authorization' => 'Bearer YOUR_API_TOKEN'],
    'json' => ['long_url' => 'https://example.com/'],
]);
$link = json_decode((string) $response->getBody(), true);
echo $link['short_url'];
import requests

response = requests.post(
    "https://api.t.ly/api/v1/link/shorten",
    headers={"Authorization": "Bearer YOUR_API_TOKEN"},
    json={"long_url": "https://example.com/"},
)
print(response.json()["short_url"])
200 OK
{
    "short_url": "https://t.ly/c55j",
    "long_url": "https://example.com/",
    "domain": "https://t.ly/",
    "short_id": "c55j",
    "public_stats": false
}

Quick start

  1. Create a token

    Open Settings › API and create a token. Keep it secret, like a password.

  2. Send it with every request

    Add an Authorization: Bearer YOUR_API_TOKEN header.

  3. Shorten a link

    Send a long_url to Create a short link and you get the short URL back.

API reference

Authentication

Send your API token in the Authorization header as Bearer YOUR_API_TOKEN. Requests without a valid token get a 401.

Each API token is tied to a team. Links you create belong to that team. Create and revoke tokens in Settings › API.

Use Get the current user to see which user is associated with your token.

Rate limits and errors

These limits apply per user and are shared across that user's API tokens. Each row has a separate counter. Requests in one row do not use another row's allowance.

Requests per minute with an API token
RequestsPer minute
Create a short link1,000
Other API endpoints500

Listing links, bulk requests, analytics, QR codes and the other endpoints in this reference share the general counter.

Monthly link allowance

Your plan also limits how many links your team can create each month. This is separate from the per-minute limits above. Every link counts, whether a teammate made it in the app or through the API.

If you reach it, waiting a minute won't help. Upgrade your plan, or wait until the 1st of next month when it resets. See your limit in Settings › Subscription.

Use Get team usage to check monthly usage, reset dates and plan allowances in one request.

When to retry

For a temporary 429, wait at least the number of seconds in Retry-After. If it is missing, use exponential backoff with jitter and stop after a few attempts. If message says the monthly allowance was reached, stop retrying until the allowance changes.

Successful responses include X-RateLimit-Limit and X-RateLimit-Remaining for the request's group. A throttled response also includes X-RateLimit-Reset, a Unix timestamp, and Retry-After.

Error responses

Error status codes
StatusMeaning
401The token is missing or invalid.
402Your team needs an active subscription.
403Your plan doesn't include this, or the request was blocked.
404The link or item wasn't found.
409The webhook is disabled. Enable it before resending a delivery.
410The short link is disabled.
422A field or product rule rejected the request. Check message and, when present, errors.
429Check message: a rate limit or your monthly link allowance was reached. Honor Retry-After when present.
503The visit-log query timed out. Retry with a shorter date range.

API errors and safe retries

More ways to connect

QR codes

Download a short link's QR code, or change its colors, style and logo.

Team analytics

Report on visits, top links and traffic sources across your team, or page through individual visits and QR scans.

Get team visits

GET https://api.t.ly/api/v1/team/stats/visits

Returns visit totals across matching links. Reports use the token's team and aggregate daily analytics. total includes QR visits; qr_total is the QR subset. unique_total adds daily unique counts and is not a count of distinct people across the whole period. Counts in grouped rows can be numeric strings. Results may be cached for a short period. Filters apply together; values within an array match any listed value. Use UTC calendar dates for consistent day boundaries.

Request
curl -X GET 'https://api.t.ly/api/v1/team/stats/visits?start_date=2026-11-01&end_date=2026-11-07T23%3A59%3A59Z&domains%5B0%5D=https%3A%2F%2Ft.ly%2F&link_type%5B0%5D=links&link_type%5B1%5D=one_links' \
  -H 'Authorization: Bearer YOUR_API_TOKEN'
const url = new URL(
    "https://api.t.ly/api/v1/team/stats/visits"
);

const params = {
    "start_date": "2026-11-01",
    "end_date": "2026-11-07T23:59:59Z",
    "tag_ids": "",
    "pixel_ids": "",
    "domains[0]": "https://t.ly/",
    "user_ids": "",
    "link_type[0]": "links",
    "link_type[1]": "one_links",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer YOUR_API_TOKEN",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.t.ly/api/v1/team/stats/visits';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer YOUR_API_TOKEN',
        ],
        'query' => [
            'start_date' => '2026-11-01',
            'end_date' => '2026-11-07T23:59:59Z',
            'tag_ids' => '',
            'pixel_ids' => '',
            'domains[0]' => 'https://t.ly/',
            'user_ids' => '',
            'link_type[0]' => 'links',
            'link_type[1]' => 'one_links',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.t.ly/api/v1/team/stats/visits'
params = {
  'start_date': '2026-11-01',
  'end_date': '2026-11-07T23:59:59Z',
  'tag_ids': '',
  'pixel_ids': '',
  'domains[0]': 'https://t.ly/',
  'user_ids': '',
  'link_type[0]': 'links',
  'link_type[1]': 'one_links',
}
headers = {
  'Authorization': 'Bearer YOUR_API_TOKEN'
}

response = requests.request('GET', url, headers=headers, params=params)
response.json()
Send a real request with your own token.
Response
{
    "total": 120,
    "unique_total": 95,
    "qr_total": 20
}

Query parameters

  • start_date string
    Start of the reporting range. Use a UTC calendar date. Defaults to the start of the day 30 days ago.

    Example: 2026-11-01

  • end_date string
    End of the reporting range. Use a UTC end-of-day timestamp to include a complete day. Defaults to the end of today.

    Example: 2026-11-07T23:59:59Z

  • tag_ids array of integers
    Match links with any of these tag IDs. Up to 10.
  • pixel_ids array of integers
    Match short links with any of these pixel IDs. Up to 10. This filter does not apply to OneLinks.
  • domains array of strings
    Domain URL strings, including scheme and trailing slash. Up to 10.

    Example: ["https://t.ly/"]

  • user_ids array of integers
    IDs of the team members who created the links. Up to 10. Find them with List team members.
  • link_type array of strings
    Include links, one_links, or both. Defaults to ["links"].

    Example: ["links","one_links"]

Get daily team visits

GET https://api.t.ly/api/v1/team/stats/daily-visits

Returns matching visits by day, oldest day first. Days without recorded visits are omitted. Reports use the token's team and aggregate daily analytics. total includes QR visits; qr_total is the QR subset. unique_total adds daily unique counts and is not a count of distinct people across the whole period. Counts in grouped rows can be numeric strings. Results may be cached for a short period. Filters apply together; values within an array match any listed value. Use UTC calendar dates for consistent day boundaries.

Request
curl -X GET 'https://api.t.ly/api/v1/team/stats/daily-visits?start_date=2026-11-01&end_date=2026-11-07T23%3A59%3A59Z&domains%5B0%5D=https%3A%2F%2Ft.ly%2F&link_type%5B0%5D=links&link_type%5B1%5D=one_links' \
  -H 'Authorization: Bearer YOUR_API_TOKEN'
const url = new URL(
    "https://api.t.ly/api/v1/team/stats/daily-visits"
);

const params = {
    "start_date": "2026-11-01",
    "end_date": "2026-11-07T23:59:59Z",
    "tag_ids": "",
    "pixel_ids": "",
    "domains[0]": "https://t.ly/",
    "user_ids": "",
    "link_type[0]": "links",
    "link_type[1]": "one_links",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer YOUR_API_TOKEN",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.t.ly/api/v1/team/stats/daily-visits';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer YOUR_API_TOKEN',
        ],
        'query' => [
            'start_date' => '2026-11-01',
            'end_date' => '2026-11-07T23:59:59Z',
            'tag_ids' => '',
            'pixel_ids' => '',
            'domains[0]' => 'https://t.ly/',
            'user_ids' => '',
            'link_type[0]' => 'links',
            'link_type[1]' => 'one_links',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.t.ly/api/v1/team/stats/daily-visits'
params = {
  'start_date': '2026-11-01',
  'end_date': '2026-11-07T23:59:59Z',
  'tag_ids': '',
  'pixel_ids': '',
  'domains[0]': 'https://t.ly/',
  'user_ids': '',
  'link_type[0]': 'links',
  'link_type[1]': 'one_links',
}
headers = {
  'Authorization': 'Bearer YOUR_API_TOKEN'
}

response = requests.request('GET', url, headers=headers, params=params)
response.json()
Send a real request with your own token.
Response
[
    {
        "label": "2026-11-01",
        "total": "120",
        "unique_total": "95",
        "qr_total": "20"
    }
]

Query parameters

  • start_date string
    Start of the reporting range. Use a UTC calendar date. Defaults to the start of the day 30 days ago.

    Example: 2026-11-01

  • end_date string
    End of the reporting range. Use a UTC end-of-day timestamp to include a complete day. Defaults to the end of today.

    Example: 2026-11-07T23:59:59Z

  • tag_ids array of integers
    Match links with any of these tag IDs. Up to 10.
  • pixel_ids array of integers
    Match short links with any of these pixel IDs. Up to 10. This filter does not apply to OneLinks.
  • domains array of strings
    Domain URL strings, including scheme and trailing slash. Up to 10.

    Example: ["https://t.ly/"]

  • user_ids array of integers
    IDs of the team members who created the links. Up to 10. Find them with List team members.
  • link_type array of strings
    Include links, one_links, or both. Defaults to ["links"].

    Example: ["links","one_links"]

Get team countries

GET https://api.t.ly/api/v1/team/stats/countries

Returns visits grouped by countries. Row order is unspecified; sort in your client if needed. Reports use the token's team and aggregate daily analytics. total includes QR visits; qr_total is the QR subset. unique_total adds daily unique counts and is not a count of distinct people across the whole period. Counts in grouped rows can be numeric strings. Results may be cached for a short period. Filters apply together; values within an array match any listed value. Use UTC calendar dates for consistent day boundaries.

Request
curl -X GET 'https://api.t.ly/api/v1/team/stats/countries?start_date=2026-11-01&end_date=2026-11-07T23%3A59%3A59Z&domains%5B0%5D=https%3A%2F%2Ft.ly%2F&link_type%5B0%5D=links&link_type%5B1%5D=one_links' \
  -H 'Authorization: Bearer YOUR_API_TOKEN'
const url = new URL(
    "https://api.t.ly/api/v1/team/stats/countries"
);

const params = {
    "start_date": "2026-11-01",
    "end_date": "2026-11-07T23:59:59Z",
    "tag_ids": "",
    "pixel_ids": "",
    "domains[0]": "https://t.ly/",
    "user_ids": "",
    "link_type[0]": "links",
    "link_type[1]": "one_links",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer YOUR_API_TOKEN",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.t.ly/api/v1/team/stats/countries';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer YOUR_API_TOKEN',
        ],
        'query' => [
            'start_date' => '2026-11-01',
            'end_date' => '2026-11-07T23:59:59Z',
            'tag_ids' => '',
            'pixel_ids' => '',
            'domains[0]' => 'https://t.ly/',
            'user_ids' => '',
            'link_type[0]' => 'links',
            'link_type[1]' => 'one_links',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.t.ly/api/v1/team/stats/countries'
params = {
  'start_date': '2026-11-01',
  'end_date': '2026-11-07T23:59:59Z',
  'tag_ids': '',
  'pixel_ids': '',
  'domains[0]': 'https://t.ly/',
  'user_ids': '',
  'link_type[0]': 'links',
  'link_type[1]': 'one_links',
}
headers = {
  'Authorization': 'Bearer YOUR_API_TOKEN'
}

response = requests.request('GET', url, headers=headers, params=params)
response.json()
Send a real request with your own token.
Response
[
    {
        "country_code": "US",
        "total": "120",
        "unique_total": "95",
        "qr_total": "20"
    }
]

Query parameters

  • start_date string
    Start of the reporting range. Use a UTC calendar date. Defaults to the start of the day 30 days ago.

    Example: 2026-11-01

  • end_date string
    End of the reporting range. Use a UTC end-of-day timestamp to include a complete day. Defaults to the end of today.

    Example: 2026-11-07T23:59:59Z

  • tag_ids array of integers
    Match links with any of these tag IDs. Up to 10.
  • pixel_ids array of integers
    Match short links with any of these pixel IDs. Up to 10. This filter does not apply to OneLinks.
  • domains array of strings
    Domain URL strings, including scheme and trailing slash. Up to 10.

    Example: ["https://t.ly/"]

  • user_ids array of integers
    IDs of the team members who created the links. Up to 10. Find them with List team members.
  • link_type array of strings
    Include links, one_links, or both. Defaults to ["links"].

    Example: ["links","one_links"]

Get team cities

GET https://api.t.ly/api/v1/team/stats/cities

Returns visits grouped by cities. Row order is unspecified; sort in your client if needed. Reports use the token's team and aggregate daily analytics. total includes QR visits; qr_total is the QR subset. unique_total adds daily unique counts and is not a count of distinct people across the whole period. Counts in grouped rows can be numeric strings. Results may be cached for a short period. Filters apply together; values within an array match any listed value. Use UTC calendar dates for consistent day boundaries.

Request
curl -X GET 'https://api.t.ly/api/v1/team/stats/cities?start_date=2026-11-01&end_date=2026-11-07T23%3A59%3A59Z&domains%5B0%5D=https%3A%2F%2Ft.ly%2F&link_type%5B0%5D=links&link_type%5B1%5D=one_links' \
  -H 'Authorization: Bearer YOUR_API_TOKEN'
const url = new URL(
    "https://api.t.ly/api/v1/team/stats/cities"
);

const params = {
    "start_date": "2026-11-01",
    "end_date": "2026-11-07T23:59:59Z",
    "tag_ids": "",
    "pixel_ids": "",
    "domains[0]": "https://t.ly/",
    "user_ids": "",
    "link_type[0]": "links",
    "link_type[1]": "one_links",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer YOUR_API_TOKEN",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.t.ly/api/v1/team/stats/cities';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer YOUR_API_TOKEN',
        ],
        'query' => [
            'start_date' => '2026-11-01',
            'end_date' => '2026-11-07T23:59:59Z',
            'tag_ids' => '',
            'pixel_ids' => '',
            'domains[0]' => 'https://t.ly/',
            'user_ids' => '',
            'link_type[0]' => 'links',
            'link_type[1]' => 'one_links',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.t.ly/api/v1/team/stats/cities'
params = {
  'start_date': '2026-11-01',
  'end_date': '2026-11-07T23:59:59Z',
  'tag_ids': '',
  'pixel_ids': '',
  'domains[0]': 'https://t.ly/',
  'user_ids': '',
  'link_type[0]': 'links',
  'link_type[1]': 'one_links',
}
headers = {
  'Authorization': 'Bearer YOUR_API_TOKEN'
}

response = requests.request('GET', url, headers=headers, params=params)
response.json()
Send a real request with your own token.
Response
[
    {
        "country_code": "US",
        "region": "NY",
        "city": "New York City",
        "total": "120",
        "unique_total": "95",
        "qr_total": "20"
    }
]

Query parameters

  • start_date string
    Start of the reporting range. Use a UTC calendar date. Defaults to the start of the day 30 days ago.

    Example: 2026-11-01

  • end_date string
    End of the reporting range. Use a UTC end-of-day timestamp to include a complete day. Defaults to the end of today.

    Example: 2026-11-07T23:59:59Z

  • tag_ids array of integers
    Match links with any of these tag IDs. Up to 10.
  • pixel_ids array of integers
    Match short links with any of these pixel IDs. Up to 10. This filter does not apply to OneLinks.
  • domains array of strings
    Domain URL strings, including scheme and trailing slash. Up to 10.

    Example: ["https://t.ly/"]

  • user_ids array of integers
    IDs of the team members who created the links. Up to 10. Find them with List team members.
  • link_type array of strings
    Include links, one_links, or both. Defaults to ["links"].

    Example: ["links","one_links"]

Get team referrers

GET https://api.t.ly/api/v1/team/stats/referrers

Returns visits grouped by referrers. Row order is unspecified; sort in your client if needed. Reports use the token's team and aggregate daily analytics. total includes QR visits; qr_total is the QR subset. unique_total adds daily unique counts and is not a count of distinct people across the whole period. Counts in grouped rows can be numeric strings. Results may be cached for a short period. Filters apply together; values within an array match any listed value. Use UTC calendar dates for consistent day boundaries.

Request
curl -X GET 'https://api.t.ly/api/v1/team/stats/referrers?start_date=2026-11-01&end_date=2026-11-07T23%3A59%3A59Z&domains%5B0%5D=https%3A%2F%2Ft.ly%2F&link_type%5B0%5D=links&link_type%5B1%5D=one_links' \
  -H 'Authorization: Bearer YOUR_API_TOKEN'
const url = new URL(
    "https://api.t.ly/api/v1/team/stats/referrers"
);

const params = {
    "start_date": "2026-11-01",
    "end_date": "2026-11-07T23:59:59Z",
    "tag_ids": "",
    "pixel_ids": "",
    "domains[0]": "https://t.ly/",
    "user_ids": "",
    "link_type[0]": "links",
    "link_type[1]": "one_links",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer YOUR_API_TOKEN",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.t.ly/api/v1/team/stats/referrers';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer YOUR_API_TOKEN',
        ],
        'query' => [
            'start_date' => '2026-11-01',
            'end_date' => '2026-11-07T23:59:59Z',
            'tag_ids' => '',
            'pixel_ids' => '',
            'domains[0]' => 'https://t.ly/',
            'user_ids' => '',
            'link_type[0]' => 'links',
            'link_type[1]' => 'one_links',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.t.ly/api/v1/team/stats/referrers'
params = {
  'start_date': '2026-11-01',
  'end_date': '2026-11-07T23:59:59Z',
  'tag_ids': '',
  'pixel_ids': '',
  'domains[0]': 'https://t.ly/',
  'user_ids': '',
  'link_type[0]': 'links',
  'link_type[1]': 'one_links',
}
headers = {
  'Authorization': 'Bearer YOUR_API_TOKEN'
}

response = requests.request('GET', url, headers=headers, params=params)
response.json()
Send a real request with your own token.
Response
[
    {
        "referrer": "google.com",
        "total": "120",
        "unique_total": "95",
        "qr_total": "20"
    }
]

Query parameters

  • start_date string
    Start of the reporting range. Use a UTC calendar date. Defaults to the start of the day 30 days ago.

    Example: 2026-11-01

  • end_date string
    End of the reporting range. Use a UTC end-of-day timestamp to include a complete day. Defaults to the end of today.

    Example: 2026-11-07T23:59:59Z

  • tag_ids array of integers
    Match links with any of these tag IDs. Up to 10.
  • pixel_ids array of integers
    Match short links with any of these pixel IDs. Up to 10. This filter does not apply to OneLinks.
  • domains array of strings
    Domain URL strings, including scheme and trailing slash. Up to 10.

    Example: ["https://t.ly/"]

  • user_ids array of integers
    IDs of the team members who created the links. Up to 10. Find them with List team members.
  • link_type array of strings
    Include links, one_links, or both. Defaults to ["links"].

    Example: ["links","one_links"]

Get team platforms

GET https://api.t.ly/api/v1/team/stats/platforms

Returns visits grouped by platforms. Row order is unspecified; sort in your client if needed. Reports use the token's team and aggregate daily analytics. total includes QR visits; qr_total is the QR subset. unique_total adds daily unique counts and is not a count of distinct people across the whole period. Counts in grouped rows can be numeric strings. Results may be cached for a short period. Filters apply together; values within an array match any listed value. Use UTC calendar dates for consistent day boundaries.

Request
curl -X GET 'https://api.t.ly/api/v1/team/stats/platforms?start_date=2026-11-01&end_date=2026-11-07T23%3A59%3A59Z&domains%5B0%5D=https%3A%2F%2Ft.ly%2F&link_type%5B0%5D=links&link_type%5B1%5D=one_links' \
  -H 'Authorization: Bearer YOUR_API_TOKEN'
const url = new URL(
    "https://api.t.ly/api/v1/team/stats/platforms"
);

const params = {
    "start_date": "2026-11-01",
    "end_date": "2026-11-07T23:59:59Z",
    "tag_ids": "",
    "pixel_ids": "",
    "domains[0]": "https://t.ly/",
    "user_ids": "",
    "link_type[0]": "links",
    "link_type[1]": "one_links",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer YOUR_API_TOKEN",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.t.ly/api/v1/team/stats/platforms';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer YOUR_API_TOKEN',
        ],
        'query' => [
            'start_date' => '2026-11-01',
            'end_date' => '2026-11-07T23:59:59Z',
            'tag_ids' => '',
            'pixel_ids' => '',
            'domains[0]' => 'https://t.ly/',
            'user_ids' => '',
            'link_type[0]' => 'links',
            'link_type[1]' => 'one_links',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.t.ly/api/v1/team/stats/platforms'
params = {
  'start_date': '2026-11-01',
  'end_date': '2026-11-07T23:59:59Z',
  'tag_ids': '',
  'pixel_ids': '',
  'domains[0]': 'https://t.ly/',
  'user_ids': '',
  'link_type[0]': 'links',
  'link_type[1]': 'one_links',
}
headers = {
  'Authorization': 'Bearer YOUR_API_TOKEN'
}

response = requests.request('GET', url, headers=headers, params=params)
response.json()
Send a real request with your own token.
Response
[
    {
        "platform": "Mac OS",
        "total": "120",
        "unique_total": "95",
        "qr_total": "20"
    }
]

Query parameters

  • start_date string
    Start of the reporting range. Use a UTC calendar date. Defaults to the start of the day 30 days ago.

    Example: 2026-11-01

  • end_date string
    End of the reporting range. Use a UTC end-of-day timestamp to include a complete day. Defaults to the end of today.

    Example: 2026-11-07T23:59:59Z

  • tag_ids array of integers
    Match links with any of these tag IDs. Up to 10.
  • pixel_ids array of integers
    Match short links with any of these pixel IDs. Up to 10. This filter does not apply to OneLinks.
  • domains array of strings
    Domain URL strings, including scheme and trailing slash. Up to 10.

    Example: ["https://t.ly/"]

  • user_ids array of integers
    IDs of the team members who created the links. Up to 10. Find them with List team members.
  • link_type array of strings
    Include links, one_links, or both. Defaults to ["links"].

    Example: ["links","one_links"]

Get team browsers

GET https://api.t.ly/api/v1/team/stats/browsers

Returns visits grouped by browsers. Row order is unspecified; sort in your client if needed. Reports use the token's team and aggregate daily analytics. total includes QR visits; qr_total is the QR subset. unique_total adds daily unique counts and is not a count of distinct people across the whole period. Counts in grouped rows can be numeric strings. Results may be cached for a short period. Filters apply together; values within an array match any listed value. Use UTC calendar dates for consistent day boundaries.

Request
curl -X GET 'https://api.t.ly/api/v1/team/stats/browsers?start_date=2026-11-01&end_date=2026-11-07T23%3A59%3A59Z&domains%5B0%5D=https%3A%2F%2Ft.ly%2F&link_type%5B0%5D=links&link_type%5B1%5D=one_links' \
  -H 'Authorization: Bearer YOUR_API_TOKEN'
const url = new URL(
    "https://api.t.ly/api/v1/team/stats/browsers"
);

const params = {
    "start_date": "2026-11-01",
    "end_date": "2026-11-07T23:59:59Z",
    "tag_ids": "",
    "pixel_ids": "",
    "domains[0]": "https://t.ly/",
    "user_ids": "",
    "link_type[0]": "links",
    "link_type[1]": "one_links",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer YOUR_API_TOKEN",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.t.ly/api/v1/team/stats/browsers';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer YOUR_API_TOKEN',
        ],
        'query' => [
            'start_date' => '2026-11-01',
            'end_date' => '2026-11-07T23:59:59Z',
            'tag_ids' => '',
            'pixel_ids' => '',
            'domains[0]' => 'https://t.ly/',
            'user_ids' => '',
            'link_type[0]' => 'links',
            'link_type[1]' => 'one_links',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.t.ly/api/v1/team/stats/browsers'
params = {
  'start_date': '2026-11-01',
  'end_date': '2026-11-07T23:59:59Z',
  'tag_ids': '',
  'pixel_ids': '',
  'domains[0]': 'https://t.ly/',
  'user_ids': '',
  'link_type[0]': 'links',
  'link_type[1]': 'one_links',
}
headers = {
  'Authorization': 'Bearer YOUR_API_TOKEN'
}

response = requests.request('GET', url, headers=headers, params=params)
response.json()
Send a real request with your own token.
Response
[
    {
        "browser": "Chrome",
        "total": "120",
        "unique_total": "95",
        "qr_total": "20"
    }
]

Query parameters

  • start_date string
    Start of the reporting range. Use a UTC calendar date. Defaults to the start of the day 30 days ago.

    Example: 2026-11-01

  • end_date string
    End of the reporting range. Use a UTC end-of-day timestamp to include a complete day. Defaults to the end of today.

    Example: 2026-11-07T23:59:59Z

  • tag_ids array of integers
    Match links with any of these tag IDs. Up to 10.
  • pixel_ids array of integers
    Match short links with any of these pixel IDs. Up to 10. This filter does not apply to OneLinks.
  • domains array of strings
    Domain URL strings, including scheme and trailing slash. Up to 10.

    Example: ["https://t.ly/"]

  • user_ids array of integers
    IDs of the team members who created the links. Up to 10. Find them with List team members.
  • link_type array of strings
    Include links, one_links, or both. Defaults to ["links"].

    Example: ["links","one_links"]

Get team languages

GET https://api.t.ly/api/v1/team/stats/languages

Returns visits grouped by languages. Row order is unspecified; sort in your client if needed. Reports use the token's team and aggregate daily analytics. total includes QR visits; qr_total is the QR subset. unique_total adds daily unique counts and is not a count of distinct people across the whole period. Counts in grouped rows can be numeric strings. Results may be cached for a short period. Filters apply together; values within an array match any listed value. Use UTC calendar dates for consistent day boundaries.

Request
curl -X GET 'https://api.t.ly/api/v1/team/stats/languages?start_date=2026-11-01&end_date=2026-11-07T23%3A59%3A59Z&domains%5B0%5D=https%3A%2F%2Ft.ly%2F&link_type%5B0%5D=links&link_type%5B1%5D=one_links' \
  -H 'Authorization: Bearer YOUR_API_TOKEN'
const url = new URL(
    "https://api.t.ly/api/v1/team/stats/languages"
);

const params = {
    "start_date": "2026-11-01",
    "end_date": "2026-11-07T23:59:59Z",
    "tag_ids": "",
    "pixel_ids": "",
    "domains[0]": "https://t.ly/",
    "user_ids": "",
    "link_type[0]": "links",
    "link_type[1]": "one_links",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer YOUR_API_TOKEN",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.t.ly/api/v1/team/stats/languages';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer YOUR_API_TOKEN',
        ],
        'query' => [
            'start_date' => '2026-11-01',
            'end_date' => '2026-11-07T23:59:59Z',
            'tag_ids' => '',
            'pixel_ids' => '',
            'domains[0]' => 'https://t.ly/',
            'user_ids' => '',
            'link_type[0]' => 'links',
            'link_type[1]' => 'one_links',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.t.ly/api/v1/team/stats/languages'
params = {
  'start_date': '2026-11-01',
  'end_date': '2026-11-07T23:59:59Z',
  'tag_ids': '',
  'pixel_ids': '',
  'domains[0]': 'https://t.ly/',
  'user_ids': '',
  'link_type[0]': 'links',
  'link_type[1]': 'one_links',
}
headers = {
  'Authorization': 'Bearer YOUR_API_TOKEN'
}

response = requests.request('GET', url, headers=headers, params=params)
response.json()
Send a real request with your own token.
Response
[
    {
        "language": "en",
        "total": "120",
        "unique_total": "95",
        "qr_total": "20"
    }
]

Query parameters

  • start_date string
    Start of the reporting range. Use a UTC calendar date. Defaults to the start of the day 30 days ago.

    Example: 2026-11-01

  • end_date string
    End of the reporting range. Use a UTC end-of-day timestamp to include a complete day. Defaults to the end of today.

    Example: 2026-11-07T23:59:59Z

  • tag_ids array of integers
    Match links with any of these tag IDs. Up to 10.
  • pixel_ids array of integers
    Match short links with any of these pixel IDs. Up to 10. This filter does not apply to OneLinks.
  • domains array of strings
    Domain URL strings, including scheme and trailing slash. Up to 10.

    Example: ["https://t.ly/"]

  • user_ids array of integers
    IDs of the team members who created the links. Up to 10. Find them with List team members.
  • link_type array of strings
    Include links, one_links, or both. Defaults to ["links"].

    Example: ["links","one_links"]

List team visits

GET https://api.t.ly/api/v1/team/clicks

Returns individual visits and QR scans, newest first, for links still in the token's team. Use key as the row identifier. OneLink rows have a null destination; visitor details can be null or unknown. IP addresses are excluded. History is limited by your plan, and earliest_available gives its cutoff. An empty data array is a valid result. To read older visits, send next_cursor as cursor with the same filters and fixed dates; stop when next_cursor is null. This is a paginated history API, not a live event stream. An invalid cursor returns 422. A query timeout returns 503 with timed_out: true; retry with a shorter date range.

Request
curl -X GET 'https://api.t.ly/api/v1/team/clicks?start_date=2026-11-01T00%3A00%3A00Z&end_date=2026-11-07T12%3A00%3A00Z&limit=50&link_type%5B0%5D=links&link_type%5B1%5D=one_links' \
  -H 'Authorization: Bearer YOUR_API_TOKEN'
const url = new URL(
    "https://api.t.ly/api/v1/team/clicks"
);

const params = {
    "start_date": "2026-11-01T00:00:00Z",
    "end_date": "2026-11-07T12:00:00Z",
    "cursor": "",
    "limit": "50",
    "link_type[0]": "links",
    "link_type[1]": "one_links",
    "short_urls": "",
    "countries": "",
    "referrers": "",
    "visit_type": "",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer YOUR_API_TOKEN",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.t.ly/api/v1/team/clicks';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer YOUR_API_TOKEN',
        ],
        'query' => [
            'start_date' => '2026-11-01T00:00:00Z',
            'end_date' => '2026-11-07T12:00:00Z',
            'cursor' => '',
            'limit' => '50',
            'link_type[0]' => 'links',
            'link_type[1]' => 'one_links',
            'short_urls' => '',
            'countries' => '',
            'referrers' => '',
            'visit_type' => '',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.t.ly/api/v1/team/clicks'
params = {
  'start_date': '2026-11-01T00:00:00Z',
  'end_date': '2026-11-07T12:00:00Z',
  'cursor': '',
  'limit': '50',
  'link_type[0]': 'links',
  'link_type[1]': 'one_links',
  'short_urls': '',
  'countries': '',
  'referrers': '',
  'visit_type': '',
}
headers = {
  'Authorization': 'Bearer YOUR_API_TOKEN'
}

response = requests.request('GET', url, headers=headers, params=params)
response.json()
Send a real request with your own token.
Response
{
    "data": [
        {
            "key": "links:123",
            "link_type": "links",
            "created_at": "2026-11-07T10:00:00+00:00",
            "short_url": "https://t.ly/fall-sale",
            "destination": "https://example.com/sale",
            "country_code": "US",
            "city": "New York City",
            "region": "NY",
            "referrer": "google.com",
            "browser": "Chrome",
            "platform": "Mac OS",
            "language": "en",
            "qr_code": false,
            "user_agent": "Mozilla/5.0"
        }
    ],
    "next_cursor": null,
    "earliest_available": "2026-11-02T00:00:00+00:00"
}

Query parameters

  • start_date string
    Start timestamp, inclusive. Times are interpreted as UTC when no offset is supplied. Values older than your plan retention are clipped to earliest_available. With no dates, all retained visits are eligible. With only end_date, the start defaults to 24 hours ago.

    Example: 2026-11-01T00:00:00Z

  • end_date string
    End timestamp, inclusive. Defaults to now; future timestamps are clipped to now. Keep the same start and end values for every page.

    Example: 2026-11-07T12:00:00Z

  • cursor string
    Opaque next_cursor from the previous page. Omit on the first request. Do not construct or modify it.
  • limit integer
    Visits per page, from 1 to 200. Defaults to 50.

    Example: 50

  • link_type array of strings
    Include links, one_links, or both. Defaults to both.

    Example: ["links","one_links"]

  • short_urls array of strings
    Only these short URLs. Up to 20.
  • countries array of strings
    Country codes, up to 20. For example, US.
  • referrers array of strings
    Referrer values, up to 20. Use Direct for missing or unknown referrers.
  • visit_type array of strings
    Include visit, qr, or both. Omit for both. A visit is a regular link click; qr means a QR scan.

Tags

The tags you use to organize links.

Pixels

Retargeting pixels that fire when someone visits a link.

UTM presets

Saved sets of UTM parameters to add to long URLs.

Account and usage

Check your token's account, find team members and monitor plan allowances.

Get the current user

GET https://api.t.ly/api/v1/account/user

Returns the name and email of the user associated with the API token.

Request
curl -X GET https://api.t.ly/api/v1/account/user \
  -H 'Authorization: Bearer YOUR_API_TOKEN'
const url = new URL(
    "https://api.t.ly/api/v1/account/user"
);

const headers = {
    "Authorization": "Bearer YOUR_API_TOKEN",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.t.ly/api/v1/account/user';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer YOUR_API_TOKEN',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.t.ly/api/v1/account/user'
headers = {
  'Authorization': 'Bearer YOUR_API_TOKEN'
}

response = requests.request('GET', url, headers=headers)
response.json()
Send a real request with your own token.
Response
{
    "name": "Alex Morgan",
    "email": "[email protected]"
}

No parameters.

List team members

GET https://api.t.ly/api/v1/account/team-members

Returns members of the token's team, sorted by name. Use their id values in the user_ids filter on link lists and team analytics. The response contains names and email addresses; keep it within your integration.

Request
curl -X GET https://api.t.ly/api/v1/account/team-members \
  -H 'Authorization: Bearer YOUR_API_TOKEN'
const url = new URL(
    "https://api.t.ly/api/v1/account/team-members"
);

const headers = {
    "Authorization": "Bearer YOUR_API_TOKEN",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.t.ly/api/v1/account/team-members';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer YOUR_API_TOKEN',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.t.ly/api/v1/account/team-members'
headers = {
  'Authorization': 'Bearer YOUR_API_TOKEN'
}

response = requests.request('GET', url, headers=headers)
response.json()
Send a real request with your own token.
Response
{
    "users": [
        {
            "id": 123,
            "name": "Alex Morgan",
            "email": "[email protected]"
        }
    ]
}

No parameters.

Get team usage

GET https://api.t.ly/api/v1/account/usage

Returns usage and plan allowances for the token's team in one request. Each resource has an integer used count and limit, including team-specific allowances. monthly.links counts short links created in the app or API during the calendar month; monthly.analytics counts tracked analytics events. These reset at monthly.resets_at in UTC. Custom domains (including pending domains), OneLinks and team members are current resource counts and do not reset monthly. Team members include the owner and exclude pending invitations. A zero limit means no allowance is configured. These allowances are separate from HTTP request rate limits. Usage totals may be cached for a short period, so recent activity may take time to appear. The top-level link and click totals cover short links only; current_month_clicks includes this month's visits to older links.

Request
curl -X GET https://api.t.ly/api/v1/account/usage \
  -H 'Authorization: Bearer YOUR_API_TOKEN'
const url = new URL(
    "https://api.t.ly/api/v1/account/usage"
);

const headers = {
    "Authorization": "Bearer YOUR_API_TOKEN",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.t.ly/api/v1/account/usage';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer YOUR_API_TOKEN',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.t.ly/api/v1/account/usage'
headers = {
  'Authorization': 'Bearer YOUR_API_TOKEN'
}

response = requests.request('GET', url, headers=headers)
response.json()
Send a real request with your own token.
Response
{
    "monthly": {
        "starts_at": "2026-11-01T00:00:00+00:00",
        "resets_at": "2026-12-01T00:00:00+00:00",
        "links": {
            "used": 12,
            "limit": 1000
        },
        "analytics": {
            "used": 150,
            "limit": 10000
        }
    },
    "custom_domains": {
        "used": 2,
        "limit": 5
    },
    "one_links": {
        "used": 1,
        "limit": 5
    },
    "team_members": {
        "used": 3,
        "limit": 5
    },
    "current_month_links": 12,
    "current_month_clicks": 150,
    "total_links": 80,
    "total_clicks": 2400
}

No parameters.

Domains

Find your team's branded domain URLs for link creation and filters.

List domains

GET https://api.t.ly/api/v1/domain/list

Returns verified custom domains belonging to the token's team. Use the domain URL when creating links or filtering short-link lists and team analytics. Include its scheme and trailing slash. Set include_non_verified=true to include pending domains; these are not verified for use yet. Set include_dns_info=true as well to include DNS details for pending domains. include_platform_domains=true adds selectable T.LY-owned domains; these have is_platform_domain: true and no custom domain ID. An empty array means no matching domains.

Request
curl -X GET 'https://api.t.ly/api/v1/domain/list?include_non_verified=0&include_dns_info=0&include_platform_domains=0' \
  -H 'Authorization: Bearer YOUR_API_TOKEN'
const url = new URL(
    "https://api.t.ly/api/v1/domain/list"
);

const params = {
    "include_non_verified": "0",
    "include_dns_info": "0",
    "include_platform_domains": "0",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer YOUR_API_TOKEN",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.t.ly/api/v1/domain/list';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer YOUR_API_TOKEN',
        ],
        'query' => [
            'include_non_verified' => '0',
            'include_dns_info' => '0',
            'include_platform_domains' => '0',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.t.ly/api/v1/domain/list'
params = {
  'include_non_verified': '0',
  'include_dns_info': '0',
  'include_platform_domains': '0',
}
headers = {
  'Authorization': 'Bearer YOUR_API_TOKEN'
}

response = requests.request('GET', url, headers=headers, params=params)
response.json()
Send a real request with your own token.
Response
[
    {
        "id": 123,
        "team_id": 456,
        "domain": "https://go.example.com/",
        "fallback_url": "https://example.com/",
        "aname_validated": null,
        "cname_validated": "2026-11-01 12:00:00",
        "is_case_sensitive": false,
        "created_at": "2026-11-01T12:00:00.000000Z",
        "updated_at": "2026-11-01T12:00:00.000000Z"
    }
]

Query parameters

  • include_non_verified boolean
    Include unverified team domains. Defaults to false.

    Example: false

  • include_dns_info boolean
    Include DNS records and setup instructions for pending domains. Use with include_non_verified. Defaults to false.

    Example: false

  • include_platform_domains boolean
    Also include selectable T.LY-owned domains. Defaults to false.

    Example: false

Webhooks

For most integrations, configure webhooks in Settings and receive events in your app. These optional endpoints automate subscription and delivery management.

Set up and receive webhook events

Create a webhook

POST https://api.t.ly/api/v1/webhook

Creates an event subscription. Store the returned secret immediately; it is shown only on creation or rotation. Your receiver must accept JSON POST requests and return 2xx. Requires a Business plan with webhook access. All operations use the token's team. See the webhook setup guide for payloads, X-Tly-Key verification, retries and delivery limits. Deliveries can be skipped when the team delivery rate limit is reached; this is separate from API request throttling. The legacy event response field is the first entry in events.

Request
curl -X POST https://api.t.ly/api/v1/webhook \
  -H 'Authorization: Bearer YOUR_API_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Campaign visits",
    "url": "https://example.com/webhooks",
    "events": [
        "short_link.visited"
    ],
    "is_active": true,
    "tags": []
}'
const url = new URL(
    "https://api.t.ly/api/v1/webhook"
);

const headers = {
    "Authorization": "Bearer YOUR_API_TOKEN",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "name": "Campaign visits",
    "url": "https:\/\/example.com\/webhooks",
    "events": [
        "short_link.visited"
    ],
    "is_active": true,
    "tags": []
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.t.ly/api/v1/webhook';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer YOUR_API_TOKEN',
            'Content-Type' => 'application/json',
        ],
        'json' => [
            'name' => 'Campaign visits',
            'url' => 'https://example.com/webhooks',
            'events' => [
                'short_link.visited',
            ],
            'is_active' => true,
            'tags' => [],
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.t.ly/api/v1/webhook'
payload = {
    "name": "Campaign visits",
    "url": "https:\/\/example.com\/webhooks",
    "events": [
        "short_link.visited"
    ],
    "is_active": true,
    "tags": []
}
headers = {
  'Authorization': 'Bearer YOUR_API_TOKEN',
  'Content-Type': 'application/json'
}

response = requests.request('POST', url, headers=headers, json=payload)
response.json()
Send a real request with your own token.
Response
{
    "id": 123,
    "name": "Campaign visits",
    "url": "https://example.com/webhooks",
    "event": "short_link.visited",
    "events": [
        "short_link.visited"
    ],
    "tags": [],
    "is_active": true,
    "last_success_at": null,
    "last_failure_at": null,
    "created_at": "2026-11-07T10:00:00.000000Z",
    "updated_at": "2026-11-07T10:00:00.000000Z",
    "secret": "YOUR_WEBHOOK_SECRET"
}

Body parameters

  • name string Required
    Webhook name, up to 100 characters.

    Example: Campaign visits

  • url string Required
    Your receiver URL. Must use HTTPS and resolve to a public hostname; redirects and IP-literal URLs are not supported. Up to 2000 characters.

    Example: https://example.com/webhooks

  • events array of strings Required
    Events to subscribe to: short_link.visited, one_link.visited, one_link.link_visited. Send at least one and at most 10.

    Example: ["short_link.visited"]

  • is_active boolean
    Enable deliveries. Defaults to true on creation.

    Example: true

  • tags array of integers
    Only deliver events for links with any of these team tag IDs. Up to 50. An empty array matches all tags.

List webhooks

GET https://api.t.ly/api/v1/webhook

Returns all webhook subscriptions for your team. Secrets are omitted. An empty array means none are configured. Requires a Business plan with webhook access. All operations use the token's team. See the webhook setup guide for payloads, X-Tly-Key verification, retries and delivery limits. Deliveries can be skipped when the team delivery rate limit is reached; this is separate from API request throttling. The legacy event response field is the first entry in events.

Request
curl -X GET https://api.t.ly/api/v1/webhook \
  -H 'Authorization: Bearer YOUR_API_TOKEN'
const url = new URL(
    "https://api.t.ly/api/v1/webhook"
);

const headers = {
    "Authorization": "Bearer YOUR_API_TOKEN",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.t.ly/api/v1/webhook';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer YOUR_API_TOKEN',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.t.ly/api/v1/webhook'
headers = {
  'Authorization': 'Bearer YOUR_API_TOKEN'
}

response = requests.request('GET', url, headers=headers)
response.json()
Send a real request with your own token.
Response
[
    {
        "id": 123,
        "name": "Campaign visits",
        "url": "https://example.com/webhooks",
        "event": "short_link.visited",
        "events": [
            "short_link.visited"
        ],
        "tags": [],
        "is_active": true,
        "last_success_at": null,
        "last_failure_at": null,
        "created_at": "2026-11-07T10:00:00.000000Z",
        "updated_at": "2026-11-07T10:00:00.000000Z"
    }
]

No parameters.

Get a webhook

GET https://api.t.ly/api/v1/webhook/{id}

Returns one subscription. Its secret is omitted. Returns 404 when the webhook does not belong to the team. Requires a Business plan with webhook access. All operations use the token's team. See the webhook setup guide for payloads, X-Tly-Key verification, retries and delivery limits. Deliveries can be skipped when the team delivery rate limit is reached; this is separate from API request throttling. The legacy event response field is the first entry in events.

Request
curl -X GET https://api.t.ly/api/v1/webhook/123 \
  -H 'Authorization: Bearer YOUR_API_TOKEN'
const url = new URL(
    "https://api.t.ly/api/v1/webhook/123"
);

const headers = {
    "Authorization": "Bearer YOUR_API_TOKEN",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.t.ly/api/v1/webhook/123';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer YOUR_API_TOKEN',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.t.ly/api/v1/webhook/123'
headers = {
  'Authorization': 'Bearer YOUR_API_TOKEN'
}

response = requests.request('GET', url, headers=headers)
response.json()
Send a real request with your own token.
Response
{
    "id": 123,
    "name": "Campaign visits",
    "url": "https://example.com/webhooks",
    "event": "short_link.visited",
    "events": [
        "short_link.visited"
    ],
    "tags": [],
    "is_active": true,
    "last_success_at": null,
    "last_failure_at": null,
    "created_at": "2026-11-07T10:00:00.000000Z",
    "updated_at": "2026-11-07T10:00:00.000000Z"
}

URL parameters

  • id integer Required
    Webhook ID returned by Create or List webhooks.

    Example: 123

Update a webhook

PUT https://api.t.ly/api/v1/webhook/{id}

Changes only the supplied fields. Send tags: [] to remove tag filters. If events is supplied, it replaces the entire subscription list and must not be empty. The response includes secret only when rotate_secret is true. Requires a Business plan with webhook access. All operations use the token's team. See the webhook setup guide for payloads, X-Tly-Key verification, retries and delivery limits. Deliveries can be skipped when the team delivery rate limit is reached; this is separate from API request throttling. The legacy event response field is the first entry in events.

Request
curl -X PUT https://api.t.ly/api/v1/webhook/123 \
  -H 'Authorization: Bearer YOUR_API_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Campaign visits",
    "url": "https://example.com/webhooks",
    "events": [
        "short_link.visited"
    ],
    "is_active": true,
    "tags": [],
    "rotate_secret": false
}'
const url = new URL(
    "https://api.t.ly/api/v1/webhook/123"
);

const headers = {
    "Authorization": "Bearer YOUR_API_TOKEN",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "name": "Campaign visits",
    "url": "https:\/\/example.com\/webhooks",
    "events": [
        "short_link.visited"
    ],
    "is_active": true,
    "tags": [],
    "rotate_secret": false
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.t.ly/api/v1/webhook/123';
$response = $client->put(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer YOUR_API_TOKEN',
            'Content-Type' => 'application/json',
        ],
        'json' => [
            'name' => 'Campaign visits',
            'url' => 'https://example.com/webhooks',
            'events' => [
                'short_link.visited',
            ],
            'is_active' => true,
            'tags' => [],
            'rotate_secret' => false,
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.t.ly/api/v1/webhook/123'
payload = {
    "name": "Campaign visits",
    "url": "https:\/\/example.com\/webhooks",
    "events": [
        "short_link.visited"
    ],
    "is_active": true,
    "tags": [],
    "rotate_secret": false
}
headers = {
  'Authorization': 'Bearer YOUR_API_TOKEN',
  'Content-Type': 'application/json'
}

response = requests.request('PUT', url, headers=headers, json=payload)
response.json()
Send a real request with your own token.
Response
{
    "id": 123,
    "name": "Campaign visits",
    "url": "https://example.com/webhooks",
    "event": "short_link.visited",
    "events": [
        "short_link.visited"
    ],
    "tags": [],
    "is_active": true,
    "last_success_at": null,
    "last_failure_at": null,
    "created_at": "2026-11-07T10:00:00.000000Z",
    "updated_at": "2026-11-07T10:00:00.000000Z"
}

URL parameters

  • id integer Required
    Webhook ID returned by Create or List webhooks.

    Example: 123

Body parameters

  • name string
    Webhook name, up to 100 characters.

    Example: Campaign visits

  • url string
    Your receiver URL. Must use HTTPS and resolve to a public hostname; redirects and IP-literal URLs are not supported. Up to 2000 characters.

    Example: https://example.com/webhooks

  • events array of strings
    Events to subscribe to: short_link.visited, one_link.visited, one_link.link_visited. Send at least one and at most 10.

    Example: ["short_link.visited"]

  • is_active boolean
    Enable deliveries. Defaults to true on creation.

    Example: true

  • tags array of integers
    Only deliver events for links with any of these team tag IDs. Up to 50. An empty array matches all tags.
  • rotate_secret boolean
    Replace the secret. The new secret is returned only in this response; update your receiver before accepting subsequent deliveries.

    Example: false

Delete a webhook

DELETE https://api.t.ly/api/v1/webhook/{id}

Deletes this subscription. Returns 202 with an empty body. Requires a Business plan with webhook access. All operations use the token's team. See the webhook setup guide for payloads, X-Tly-Key verification, retries and delivery limits. Deliveries can be skipped when the team delivery rate limit is reached; this is separate from API request throttling. The legacy event response field is the first entry in events.

Request
curl -X DELETE https://api.t.ly/api/v1/webhook/123 \
  -H 'Authorization: Bearer YOUR_API_TOKEN'
const url = new URL(
    "https://api.t.ly/api/v1/webhook/123"
);

const headers = {
    "Authorization": "Bearer YOUR_API_TOKEN",
    "Accept": "application/json",
};


fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.t.ly/api/v1/webhook/123';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer YOUR_API_TOKEN',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.t.ly/api/v1/webhook/123'
headers = {
  'Authorization': 'Bearer YOUR_API_TOKEN'
}

response = requests.request('DELETE', url, headers=headers)
response.json()
Send a real request with your own token.
Response
No content

URL parameters

  • id integer Required
    Webhook ID returned by Create or List webhooks.

    Example: 123

Send a test webhook

POST https://api.t.ly/api/v1/webhook/{id}/test

Requests a test delivery for the first subscribed event and returns 202 with an empty body without waiting for delivery. Set sync=true to attempt delivery before returning a JSON delivery result with HTTP 200. A 200 response does not itself mean delivery succeeded: check status and response_status. Synchronous network or receiver failures can return an error instead of a delivery result; inspect delivery logs after a failed test. Requires a Business plan with webhook access. All operations use the token's team. See the webhook setup guide for payloads, X-Tly-Key verification, retries and delivery limits. Deliveries can be skipped when the team delivery rate limit is reached; this is separate from API request throttling. The legacy event response field is the first entry in events.

Request
curl -X POST https://api.t.ly/api/v1/webhook/123/test \
  -H 'Authorization: Bearer YOUR_API_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "sync": false
}'
const url = new URL(
    "https://api.t.ly/api/v1/webhook/123/test"
);

const headers = {
    "Authorization": "Bearer YOUR_API_TOKEN",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "sync": false
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.t.ly/api/v1/webhook/123/test';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer YOUR_API_TOKEN',
            'Content-Type' => 'application/json',
        ],
        'json' => [
            'sync' => false,
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.t.ly/api/v1/webhook/123/test'
payload = {
    "sync": false
}
headers = {
  'Authorization': 'Bearer YOUR_API_TOKEN',
  'Content-Type': 'application/json'
}

response = requests.request('POST', url, headers=headers, json=payload)
response.json()
Send a real request with your own token.
Response
No content

URL parameters

  • id integer Required
    Webhook ID returned by Create or List webhooks.

    Example: 123

Body parameters

  • sync boolean
    Wait for a delivery result before responding. Defaults to false.

    Example: false

List webhook deliveries

GET https://api.t.ly/api/v1/webhook/logs

Returns delivery attempts, newest first, with success, failed or skipped status. Retries can create separate rows with different attempt values. Logs are retained for seven days. Use page while current_page < last_page. The list omits payloads, response bodies and secrets. Requires a Business plan with webhook access. All operations use the token's team. See the webhook setup guide for payloads, X-Tly-Key verification, retries and delivery limits. Deliveries can be skipped when the team delivery rate limit is reached; this is separate from API request throttling. The legacy event response field is the first entry in events.

Request
curl -X GET 'https://api.t.ly/api/v1/webhook/logs?limit=25&page=1' \
  -H 'Authorization: Bearer YOUR_API_TOKEN'
const url = new URL(
    "https://api.t.ly/api/v1/webhook/logs"
);

const params = {
    "webhook_id": "",
    "limit": "25",
    "page": "1",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer YOUR_API_TOKEN",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.t.ly/api/v1/webhook/logs';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer YOUR_API_TOKEN',
        ],
        'query' => [
            'webhook_id' => '',
            'limit' => '25',
            'page' => '1',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.t.ly/api/v1/webhook/logs'
params = {
  'webhook_id': '',
  'limit': '25',
  'page': '1',
}
headers = {
  'Authorization': 'Bearer YOUR_API_TOKEN'
}

response = requests.request('GET', url, headers=headers, params=params)
response.json()
Send a real request with your own token.
Response
{
    "data": [
        {
            "id": 456,
            "webhook_id": 123,
            "webhook_name": "Campaign visits",
            "event": "short_link.visited",
            "status": "success",
            "response_status": 204,
            "error_message": null,
            "attempt": 1,
            "is_test": false,
            "created_at": "2026-11-07T10:00:00.000000Z"
        }
    ],
    "meta": {
        "current_page": 1,
        "last_page": 1,
        "per_page": 25,
        "total": 1
    }
}

Query parameters

  • webhook_id integer
    Only attempts for this webhook ID.
  • limit integer
    Rows per page, from 1 to 100. Defaults to 25.

    Example: 25

  • page integer
    Page number, starting at 1.

    Example: 1

Resend a webhook delivery

POST https://api.t.ly/api/v1/webhook/logs/{id}/resend

Resends the original event using the webhook's current settings. This sends a new delivery and can repeat a downstream action; use it deliberately. Returns 202 with an empty body. Returns 404 if the log or its webhook no longer exists, or 409 if the webhook is disabled. Requires a Business plan with webhook access. All operations use the token's team. See the webhook setup guide for payloads, X-Tly-Key verification, retries and delivery limits. Deliveries can be skipped when the team delivery rate limit is reached; this is separate from API request throttling. The legacy event response field is the first entry in events.

Request
curl -X POST https://api.t.ly/api/v1/webhook/logs/456/resend \
  -H 'Authorization: Bearer YOUR_API_TOKEN'
const url = new URL(
    "https://api.t.ly/api/v1/webhook/logs/456/resend"
);

const headers = {
    "Authorization": "Bearer YOUR_API_TOKEN",
    "Accept": "application/json",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://api.t.ly/api/v1/webhook/logs/456/resend';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer YOUR_API_TOKEN',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://api.t.ly/api/v1/webhook/logs/456/resend'
headers = {
  'Authorization': 'Bearer YOUR_API_TOKEN'
}

response = requests.request('POST', url, headers=headers)
response.json()
Send a real request with your own token.
Response
No content

URL parameters

  • id integer Required
    Delivery log ID from List webhook deliveries, not the webhook ID.

    Example: 456

Still stuck?

Email [email protected], or connect without code using Zapier, Google Sheets and more.

Contact support

Last updated: November 8, 2026