URL Shortener API: Track Links with Code and Webhooks
Creating tracked links from code, receiving click and conversion events within seconds, and feeding link data into your own warehouse.
The dashboard works fine until the day it doesn't. Say you're sending a re-engagement email to 40,000 users, and each one needs its own tracked link so you can tie a click back to a person, not a campaign. Nobody is pasting 40,000 destinations into a form. Or you're spinning up affiliate links per partner, referral links per customer, or one short link per SMS in a drip sequence. The pattern is the same every time. Links get created by your code, at the moment your system needs them, in volumes no human touches.
That's an API job. The link has to exist, be tagged, and be trackable from the first click, because there's no analyst around to fix its metadata later. Machine-made links that aren't tracked from birth are just redirects, and redirects don't answer the one question that matters, which of these made money.
This page covers the full loop. Creating links from code with the right conventions baked in. Getting click and conversion events back out, either by polling or by webhook. Reporting conversions server-to-server with the click ID attached. And the three production details that separate a pipeline you trust from one you quietly stop believing.
If you're still deciding how link tracking should work at all, start with the complete link tracking guide and come back. This one assumes you know why you're tracking and want to do it at scale.
Creating links from code
A link-creation call has four parts that matter: the destination URL, an optional custom alias, tags, and a campaign. Everything else is decoration. The destination is the only required field, and it's tempting to start there and stop there. That's the mistake. A link with no tags and no campaign is a click counter you'll never be able to group, and grouping is the whole point.
A creation request looks like this:
POST /api/v1/links
{
"url": "https://example.com/pricing?utm_source=newsletter&utm_medium=email&utm_campaign=aug-launch",
"customAlias": "aug-pricing",
"tags": ["newsletter", "q3"],
"campaignId": "3b2f6c1e-8d4a-4f0b-9c7e-1a5d2e8f4b60"
}
Only url is required. Note that a campaign is referenced by its identifier rather than by name, so your code needs the campaign's ID on hand. Create the campaign once, keep the ID, and reuse it across every link that belongs to it.
The alias is optional too. Skip it and you get a random slug, which is fine for links nobody types or reads aloud. Set it when the link appears somewhere a human sees it. One warning, though. Aliases are a namespace, and machine-generated aliases collide. If your code builds them from a pattern, put a uniqueness check or a suffix scheme in from day one, because the first collision will happen in production, not in testing.
Tags and campaign are what make ten thousand links queryable instead of ten thousand rows of noise. Filter by tag, roll up by campaign, and a quarter's worth of links collapses into a report you can actually read.
Then there are UTMs, which live inside the destination URL, not in the link record. They're for the analytics tool on the other end. And this is where machine-made links change the math. A human building twenty links by hand introduces maybe three naming variants: Newsletter, newsletter, email-newsletter. Annoying, fixable in an afternoon. A script with a sloppy template introduces that same inconsistency ten thousand times before anyone looks.
Your code will follow whatever convention you give it with perfect discipline, so the convention itself is the entire ballgame. Lock it down before the first automated link ships. Lowercase, fixed vocabulary per field, one spelling per source. We wrote up a UTM naming scheme that holds if you don't have one yet.
The rule of thumb is that parameters a human would eyeball, a machine must validate. Reject a bad utm_source at creation time and it costs you one failed API call. Let it through and it costs you a quarter of unmergeable data.
Getting click data out: pull vs push
There are two ways to get click data from a tracking platform into your system. Either you ask for it, or it gets sent to you. Pull and push. Often one of them is enough, and picking the wrong one costs you either freshness or infrastructure you didn't need to build.
Pull means querying analytics endpoints on your own schedule. Your job runs at 2 a.m., requests yesterday's clicks grouped by country or device, and writes the rows to your warehouse. The REST API's analytics endpoints return clicks grouped by country, device, browser, or referrer, bucketed by hour, day, week, or month. Pull is simple to reason about because you control the clock. If the job fails, you re-run it and get the same answer. Backfills are trivial for the same reason. Ask for a wider date range.
Push means webhooks. You register an endpoint, and each click and conversion event arrives as an HTTP POST, recorded asynchronously off a queue and normally delivered within a few seconds under normal load. No polling loop, no "how stale is this number" question. The cost is that you're now running a receiver, an endpoint that has to be up, accept the payload fast, and queue it for processing.
Which one fits comes down to how expensive a five-minute delay is.
| You're building | Use | Why |
|---|---|---|
| A weekly campaign report | Pull | Data that's 12 hours old is fine. One cron job, zero infrastructure. |
| A warehouse sync | Pull | Batch loads want batches. Pulling a day at a time beats reassembling a day from 40,000 individual events. |
| A fraud check on signups | Push | A click that arrived 3 seconds ago matters. A click from last night's batch doesn't. |
| A Slack alert when a big client clicks a proposal link | Push | The whole point is immediacy. Polling every minute to fake it costs 1,440 API calls a day for the same result. |
Dashboards pull. Pipelines push.
That's the short version. Anything a human reads on a delay should come from scheduled queries, and anything a machine reacts to should come from events.
One warning on mixing them. Teams that push events into a warehouse and also pull daily aggregates will eventually compare the two numbers and find a gap, from retries, late-arriving events, or each path counting slightly different things. That's expected, not a bug. Pick one source as canonical per report and don't reconcile them in front of an executive.
Conversions server-to-server
A click tells you the link worked. It doesn't tell you the campaign did. For that you need the conversion, and at machine scale the only reliable way to report one is from your backend, over the API, with the click ID attached.
The mechanics are short. When someone lands via a tracked link, Acturity assigns the visit a click ID and passes it to your destination page as a first-party parameter. Your job is to keep it. Store it on the session, write it into the order record at checkout. When the sale completes, your server sends one API call back to Acturity with the click ID and the conversion details, order value included. No pixel, no cookie, no browser involved after the landing.
That last part is why this approach holds up while pixel-based reporting keeps eroding. A browser pixel depends on the visitor's device cooperating: no tracking protection, no ad blocker, a purchase completed in the same browser that clicked. Server-to-server reporting depends on your database, which cooperates every time. We cover what tracking protection blocks, and how server-side reporting gets around it, in our guide to cookieless conversion tracking.
Two rules keep the data trustworthy. First, send the conversion when the money moves, not when the button is clicked. A "purchase" event fired before payment settles inflates your conversion rate with failed cards and abandoned confirmations. Second, always include the order value. Take two campaigns, for example: one with 40 conversions at a $12 average and one with 15 at a $90 average. They rank in opposite orders depending on whether you counted or summed. Count-only reporting picks the wrong winner, and nobody notices until the budget's already spent.
Building it into your stack honestly
Three failure modes are worth designing for before you go live.
Idempotency first. Your job that creates links will run twice. Not might, will. A deploy restarts the worker mid-batch, a queue redelivers a message, and now every customer in that email send has two tracked links splitting their clicks. The fix is cheap. Give each link a deterministic custom alias built from something stable, like ord-4412-receipt, and save the link's id against your own record as soon as the first call succeeds. Aliases are unique, so a second attempt with the same alias is refused with an error saying the alias is already in use, instead of creating a duplicate. Your code can treat that refusal as "already done" and use the id it saved. Make the alias specific enough that nobody else is likely to pick it, and only trust the refusal when you hold a saved id. One extra field in the request versus a week of deduplicating analytics after the fact.
Retries with a backoff, and a dead-letter path. The naive version retries instantly, three times, then drops the event. That turns a 30-second network blip into silent data loss. Retry with increasing delays, cap it, and write anything that still fails somewhere a human will see it. The same applies on the receiving end. Your webhook handler should return quickly and process later, because a handler that takes 20 seconds under load looks exactly like an outage to the sender, which triggers retries, which means duplicate events, which brings you back to idempotency. Every webhook payload carries an event_id. Store it and skip what you've already processed.
Filter before you trust a count. Raw click totals include Slack unfurling your link, email scanners opening it before any human does, and crawlers hitting it forever. The gap between raw and filtered counts can be large enough to flip which channel looks like the winner. Our bot clicks guide makes the case with a worked example; the short version is that a warehouse fed unfiltered events is a warehouse that can rank channels wrong.
Every click Acturity processes is scored for bot likelihood, and suspected bots show up in the bot traffic report. Excluding them from counts, billing and webhooks is a switch that Acturity operates, and it is off by default. Until it is switched on, the numbers and webhook events you pipe downstream include bot clicks. The webhook payload carries the link id and short code, a timestamp, the IP address, user agent, referrer, location and device details, but not a bot verdict. So if you need clean data sooner, apply your own user-agent and rate rules downstream. That's one of the three problems partly handled for you. The other two are still yours.
If you want to see what a finished pipeline looks like, the post on automating link marketing with webhooks walks through a click-to-CRM workflow.