← ALL GUIDES

Tracking Links Programmatically: API and Webhooks

Creating tracked links from code, receiving click and conversion events in real time, and feeding link data into your own warehouse.

LAST UPDATED AUG 20, 2026 · 8 MIN READ

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 nine out of ten integrations 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://yoursite.com/pricing?utm_source=newsletter&utm_medium=email&utm_campaign=aug-launch",
  "customAlias": "aug-pricing",
  "tags": ["newsletter", "q3"],
  "campaignId": "660e8400-e29b-41d4-a716-446655440001"
}

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. Nine out of ten integrations need only one of them, 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 campaign, and writes the rows to your warehouse. The REST API's analytics endpoints cover the same aggregations the dashboard shows (clicks by link, by source, by country, by day). 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 within seconds of it happening. 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 buildingUseWhy
A weekly campaign reportPullData that's 12 hours old is fine. One cron job, zero infrastructure.
A warehouse syncPullBatch loads want batches. Pulling a day at a time beats reassembling a day from 40,000 individual events.
A fraud check on signupsPushA 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 linkPushThe 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, and it holds up in practice. 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, usually 1 to 3 percent from retries, late-arriving events, and bot filtering applied at different stages. 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. The gap between the two isn't small, and it isn't shrinking. We cover the full picture, including what tracking protection actually blocks and why the loss lands hardest on high-intent channels, 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. A campaign with 40 conversions at $12 average and one with 15 at $90 average 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 account for nearly every broken link integration we see, and all three show up in the first month of production.

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. Use a deterministic custom alias, or key link creation on something stable like order-4412-confirmation, so the second attempt returns the existing link instead of minting a duplicate. 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. Store the event ID, 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. Based on what we see across campaigns, the gap between raw and filtered counts is large enough to flip which channel looks like the winner. The numbers on why click counts lie make the case in detail; the short version is that a warehouse fed unfiltered events is a warehouse that ranks channels wrong.

Acturity's API applies bot filtering before events reach your webhooks, so the counts you pipe downstream are already the human ones. That's one of the three problems handled for you. The other two are still yours.