Short links with click tracking and campaign attribution
Orbit’s short-link subsystem runs onPOST /api/v1/links. Send a URL, get a tracked short URL back, then read per-click analytics that attribute the tap back to the recipient, campaign, and message that sent it. Send time can mint links inline when metadata.shorten_urls is set, so a campaign template with a long https://… destination goes out with a compact, click-tracked URL without changing the template. This guide covers the managed short-link surface; the quick-action deep-links guide covers dashboard compose URLs, which are a separate feature.
For request and response schemas see the links API reference. This page covers when to use each mode and what each click surface gives you.
1. What a short link is in Orbit
Every short link is a row with an id, a 6-character code, and the original URL it points to. The public redirect isGET /l/:code, mounted at the root of the API host (not under /api/v1), anonymous, and rate-limited per IP. A minted URL therefore looks like https://api.orbit.devotel.io/l/AbC123 on the platform default, or https://go.yourbrand.com/l/AbC123 on a branded domain.
Each click on the redirect records a raw click row (IP, user-agent, referrer), a best-effort country from the edge CDN geo header, and a traffic-quality classification — then fans out to per-link stats, per-contact click history, and a short_link.click webhook event. Code generation uses rejection sampling over a 56-character alphanumeric alphabet (no ambiguous 0/O/1/I/l characters), so codes are uniformly distributed.
2. Auto-shorten inside a send
The send pipeline shortens URLs inline in three cases: Explicit opt-in. Passmetadata.shorten_urls: true on the send (the dashboard compose dialog shows a preview before launch). On non-SMS channels this shortens every http(s):// URL — shortening there is for click analytics, so length is irrelevant. On SMS it refuses to swap a URL for an equal-or-longer short link, because the whole point of SMS shortening is to cut segment cost.
SMS auto-shorten. Defaults ON per tenant (sms_auto_shorten_urls in org settings; set it to false to disable). It only shortens URLs longer than 30 characters AND longer than the minted short-URL length for your resolved domain — so it never makes a body longer than the original, which matters on the platform default domain where minted links run ~37 characters.
WhatsApp auto-track. Defaults ON per tenant (whatsapp_auto_shorten_urls). WhatsApp linkifies a scheme-less www. host into a tappable link, so this mode also mints for those spans (anchored on www. plus at least two dot-separated labels, so prose like index.js or version 2.0 is never mistaken for a link). Without it, a tap on a raw link in a free-text WhatsApp body never reaches our redirect and short_link.click never fires.
Across all three modes, minting is fire-and-forget per URL: if a mint call fails, the original URL stays in the body verbatim and the send proceeds with the long link. Trailing prose punctuation captured alongside a URL (See https://example.com/sale. ) is stripped from the minted URL but left in the on-wire text, so the recipient still reads the original punctuation.
3. Explicit shortening — one-off links
POST /api/v1/links mints a tracked link outside any send — for web pages, social bios, print collateral, or partner distribution. The body is { url, campaign_id? }; the response carries the record plus short_url. Only http:// and https:// URLs are accepted.
Use this when the link is going into a surface you don’t control through the Orbit send pipeline — you still get the same click-tracking, stats, and attribution as a send-embedded link. Add campaign_id explicitly when the link belongs to a campaign and you want it on the per-campaign analytics rollup.
4. Branded short domains
Set a branded domain under Settings → General → Branded short-link domain. The resolution order is:- Your tenant’s branded domain, if set. It must be a bare
https://(orhttp://for staging) host with no path, query, or fragment —https://go.yourbrand.com, nothttps://go.yourbrand.com/landing. DNS must point to Orbit’s redirector with a valid TLS certificate and a servedGET /l/:coderoute. - Platform default —
https://api.orbit.devotel.io, resolved fromDEVOTEL_SHORT_LINK_DOMAINwhen an operator has provisioned a genuine vanity short host (e.g.https://orbt.to), thenDEVOTEL_API_URL.
/l/:code ingress rule before setting a branded domain; point it at a host that does not serve the redirect and newly minted links will fail to resolve.
5. Click-quality signals — human vs scanner
A click is recorded raw, then scored against a user-agent bot signature list: generic automation (bot, crawl, spider), link-preview fetchers (WhatsApp, Slack, Telegram, Facebook, Discord, LinkedIn), search/SEO crawlers, headless browsers, uptime monitors, and scripted HTTP libraries (curl, python-requests, okhttp, …). Each click gets:
is_bot— true when the UA matches a signature, or the UA is missing entirely.quality_score—1.0confident human,0.0confident bot,0.3missing UA. A verified click meansscore >= 0.5.quality_reason—bot_user_agent,missing_user_agent, or null.
6. Campaign attribution and webhooks
Every mint mintscampaign_id and message_id onto the link row, so the click can resolve back to the recipient contact through the originating message. Three read surfaces expose the attribution chain:
- Per-campaign rollup:
GET /api/v1/links/campaigns/{campaignId}/link-analyticsreturns totals (minted links, raw clicks, verified clicks, unique clicker recipients, attributed recipients), per-channel breakdown, and a top-links ranking — with derivedunique_attribution_rateandverified_attribution_rateguards so a campaign with no clicks never divides by zero. - Per-contact history:
GET /api/v1/links/contacts/{contactId}/clickscloses the loop — the CDP engagement view answers “which links did THIS recipient tap,” newest first. - Tenant-wide insights:
GET /api/v1/links/insightsbundles KPIs, top links, and a day-by-day click timeline in one call.
short_link.click webhook (link_id, code, url, click_id, message_id, campaign_id, ip, user_agent, referrer, country, clicked_at, is_bot, quality_score, quality_reason). Subscribe to it from a webhook endpoint for real-time click streams in your CRM or alerting — the dispatch is fire-and-forget so a slow consumer never blocks the redirect. Use is_bot / quality_score in the consumer the same way the analytics layer does, so your CTR is filtered consistently.
7. Landing pages
POST /api/v1/links/landing-pages builds a no-code microsite from a block list, on the same /links base path. A page starts in draft and only mints its trackable short URL when you POST /landing-pages/:id/publish. Per-page visits and conversions roll up at GET /landing-pages/:id/analytics by campaign and by originating message. The landing-page surface is tenant-facing API, not a platform introspection tool — the same state machine (draft → published → archived) applies whether you build pages via the API or the dashboard.
8. Error surface
POST /api/v1/links returns 422 Unprocessable Entity when url is missing, not a string, or fails URL parsing; the service additionally rejects any non-http(s) protocol with a validation error, so a javascript: or data: target never mints. Per-write rate limit: 20 requests per minute. Per-read: 60 requests per minute.
On the public redirect (GET /l/:code), a malformed code or an unknown/expired link returns a 400 or 404 — but browsers get a self-contained, branded HTML page (not a raw JSON envelope), while API clients keep the canonical JSON error shape. The page is noindex, carries no tracking, and never echoes the raw code. A tampered stored target that somehow drifted to a non-http(s) protocol is refused at redirect time with the same branded page.
For status taxonomy across the API, see error handling.
Troubleshooting
- Links minted with the long platform default, not my branded domain. The tenant setting must be a bare
https://hostwith no path/query/fragment — a value likehttps://go.yourbrand.com/pagefalls back to the default. Check the dashboard field or the warn log; after fixing, re-mint your links (existing short URLs keep resolving). - SMS body got longer with
shorten_urlsON. Impossible after the net-shortening guard — the explicit opt-in path also refuses equal-or-longer swaps. If you saw growth on an older send, the link predates the guard; re-send and the current send pipeline will no-op on that URL. - Click counts are inflated for a WhatsApp or SMS send. Filter on
verified_clicksandquality_score >= 0.5— link-preview fetches inflate raw clicks by design, because the redirect can’t distinguish a preview from a tap until it reads the user-agent. short_link.clickwebhook is absent for a free-text WhatsApp send. Template/broadcast sends carry no on-wire body (finalBodyis empty — Meta renders the approved template at send time), so auto-track only fires for free-text bodies. For template clicks, the tracking has to live in the template’s approved URL, which is a Meta re-approval concern.