Skip to main content

Short links cookbook

Every recipe here runs against the links surface (/api/v1/links) end to end — request in, decoded response out — so you can copy the exact envelope your integration receives. The concept guide (Short links with click tracking) explains the modes and quality signals; this page shows the wire format. Field and parameter definitions are in the links API reference. Authenticate every request with X-API-Key. Run against the sandbox with a dv_test_sk_… key, then swap in your live key.

Task index

POST /api/v1/links mints a tracked link outside any send — use it when the URL goes into a surface you do not control through the Orbit send pipeline (a web page, a social bio, print collateral, a partner hand-off). Send the destination URL and, when the link belongs to a campaign, the campaign id so it lands on the per-campaign rollup.
cURL
A 201 Created returns the full decoded envelope:
Three things to keep from the response:
  • data.code is the 6-character public code. The shareable URL is short_url{short-domain}/l/{code} — and GET /l/{code} is the anonymous, rate-limited public redirect (mounted at the API host root, outside /api/v1). On a tenant with a branded domain the same mint returns https://go.yourbrand.com/l/gH4kM2.
  • data.campaign_id is the attribution handle. Links minted without it drift out of the per-campaign rollup (recipe 3), even though their clicks still count tenant-wide.
  • Only http:// and https:// URLs mint. A missing, non-string, or unparsable url answers 422; so does a javascript: or data: target. The write rate limit is 20 requests per minute.

2. Send an SMS with inline shortening

You do not have to mint links yourself before a send. Pass metadata.shorten_urls: true on the SMS send and the pipeline rewrites every http(s):// URL in the body into a tracked short link before the message leaves — stamping the campaign and message ids onto each minted link so clicks attribute back to this send.
cURL
The send returns the persisted message in the standard envelope:
The on-wire swap. What the recipient’s handset renders is the original text with the URL replaced:
Every minted link carries campaign_id and message_id from the send, so the click resolves back to this recipient (recipe 3). Two guards matter on SMS:
  • Net-shortening only. On SMS a URL is swapped only when the minted short URL is strictly shorter than the original — shortening on SMS exists to cut segment cost, so an equal-or-longer swap is refused and the original URL stays verbatim. On non-SMS channels every http(s) URL shortens regardless of length, because there the point is click analytics, not length.
  • Mint failures never fail the send. Shortening is fire-and-forget per URL: if the mint fails, the original URL ships untouched and the send proceeds.
If you skip the metadata flag entirely, the per-tenant SMS auto-shorten setting (sms_auto_shorten_urls, default ON) still rewrites URLs longer than 30 characters. The explicit metadata.shorten_urls: true opt-in is for URLs below that floor.

3. Read campaign, per-contact, and tenant analytics

Three read surfaces answer three different questions. All clamp out-of-range window_days / top_limit values instead of rejecting them, and every top_links row re-attaches the resolved short_url so you can display it without reconstructing the domain. Per-campaign rollup — “did this campaign’s links drive clicks?”
cURL
Read verified_clicks for human-only CTR — link-preview fetches and scanners inflate raw clicks by design. unique_attribution_rate is unique clicker recipients over attributed recipients (the recipients the campaign’s tracked messages reached); both derived rates report 0 when the denominator is empty, never NaN. Per-contact history — “which links did THIS recipient tap?”
cURL
Newest first, bounded by limit (1–500, default 100). Clicks resolve to a contact through the originating message, so an anonymous click on a link that was never message-attributed never appears here. Tenant-wide insights — one call for the dashboard KPI row
cURL
GET /api/v1/links/insights bundles summary KPIs, the top-N ranking, and a day-by-day click timeline in one call — the same aggregates that power Insights → Link tracking in the dashboard. Pass campaign_id to restrict the whole bundle to one campaign.

4. Build and publish a landing page

Landing pages are no-code microsites built from a block list, served under the same /links base path. A page starts in draft; publishing mints its trackable short URL, so clicks attribute exactly like any other tracked link.
cURL
While status is draft, short_url and render_url stay null — the page has no public address. A slug you pass is suffixed on collision (may-promo-3fka) so it never fails on a duplicate.
cURL
Publish enforces a non-empty block list — a page with zero blocks answers 422. The minted short_url (/l/{code}) redirects to the internal render path (/lp/{code}), so the click-tracking pipeline covers landing-page traffic with no extra wiring. Per-page visits and conversions then roll up by campaign and by originating message:
cURL
Subscribe an endpoint under Settings → Webhooks to short_link.click and every tracked click POSTs to you in real time — the recipient-side sibling of email.clicked. Verify the X-Orbit-Signature header before trusting the body (one function call per Webhook consumer). The decoded payload:
Node consumer
Filter on is_bot / quality_score (>= 0.5 means verified human) exactly the way the analytics endpoints do, so your CTR matches the dashboard. A missing user_agent scores 0.3 because a real browser essentially always sends one; quality_reason names the verdict (bot_user_agent | missing_user_agent). country is the edge-resolved ISO-3166-1 alpha-2 code, or null when the edge supplies no geo header. Delivery never blocks the redirect — a slow consumer cannot slow the clicker’s page load, and Orbit retries a failed POST on its own tail.

6. Pick a read surface

Raw clicks accumulate faster than most consumers should poll them. Match the question to the surface: Rule of thumb: the webhook for streams, the analytics endpoints for aggregates, the delivery logs for forensics. Poll an aggregate endpoint on a schedule only when you genuinely cannot run an endpoint for the webhook.

Troubleshooting

  • My mint returned the platform default host, not my branded domain. The tenant branded-domain value must be a bare https://host with no path/query/fragment; a malformed value falls back to the default rather than failing. Fix the setting and re-mint — existing short URLs keep resolving.
  • A 422 on POST /api/v1/links. url is missing, not a string, or fails URL parsing; a non-http(s) protocol is also refused. On landing pages, 422 at publish means the block list is empty.
  • Click counts look inflated on SMS or WhatsApp sends. Read verified_clicks (or filter the webhook on quality_score >= 0.5). Preview fetchers inflate raw clicks by design; the raw count is kept so retro-filtering applies to already-collected data.
  • No short_link.click for a WhatsApp template send. Template/broadcast sends carry no on-wire body to shorten — automatic tracking only fires on free-text bodies. Tracking inside a template’s URL has to live in the approved template itself.