Your marketing team just dropped 50 product URLs into a shared spreadsheet. Subject line: "Flash sale this weekend -- need trackable links for all of these by Friday."
You open the spreadsheet. Fifty rows. Each one needs a short link with a descriptive title and social preview metadata, so the links look good when shared on LINE, Facebook, and X. You could open the toui.io dashboard, paste each URL one at a time, fill in the OG fields, copy the short link back... and repeat that 49 more times.
Or you could write a script that does it in 30 seconds.
This guide walks through exactly that. I built toui.io partly because I kept running into this exact scenario -- so the API is designed around it. By the end, you'll have a working promotion engine that creates short URLs, verifies them, and pulls campaign analytics. No browser required.
Before you start
You'll need three things:
A toui.io account on the Pro or Business plan. The free tier covers the dashboard, but API access is on the paid plans. Here's the pricing if you want to check what's included.
An API key. Log into the toui.io dashboard, go to API Keys, and create one. You'll see it once -- copy it and save it to a
.envfile in your project root:
# .env
TOUI_API_KEY=toui_your_key_here
Make sure .env is in your .gitignore -- never commit API keys to version control.
-
Node.js 18+ (for built-in
fetch) and dotenv for loading environment variables:
npm install dotenv
If you're on Node.js 20.6+, you can skip dotenv and use the built-in
--env-fileflag instead:node --env-file=.env create-links.js
Every code example in this guide reads the key from the environment. The first lines of every script should look like this:
import "dotenv/config";
const API_KEY = process.env.TOUI_API_KEY;
if (!API_KEY) {
throw new Error("Missing TOUI_API_KEY in .env file");
}
That check runs first. If someone clones your repo and forgets to create the .env file, they get a clear error instead of a cryptic 401.
Step 1: Create short URLs
The endpoint is POST https://toui.io/api/v1/shorten. You send a JSON body with the target URL and optional metadata; you get back a short link.
Here's the simplest possible call:
const response = await fetch("https://toui.io/api/v1/shorten", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.TOUI_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
url: "https://shop.example.com/products/wireless-earbuds-pro",
title: "Wireless Earbuds Pro - Flash Sale",
}),
});
if (!response.ok) {
const err = await response.json();
throw new Error(`API error ${response.status}: ${err.error}`);
}
const data = await response.json();
console.log(data);
// {
// short_url: "https://toui.io/xK3mQp",
// short_code: "xK3mQp",
// target_url: "https://shop.example.com/products/wireless-earbuds-pro",
// created_at: "2026-04-10T08:30:00.000Z"
// }
That's the core pattern: POST the URL, check response.ok, parse the result. Every example below follows this same shape.
Adding social preview metadata
When someone shares your short link on LINE or Facebook, the platform fetches Open Graph tags to render a preview card. toui.io lets you set these at creation time:
const result = await createLink({
url: "https://shop.example.com/products/wireless-earbuds-pro",
title: "Wireless Earbuds Pro - Flash Sale",
og_title: "60% Off Wireless Earbuds Pro -- This Weekend Only",
og_description: "Our top-rated earbuds at the lowest price ever. 48 hours only.",
});
The og_title and og_description fields are optional. If you skip them, social platforms will fall back to whatever metadata the destination page has. But for a flash sale where every click matters, controlling the preview card is worth the extra two lines.
You can also pass og_image_url if you have a hosted image URL for the card. It accepts any public https:// image link.
Choosing a custom short code
By default, toui.io generates a random 6-character code. But if you want something readable -- say, for a link you'll mention on a podcast or print on packaging -- you can request a custom code:
{
url: "https://shop.example.com/flash-sale",
title: "Weekend Flash Sale",
custom_code: "sale426"
}
// Result: https://toui.io/sale426
Custom codes must be 4-8 alphanumeric characters. If the code is already taken, the API returns a 400 error. This is a paid-plan feature -- free accounts can only use auto-generated codes.
Processing 50 products in a loop
Now let's put it together. Here's a complete script that reads a product list and creates tracked short links for each one:
import "dotenv/config";
const API_KEY = process.env.TOUI_API_KEY;
if (!API_KEY) {
throw new Error("Missing TOUI_API_KEY in .env file");
}
const API_BASE = "https://toui.io/api/v1";
// Your marketing team's product list
const products = [
{
name: "Wireless Earbuds Pro",
url: "https://shop.example.com/products/wireless-earbuds-pro",
discount: "60%",
},
{
name: "USB-C Hub 7-in-1",
url: "https://shop.example.com/products/usb-c-hub-7in1",
discount: "45%",
},
// ... 48 more products
];
async function createLink(body) {
const response = await fetch(`${API_BASE}/shorten`, {
method: "POST",
headers: {
"Authorization": `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
if (!response.ok) {
const err = await response.json();
throw new Error(`Failed to shorten ${body.url}: ${err.error}`);
}
return response.json();
}
async function main() {
const results = [];
for (const product of products) {
const result = await createLink({
url: product.url,
title: `${product.name} - Flash Sale`,
og_title: `${product.discount} Off ${product.name} -- This Weekend Only`,
og_description: `Flash sale pricing on ${product.name}. Limited time offer.`,
});
results.push({
product: product.name,
short_url: result.short_url,
code: result.short_code,
});
console.log(`Created: ${result.short_url} -> ${product.name}`);
}
console.log(`\nDone. Created ${results.length} short links.`);
return results;
}
main().catch(console.error);
Run it, and 50 short links appear in your terminal. Each one is tracked, has social preview metadata, and is ready to drop into email campaigns, ad creatives, or social posts.
Step 2: Verify your links
Before handing the links to your marketing team, a quick sanity check never hurts. The GET /api/v1/urls/{code} endpoint returns everything toui.io knows about a short link:
async function getLink(code) {
const response = await fetch(`${API_BASE}/urls/${code}`, {
headers: {
"Authorization": `Bearer ${API_KEY}`,
},
});
if (!response.ok) {
const err = await response.json();
throw new Error(`Lookup failed for ${code}: ${err.error}`);
}
return response.json();
}
const link = await getLink("xK3mQp");
console.log(link);
// {
// short_code: "xK3mQp",
// target_url: "https://shop.example.com/products/wireless-earbuds-pro",
// title: "Wireless Earbuds Pro - Flash Sale",
// click_count: 0,
// is_active: 1,
// og_title: "60% Off Wireless Earbuds Pro -- This Weekend Only",
// og_description: "Our top-rated earbuds at the lowest price ever. 48 hours only.",
// og_image_url: null,
// created_at: "2026-04-10T08:30:00.000Z"
// }
This is useful for spot-checking: confirm the target_url is correct, verify the OG fields are set, and make sure is_active is 1. You could also run this across all 50 links as a validation step before sending the spreadsheet back to marketing.
Step 3: Check campaign results
The sale is over. Time to see what worked. The GET /api/v1/urls/{code}/stats endpoint gives you click data with daily breakdowns and audience insights:
async function getStats(code, days = 7) {
const response = await fetch(`${API_BASE}/urls/${code}/stats?days=${days}`, {
headers: {
"Authorization": `Bearer ${API_KEY}`,
},
});
if (!response.ok) {
const err = await response.json();
throw new Error(`Stats failed for ${code}: ${err.error}`);
}
return response.json();
}
const stats = await getStats("xK3mQp", 7);
console.log(stats);
// {
// short_code: "xK3mQp",
// target_url: "https://shop.example.com/products/wireless-earbuds-pro",
// total_clicks: 1284,
// daily: [
// { date: "2026-04-10", clicks: 412, unique_visitors: 389 },
// { date: "2026-04-09", clicks: 507, unique_visitors: 461 },
// { date: "2026-04-08", clicks: 365, unique_visitors: 330 },
// ...
// ],
// countries: [
// { country: "TW", clicks: 623 },
// { country: "JP", clicks: 287 },
// { country: "US", clicks: 194 },
// ...
// ],
// referers: [
// { referer: "facebook.com", clicks: 512 },
// { referer: "line.me", clicks: 401 },
// ...
// ],
// devices: [
// { device_type: "mobile", browser: "Chrome Mobile", clicks: 743 },
// { device_type: "desktop", browser: "Chrome", clicks: 412 },
// ...
// ],
// limited: false
// }
A note on the limited field: the advanced breakdowns (countries, referers, devices) require a Pro or Business plan. On the free plan, you'd get a truncated preview with limited: true. Since you're using the API, you're already on a paid plan -- so you get the full picture.
Building a console report
Here's a quick script that pulls stats for every link from your campaign and prints a summary:
async function campaignReport(codes) {
console.log("Product Link Report");
console.log("=".repeat(60));
let totalClicks = 0;
for (const code of codes) {
const stats = await getStats(code, 7);
totalClicks += stats.total_clicks;
const topCountry = stats.countries[0];
const topReferer = stats.referers[0];
console.log(`\n${stats.short_code} -> ${stats.target_url}`);
console.log(` Total clicks: ${stats.total_clicks}`);
if (topCountry) {
console.log(` Top country: ${topCountry.country} (${topCountry.clicks})`);
}
if (topReferer) {
console.log(` Top referrer: ${topReferer.referer} (${topReferer.clicks})`);
}
}
console.log(`\n${"=".repeat(60)}`);
console.log(`Campaign total: ${totalClicks} clicks across ${codes.length} links`);
}
// Pass in the short codes from Step 1
const codes = ["xK3mQp", "bR7nWz", /* ... */];
campaignReport(codes).catch(console.error);
That's your entire post-campaign analytics pipeline. No dashboards to click through, no CSV exports to wrangle. Just raw data you can pipe into whatever reporting tool your team already uses.
Production tips
Rate limits
toui.io enforces per-minute burst limits based on your plan:
| Plan | Burst limit | Monthly API quota |
|---|---|---|
| Pro | 200 req/min | 500,000 req/month |
| Business | 600 req/min | 500,000 req/month |
For 50 products, you won't come close to either limit. But if you're building something that creates thousands of links -- say, per-order tracking URLs for a high-volume store -- you'll want to pace your requests.
Handling 429 (rate limited)
If you hit the limit, the API returns a 429 status. A simple backoff strategy handles this gracefully:
async function fetchWithRetry(url, options, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const response = await fetch(url, options);
if (response.status === 429 && attempt < maxRetries) {
const body = await response.json();
const delay = (body.retry_after || Math.pow(2, attempt)) * 1000;
console.log(`Rate limited. Retrying in ${delay / 1000}s...`);
await new Promise((resolve) => setTimeout(resolve, delay));
continue;
}
return response;
}
}
The 429 response body includes a retry_after field (in seconds) telling you exactly how long to wait. Every successful response also includes X-RateLimit-Remaining in the headers, so you can pace proactively if you need to.
Error codes at a glance
| Status | Meaning | Common cause |
|---|---|---|
| 400 | Bad request | Missing url field, invalid custom code, or malformed JSON |
| 401 | Unauthorized | Missing or invalid API key |
| 403 | Forbidden | Free plan trying to use API, custom code on free plan, or URL flagged by Safe Browsing |
| 404 | Not found | Short code doesn't exist or belongs to a different team |
| 429 | Rate limited | Too many requests per minute or monthly quota exceeded |
Always check response.ok before parsing. The error response body consistently has an error field with a human-readable message -- log it, and debugging becomes straightforward.
What you just built
In under 100 lines of JavaScript, you built a promotion engine that:
- Creates 50 tracked short links with social preview cards
- Verifies each link is correctly configured
- Pulls post-campaign analytics with country, referrer, and device breakdowns
No dependencies beyond Node.js. No dashboard clicking. Just node create-links.js and the links are ready to ship.
This same pattern works for anything that needs tracked links at scale -- order confirmations, event invitations, newsletter campaigns, affiliate programs.
That's the whole API -- three endpoints, no SDK to install, just fetch. I built toui.io as a side project because I only needed about 5% of what tools like Bitly offer -- and paying $35/month for that didn't make sense.
There's a free tier if you want to kick the tires. The API itself is on the paid plans -- but honestly, if you're only creating a few links a week, the dashboard does the job fine. The API starts making sense when your marketing team sends you that spreadsheet.
Top comments (0)