tinyurl.et

API reference

Create and measure tinyurl.et links from your own systems: receipts, articles, campaigns, app installs.

Introduction

The API is JSON over HTTPS. Every endpoint lives under https://tinyurl.et/api/v1. Send Content-Type: application/json on requests with a body. Times are ISO 8601 in UTC.

API access is included with every paid plan. You need an active plan and an API key, created in Dashboard → API keys.

Authentication

Send your key as a bearer token:

Authorization: Bearer tu_live_3kq0…

Limits and quotas

Two limits apply, both set by your plan:

See pricing for each plan's numbers. Business API customers can ask for higher limits.

Errors

Errors use standard HTTP status codes and a stable machine-readable code:

{ "error": { "code": "alias_taken", "message": "tinyurl.et/selam-shop is already taken." } }
FieldTypeDescription
unauthorized401Missing, invalid or revoked API key.
plan_inactive402The account has no active plan. Renew in Billing.
feature_unavailable403The plan doesn't include this (e.g. device routing on Starter).
not_found404No link with that code on your account.
domain_not_found / domain_not_ready404 / 409The domain isn't on your account, or its DNS isn't finished yet.
immutable405Tried to edit a link. Links can't be changed.
alias_taken / alias_reserved409The alias exists or is reserved by tinyurl.et.
validation_error / invalid_url / invalid_alias400 / 422The body is malformed. message says which field.
blocked_domain422The destination domain is blocked for abuse.
missing_params422A template placeholder wasn't supplied.
rate_limited / quota_exceeded429Daily request limit or monthly link quota reached.

POST/api/v1/links

FieldTypeDescription
urlstring, requiredDestination for everyone without a device-specific route. Must be http(s).
domainstringOne of your live custom domains, e.g. dashen.et. Omit to use your default domain; send "tinyurl.et" to force the shared one.
aliasstringCustom code, 2–63 letters, numbers, - or _. Case-sensitive. Omit for a random 6-character code.
titlestringLabel for your own reference, up to 255 characters.
alias_from_titlebooleanTurn title into the alias (see below).
ios_urlstringWhere iPhones and iPads go. App Store link or a deep link like myapp://item/42. Pro and up.
android_urlstringWhere Android phones go. Play Store link or a deep link. Pro and up.
expires_atISO datetimeOptional. After this time the link shows an expired page.
metadataobjectAny JSON you want back on reads, e.g. your own IDs. Not shown to visitors.
passwordstringVisitors must enter it before being sent on. At least 4 characters; stored hashed.
max_clicksnumberThe link stops working after this many visits.
variantsarrayA/B rotation: 2–5 objects { url, weight, label }. See below.
redirect_typestringdirect (default), splash or cta. See landing pages below.
landingobjectContent for splash/cta pages: { title, message, button_text, button_url, delay }.
utmobject{ source, medium, campaign, term, content } added to every destination as utm_* parameters.
pixel_idsarrayIDs of retargeting pixels (Dashboard → Pixels) to fire before the redirect.
campaignstringCampaign slug or id to file the link under.
curl -X POST https://tinyurl.et/api/v1/links \
  -H "Authorization: Bearer $TINYURL_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://selam.example.com/shop",
    "alias": "selam-shop",
    "ios_url": "https://apps.apple.com/app/id000000000",
    "android_url": "https://play.google.com/store/apps/details?id=com.example.selam"
  }'

Response 201 Created:

{
  "data": {
    "code": "selam-shop",
    "short_url": "https://tinyurl.et/selam-shop",
    "display_url": "tinyurl.et/selam-shop",
    "url": "https://selam.example.com/shop",
    "ios_url": "https://apps.apple.com/app/id000000000",
    "android_url": "https://play.google.com/store/apps/details?id=com.example.selam",
    "title": null,
    "kind": "api",
    "metadata": null,
    "clicks": 0,
    "disabled": false,
    "expires_at": null,
    "created_at": "2026-10-26T09:14:03.000Z"
  }
}

Alias from a title

For blogs and news sites: pass the post title and alias_from_title: true. The title is lower-cased and dashed; if the result is taken, a number is added.

request body
{
  "url": "https://hana.example.com/posts/1842",
  "title": "Coffee prices in Addis, 2026",
  "alias_from_title": true
}
// → tinyurl.et/coffee-prices-in-addis-2026
// → tinyurl.et/coffee-prices-in-addis-2026-2   (if the first was taken)

Bulk create

POST/api/v1/links/bulk

Up to 100 links per call, each with the same fields as a single create. Items succeed or fail independently; the response is 207 Multi-Status with one result per input, in order.

// request
{ "links": [ { "url": "https://a.example.com" }, { "url": "https://b.example.com", "alias": "selam-shop" } ] }

// response
{
  "data": [
    { "index": 0, "ok": true,  "data": { "code": "Hq3mZt", "short_url": "…" } },
    { "index": 1, "ok": false, "error": { "code": "alias_taken", "message": "…" } }
  ],
  "meta": { "created": 1, "failed": 1 }
}

GET/api/v1/links?page=1&limit=50

Newest first. limit is at most 100. The response has data and meta: { page, limit, total, pages }.

GET/api/v1/links/{code}

Returns the same object as create, including the current clicks total. For a link on a custom domain, add ?domain=dashen.et: the same code can exist once per domain. This also applies to /stats and DELETE.

Click stats

GET/api/v1/links/{code}/stats

Clicks from link-preview bots (Telegram, WhatsApp, Facebook…) are counted separately in bot_clicks and left out of everything else.

{
  "data": {
    "code": "selam-shop",
    "total_clicks": 1284,
    "bot_clicks": 37,
    "last_clicked_at": "2026-10-26T08:51:12.000Z",
    "daily": [ { "day": "2026-09-27", "clicks": 31 }, … ],   // last 30 days
    "devices":   [ { "label": "MOBILE", "count": 1022 }, { "label": "DESKTOP", "count": 262 } ],
    "os":        [ { "label": "Android", "count": 811 }, … ],
    "browsers":  [ … ],
    "countries": [ { "label": "ET", "count": 1170 }, … ],
    "referrers": [ { "label": "t.me", "count": 402 }, … ],
    "routes":    [ { "label": "android", "count": 640 }, { "label": "ios", "count": 211 }, { "label": "default", "count": 433 } ]
  }
}

QR code

GET/api/v1/links/{code}/qr

Returns the link's QR code as SVG, in the style saved in the dashboard: the link's own style, otherwise your account default (colours, shape and logo). Query parameters override it for one request.

FieldTypeDescription
sizenumberWidth and height in pixels, 64–2048. Default 512. SVG scales cleanly, so this mostly sets the default display size.
fg / bghex colourCode and background colours, e.g. fg=%230d7a4a. Keep strong contrast so phones can scan it.
stylestringsquare, rounded or dots.
logo0logo=0 leaves your logo out.
domainstringFor links on a custom domain, as with the other link endpoints.
curl "https://tinyurl.et/api/v1/links/rcpt-ft26300abc12/qr?size=600" \
  -H "Authorization: Bearer $TINYURL_KEY" -o receipt-qr.svg

With a logo, codes use the highest error-correction level, so up to about 30% of the code can be covered and it still scans.

Switch off a link

DELETE/api/v1/links/{code}

Switches the link off permanently. Visitors see a “link switched off” page. The code stays reserved, so it can never be reused for a different destination. PATCH and PUT return 405 immutable.

Templates

A template is a destination with placeholders, registered once. Then each link needs only the values that change. This is how a bank creates a link for every receipt without building URLs in its core system.

POST/api/v1/templates

FieldTypeDescription
slugstring, requiredKey you'll pass as template. Lowercase letters, numbers, dashes.
namestring, requiredHuman-readable name.
destination_patternstring, requiredURL with {placeholders}. Values are URL-encoded when filled.
alias_patternstringOptional alias with placeholders, e.g. rcpt-{ref}. Lower-cased. Omit for random codes.
request body
{
  "slug": "receipt",
  "name": "Transfer receipt",
  "destination_pattern": "https://receipts.example-bank.et/view?ref={ref}",
  "alias_pattern": "rcpt-{ref}"
}

List them with GET /api/v1/templates; each comes back with its params. You can also manage templates in Dashboard → Templates.

Create from a template

POST/api/v1/links/from-template

FieldTypeDescription
templatestring, requiredThe template slug.
paramsobject, requiredValues for every placeholder, e.g. { "ref": "FT26300ABC12" }.
aliasstringOverrides the template's alias pattern.
title, ios_url, android_url, expires_at, metadataSame as a normal create. template and params are added to metadata automatically.
curl -X POST https://tinyurl.et/api/v1/links/from-template \
  -H "Authorization: Bearer $TINYURL_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "template": "receipt", "params": { "ref": "FT26300ABC12" }, "metadata": { "branch": "Bole" } }'

All of these are set when the link is created and, like everything else about a link, can't be changed later.

request body
{
  "url": "https://shop.example.et/sale",
  "alias": "sale",
  "variants": [
    { "url": "https://shop.example.et/sale?layout=a", "weight": 70, "label": "Grid" },
    { "url": "https://shop.example.et/sale?layout=b", "weight": 30, "label": "List" }
  ],
  "max_clicks": 5000
}

Splash and call-to-action pages

With redirect_type: "splash", visitors see your heading and message, then continue after delay seconds (0 = they click). With "cta", your button comes first (for example “Join our Telegram”) and they can still continue to the destination.

request body
{
  "url": "https://example.et/article",
  "redirect_type": "cta",
  "landing": {
    "title": "Before you read",
    "message": "Get the next one in your inbox.",
    "button_text": "Join our Telegram",
    "button_url": "https://t.me/example"
  }
}

UTM tags, pixels and campaigns

Custom domains

On the Business API plan, links can use your own domain: dashen.et/rcpt-ft26300abc12 instead of tinyurl.et/…. You add the domain once in Dashboard → Domains; we check its DNS, route it, and issue its HTTPS certificate automatically.

DNS setup

Each domain needs two records. The exact values, including your verification token, are on the domain's page in the dashboard.

FieldTypeDescription
TXT _tinyurl.<your domain>ownershiptinyurl-verify=<token>. Proves the domain is yours.
CNAME go.dashen.etsubdomainPoints a subdomain to cname.tinyurl.et. Recommended: your main website is untouched.
A dashen.etbare domainPoints a bare domain to our server IP addresses (shown in the dashboard). Remove other A/AAAA records on it.

We re-check pending domains every few minutes, and the domain switches on by itself once both records are found. HTTPS certificates are issued automatically right after, usually within minutes.

List domains

GET/api/v1/domains

{
  "data": [
    { "hostname": "dashen.et", "status": "active", "ready": true, "default": true, "https": true, "links": 1840 }
  ]
}

Webhooks

Add an endpoint in Dashboard → Webhooks and pick events. We send a POST with a JSON body within seconds of the event, with a 5-second timeout. After 20 failures in a row the webhook is paused.

FieldTypeDescription
link.createdeventA link was created (dashboard, API, import).
link.clickedeventA person opened a link. Bots and link previews are not sent.
X-Tinyurl-EventheaderThe event name.
X-Tinyurl-Signatureheadersha256=<HMAC-SHA256 of the raw body, keyed with your signing secret>.
link.clicked payload
{
  "event": "link.clicked",
  "created_at": "2026-10-27T09:14:03.000Z",
  "data": {
    "link_id": "cm…",
    "code": "rcpt-ft26300abc12",
    "device": "mobile",
    "os": "Android",
    "browser": "Chrome",
    "country": "ET",
    "referrer": "t.me",
    "routed_to": "default",
    "clicked_at": "2026-10-27T09:14:03.000Z"
  }
}

Verify the signature before trusting a delivery:

import crypto from "crypto";

function verify(rawBody, header, secret) {
  const expected = "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  return header && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(header));
}

Device routing

When a link has ios_url or android_url, we read the visitor's User-Agent on every click:

Redirects are 302, so browsers don't cache past the click counter. Deep links (myapp://…) work in the device fields; keep url as a normal web page for people without the app. Each click records which route it took; see routes in stats.

Why links can't be edited

A short link printed on a receipt or sent in an SMS has to keep meaning the same thing. If links could be edited, a leaked API key could quietly point every receipt at a phishing page. So there is no update endpoint. Wrong link? Switch it off and create a new one.

Free public links

The homepage uses POST /api/public/shorten with { "url": "…" } and no key. Each visitor gets one active link at a time, valid for 30 days, with a per-network cap. It's meant for people, not integrations: use an API key for anything automated.

URL shortener API documentation · tinyurl.et