Skip to main content

Idempotent Link Creation

The problem

A common pattern in mobile apps is to call the link creation API every time a user taps "Share". Functionally this works — every tap produces a working link. But it creates a problem: thousands of duplicate links pointing at the same content, each receiving only a handful of clicks.

The result: a distorted "average clicks per link" metric, an inflated total link count, and noisy attribution data.

The fix: pass externalId

Every link-creation endpoint accepts an optional externalId field. ULink scopes externalId to your project and enforces uniqueness: if you call create with an externalId that already exists in your project, ULink returns the existing link instead of creating a duplicate.

Pick a deterministic key from your own system — typically a composite of the IDs that uniquely identify what the link points at:

Use caseSuggested externalId
User profile sharingprofile:${userId}
Post sharing (per-user)share:user:${userId}:post:${postId}
List sharing (per-user)share:user:${userId}:list:${listId}
Campaign linkcampaign:${campaignId}

The key can be anything from 1 to 255 characters, no whitespace. ULink does not interpret its contents — it's just an identifier.

REST API example

# First call — creates the link, returns 201
curl -X POST https://api.ulink.ly/sdk/links \
-H "X-App-Key: $YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "unified",
"iosUrl": "myapp://post/456",
"androidUrl": "myapp://post/456",
"fallbackUrl": "https://example.com/post/456",
"externalId": "share:user:123:post:456"
}'
# → 201 Created { "slug": "abc123", "shortUrl": "https://shared.ly/abc123", ... }

# Subsequent calls with the same externalId — returns the same link, 200
curl -X POST https://api.ulink.ly/sdk/links \
-H "X-App-Key: $YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "unified",
"iosUrl": "myapp://post/456",
"androidUrl": "myapp://post/456",
"fallbackUrl": "https://example.com/post/456",
"externalId": "share:user:123:post:456"
}'
# → 200 OK { "slug": "abc123", "shortUrl": "https://shared.ly/abc123", ... }

Semantics

  • Scope: per project. Two different projects can each have a link with externalId = "x" — they won't collide.
  • Status code: 201 Created when a new link is created, 200 OK when an existing link is returned.
  • Strict idempotency: if a link already exists for an externalId, subsequent calls ignore the payload (different iosUrl, androidUrl, metadata are NOT applied to the existing link) and return the existing link as-is. To change a link's target URLs, use PUT /links/:id.
  • Immutability: externalId cannot be changed after a link is created. Attempts via PUT return 400 Bad Request. If you need a different identity, create a new link.
  • Backward compatible: externalId is optional. Existing integrations that omit it keep working unchanged — each call creates a new link, as before.

Validation

ULink rejects invalid externalId values with 400 Bad Request:

  • Must be 1–255 characters
  • Must not contain whitespace (space, tab, newline, etc.)
  • Must be a string (numbers and other types are rejected)

When you don't need externalId

You can skip externalId for one-off links: marketing campaigns where each link is intentionally unique, A/B test variants, throwaway short URLs. The field is purely opt-in.

Migrating an existing integration

If your app already creates duplicate links today, adding externalId only prevents future duplicates. To clean up existing duplicates retroactively, contact support@ulink.ly — we can run a one-time dedup migration that merges duplicate links and consolidates click counts onto the canonical link.

Frontend best practices (still useful)

Even with externalId, you should:

  • Debounce share buttons — prevents concurrent API calls before the first response lands.
  • Cache locally — store the returned URL in your app state for the user's session so re-shares don't even need the network round-trip.

externalId is the safety net; local caching is the latency win.