Skip to main content

Links API

Links endpoints exposed by the Devotel CPaaS API Base path: /api/v1/links Endpoint count: 14 A tracked short link moves through four calls below — mint it (POST /api/v1/links), watch the click stream land (POST to the redirect, GET /api/v1/links//stats), page the full list (GET /api/v1/links), then receive the per-click webhook (short_link.click with an HMAC signature). Substitute your key and tenant ids where shown. Successful requests below use the standard { data, meta } envelope; the redirect (GET /l/{code}) is the one public surface that bypasses the envelope entirely (it issues a 302 with Location headers, no JSON body).
POST /api/v1/links
Mint a tracked link by passing the destination url. Drop UTM parameters straight into the destination URL (the API never parses them out — it treats the query string as opaque) and pair with campaign_id when the link belongs to a campaign so clicks roll up to the per-campaign analytics surface. The write rate limit is 20 requests per minute per tenant.
Two things worth keeping off the response: data.code is the 6-character public code (uniform distribution, no ambiguous characters) and data.short_url is the shareable string — {short-domain}/l/{code}. On a tenant with a branded domain (organizations.settings.short_link_domain) the same mint comes back with your vanity host, e.g. https://go.yourbrand.com/l/gH4kM2. Only http:// and https:// URLs mint; any other protocol answers 422.

2. Redirect-click deep dive

GET /api/v1/links/{id}/stats
A click on https://api.orbit.devotel.io/l/gH4kM2 redirects (302) to the destination, records IP + user-agent + referrer, resolves the best-effort country the edge CDN stamps (e.g. Cloudflare’s cf-ipcountry), classifies the click’s traffic quality (bot / preview vs probable-human), then lands in this endpoint’s stats view. Read it back with GET on the link id.
The recent_clicks array is the per-click forensics surface (recent-clicks capped at 50 rows; link_clicks rows past that cap drop off this view but still count in clicks_by_day). The is_bot verdict on each click comes from the same traffic-quality classification the per-campaign analytics view reuses, so a human-only CTR is (clicks where is_bot=false) / raw_clicks. Country resolution depends on the deployment’s edge — when the edge supplies no geo header, country reads null rather than a fabricated guess.
GET /api/v1/links?cursor=
The tenant’s full short-link surface pages newest-first via a cursor. Pass cursor=<next_cursor> on the next call; the response carries meta.pagination.next_cursor until you reach the tail. Filter by campaign_id when you only want one campaign’s links.
Counting is bounded: the list-count is capped (same bounded posture as the sibling messages / conversations / contacts list endpoints) so the total field saturates rather than scanning the tenant’s whole history. Use the cursor to page safely; never trust total as the exact count past the cap.

4. Signed webhook for click events

A click on a tracked short link fans out short_link.click to every webhook endpoint registered for the tenant. Verify the signature before reading the body — the header is X-Orbit-Signature (Stripe-style t=<unix_ts>,v1=<hex>; the legacy X-Devotel-Signature is still accepted as a fallback for old queued deliveries). The signed string is ${t}.${raw_body}; verification uses your endpoint secret. A complete per-language handler lives at Verify webhook signatures.
Node.js
The short_link.click payload is the consumer-side sibling of email.clicked: link_id + code identify the mint, message_id + campaign_id carry the attribution chain, and the country / user_agent / referrer fields match what the stats endpoint above returns. is_bot + quality_score + quality_reason let you filter preview / bot clicks on receive, the same way the analytics view filters them at read time. The short-links cookbook walks the whole chain with runnable end-to-end recipes (mint → send → measure → react, then landing pages); the samples above live here so this endpoint page answers “what comes back” without leaving the API reference. Reach for the cookbook when you want a full use case; reach for this page’s sections when you only need the wire shape of one endpoint.

Errors worth branching on

These five failures cover the short-link create (POST /api/v1/links), hammered by campaign sends that mint tracked URLs. Each block below is a full { error, meta } envelope as the API returns it, and the matrix at the bottom answers retry vs surface for the same five classes. For the platform-wide decision table these branches plug into, see the error handling guide.

401 — Unauthorized

A 401 on this page is the bearer key failing before the route ran — it never means the resource is wrong. Rotate the key or re-mint the scoped token; retrying the same request changes nothing.

403 — Forbidden

A 403 means the key authenticated but the operation is gated by scope — check the key’s scopes on the developer page; a 403 is never a data-not-found shape.

422 — Schema

A 422 means the payload did not match the request schema — branch on error.details.field and resend with an https:// destination_url instead of blind-retrying the same body.

429 — Rate

The Retry-After header and error.details.retry_after are both set on every 429 — resend the SAME request after the lower of the two.

404 — Not found

Surface the DNS record to re-check; do not recreate the link.

60-second retry matrix


GET /api/v1/links
string (enum: true|false)
Sandbox opt-in for Clerk-session-authenticated requests. Set to true to route the call through the test-mode pipeline: no real provider delivery, no credits deducted, response meta.test_mode: true. Ignored for live API keys (dv_live_sk_*) — server-to-server clients must use a test-prefixed key (dv_test_sk_*) to exercise sandbox. Test-prefixed keys unconditionally enable sandbox regardless of this header.

List stats

GET /api/v1/links/{id}/stats
string
required
—
string (enum: true|false)
Sandbox opt-in for Clerk-session-authenticated requests. Set to true to route the call through the test-mode pipeline: no real provider delivery, no credits deducted, response meta.test_mode: true. Ignored for live API keys (dv_live_sk_*) — server-to-server clients must use a test-prefixed key (dv_test_sk_*) to exercise sandbox. Test-prefixed keys unconditionally enable sandbox regardless of this header.

GET /api/v1/links/campaigns/{campaignId}/link-analytics
Per-campaign SMS link-tracking + click-attribution rollup: totals (minted links, raw clicks, verified human-only clicks, unique clicker recipients, attributed recipients, and the derived attribution rates), a per-channel breakdown, and a top-links ranking with each row’s resolved short URL. Out-of-range values clamp to the spec bounds; a campaign with no shortened links reports zeroed totals rather than NaN rates.
string
required
Campaign identifier the links were minted for.
integer
Trailing aggregation window in days (1–365, default 7).
integer
Max top-links rows (1–50, default 10).
string (enum: true|false)
Sandbox opt-in for Clerk-session-authenticated requests. Set to true to route the call through the test-mode pipeline: no real provider delivery, no credits deducted, response meta.test_mode: true. Ignored for live API keys (dv_live_sk_*) — server-to-server clients must use a test-prefixed key (dv_test_sk_*) to exercise sandbox. Test-prefixed keys unconditionally enable sandbox regardless of this header.

List clicks

GET /api/v1/links/contacts/{contactId}/clicks
string
required
—
string (enum: true|false)
Sandbox opt-in for Clerk-session-authenticated requests. Set to true to route the call through the test-mode pipeline: no real provider delivery, no credits deducted, response meta.test_mode: true. Ignored for live API keys (dv_live_sk_*) — server-to-server clients must use a test-prefixed key (dv_test_sk_*) to exercise sandbox. Test-prefixed keys unconditionally enable sandbox regardless of this header.

List insights

GET /api/v1/links/insights
string (enum: true|false)
Sandbox opt-in for Clerk-session-authenticated requests. Set to true to route the call through the test-mode pipeline: no real provider delivery, no credits deducted, response meta.test_mode: true. Ignored for live API keys (dv_live_sk_*) — server-to-server clients must use a test-prefixed key (dv_test_sk_*) to exercise sandbox. Test-prefixed keys unconditionally enable sandbox regardless of this header.

List landing pages

GET /api/v1/links/landing-pages
List the tenant’s landing pages with cursor-based pagination and an optional lifecycle-status filter.
string (enum: draft|published|archived)
Filter by lifecycle status.
string
Opaque cursor for the next page (from previous response meta.pagination.cursor).
integer
Number of items per page (default 25; silently capped at 200).
string (enum: true|false)
Sandbox opt-in for Clerk-session-authenticated requests. Set to true to route the call through the test-mode pipeline: no real provider delivery, no credits deducted, response meta.test_mode: true. Ignored for live API keys (dv_live_sk_*) — server-to-server clients must use a test-prefixed key (dv_test_sk_*) to exercise sandbox. Test-prefixed keys unconditionally enable sandbox regardless of this header.

Get a landing page

GET /api/v1/links/landing-pages/{id}
Fetch a single landing page by id.
string
required
Landing page identifier.
string (enum: true|false)
Sandbox opt-in for Clerk-session-authenticated requests. Set to true to route the call through the test-mode pipeline: no real provider delivery, no credits deducted, response meta.test_mode: true. Ignored for live API keys (dv_live_sk_*) — server-to-server clients must use a test-prefixed key (dv_test_sk_*) to exercise sandbox. Test-prefixed keys unconditionally enable sandbox regardless of this header.

Get landing page analytics

GET /api/v1/links/landing-pages/{id}/analytics
Per-page visits + conversions rolled up by campaign and by originating message, plus the most recent conversions.
string
required
Landing page identifier.
integer
Max rows per breakdown (by campaign / by message), 1–50, default 10.
string (enum: true|false)
Sandbox opt-in for Clerk-session-authenticated requests. Set to true to route the call through the test-mode pipeline: no real provider delivery, no credits deducted, response meta.test_mode: true. Ignored for live API keys (dv_live_sk_*) — server-to-server clients must use a test-prefixed key (dv_test_sk_*) to exercise sandbox. Test-prefixed keys unconditionally enable sandbox regardless of this header.

POST /api/v1/links
string
Stripe-style idempotency token. Pass a stable, client-generated value (1-255 chars) to dedupe retries on transient timeouts. The same key+credential+path replays the original response for 24h on 2xx (5min on 4xx, 30s on 5xx). Returns 409 if a concurrent request with the same key is already in flight; replayed responses include the Idempotency-Replay: true response header.
string (enum: true|false)
Sandbox opt-in for Clerk-session-authenticated requests. Set to true to route the call through the test-mode pipeline: no real provider delivery, no credits deducted, response meta.test_mode: true. Ignored for live API keys (dv_live_sk_*) — server-to-server clients must use a test-prefixed key (dv_test_sk_*) to exercise sandbox. Test-prefixed keys unconditionally enable sandbox regardless of this header.

Create a landing page

POST /api/v1/links/landing-pages
Create a draft no-code landing page. content is the builder block list, re-validated in depth by the service. The page starts in draft and only mints a trackable short URL once published.
string
Stripe-style idempotency token. Pass a stable, client-generated value (1-255 chars) to dedupe retries on transient timeouts. The same key+credential+path replays the original response for 24h on 2xx (5min on 4xx, 30s on 5xx). Returns 409 if a concurrent request with the same key is already in flight; replayed responses include the Idempotency-Replay: true response header.
string (enum: true|false)
Sandbox opt-in for Clerk-session-authenticated requests. Set to true to route the call through the test-mode pipeline: no real provider delivery, no credits deducted, response meta.test_mode: true. Ignored for live API keys (dv_live_sk_*) — server-to-server clients must use a test-prefixed key (dv_test_sk_*) to exercise sandbox. Test-prefixed keys unconditionally enable sandbox regardless of this header.
string
required
Human-readable page title.
string
Optional URL slug. Auto-derived from the title when omitted; unique per tenant.
any
Builder block list (page structure). Validated in depth by the service.
string
Optional campaign to attribute visits/conversions to.

Publish a landing page

POST /api/v1/links/landing-pages/{id}/publish
Publish a landing page and mint its trackable short URL. Transitions status to published and returns the page with short_url populated.
string
required
Landing page identifier.
string
Stripe-style idempotency token. Pass a stable, client-generated value (1-255 chars) to dedupe retries on transient timeouts. The same key+credential+path replays the original response for 24h on 2xx (5min on 4xx, 30s on 5xx). Returns 409 if a concurrent request with the same key is already in flight; replayed responses include the Idempotency-Replay: true response header.
string (enum: true|false)
Sandbox opt-in for Clerk-session-authenticated requests. Set to true to route the call through the test-mode pipeline: no real provider delivery, no credits deducted, response meta.test_mode: true. Ignored for live API keys (dv_live_sk_*) — server-to-server clients must use a test-prefixed key (dv_test_sk_*) to exercise sandbox. Test-prefixed keys unconditionally enable sandbox regardless of this header.

Update a landing page

PATCH /api/v1/links/landing-pages/{id}
Patch a landing page’s title, builder content, campaign attribution, or archive it. Every field is optional; content is re-validated in depth when present.
string
required
Landing page identifier.
string (enum: true|false)
Sandbox opt-in for Clerk-session-authenticated requests. Set to true to route the call through the test-mode pipeline: no real provider delivery, no credits deducted, response meta.test_mode: true. Ignored for live API keys (dv_live_sk_*) — server-to-server clients must use a test-prefixed key (dv_test_sk_*) to exercise sandbox. Test-prefixed keys unconditionally enable sandbox regardless of this header.
string
Human-readable page title.
any
Builder block list (page structure). Validated in depth by the service.
string | null
Campaign to attribute visits/conversions to. Pass null to clear.
string (enum: draft|archived)
Move the page between draft and archived.

DELETE /api/v1/links/{id}
string
required
—
string (enum: true|false)
Sandbox opt-in for Clerk-session-authenticated requests. Set to true to route the call through the test-mode pipeline: no real provider delivery, no credits deducted, response meta.test_mode: true. Ignored for live API keys (dv_live_sk_*) — server-to-server clients must use a test-prefixed key (dv_test_sk_*) to exercise sandbox. Test-prefixed keys unconditionally enable sandbox regardless of this header.

Delete a landing page

DELETE /api/v1/links/landing-pages/{id}
Delete a landing page; its visit/conversion events cascade. Returns 204 on success.
string
required
Landing page identifier.
string (enum: true|false)
Sandbox opt-in for Clerk-session-authenticated requests. Set to true to route the call through the test-mode pipeline: no real provider delivery, no credits deducted, response meta.test_mode: true. Ignored for live API keys (dv_live_sk_*) — server-to-server clients must use a test-prefixed key (dv_test_sk_*) to exercise sandbox. Test-prefixed keys unconditionally enable sandbox regardless of this header.
Response: 204 No Content