API v1 Dashboard →

LinkDrop API

Manage your links and page, pull views, clicks, campaigns and revenue into a spreadsheet, Airtable base or automation tool, and get every click pushed to you as a webhook. Plain JSON over HTTPS.

Included with the Business plan. Generate your key in the dashboard under Settings → Account → API. Base URL: https://linkdrop.uk/api/v1

Authentication

Send the key as a Bearer token on every request. x-api-key works too. Up to 10 named keys per account; a read-only key gets 403 on anything but GET.

curl https://linkdrop.uk/api/v1/me \
  -H "Authorization: Bearer ld_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
The full key is shown once, when you generate it. Treat it like a password: anyone holding it can change your links. If it leaks, Revoke it in the dashboard and generate a new one - the old key stops working instantly. Give each tool its own key so you can cut one without touching the others.

Pages

Every request acts on your main page by default. If you have extra link pages, pass ?page=<page id> on any endpoint to act on that page instead. Page ids come from GET /me.

GET /api/v1/links?page=9f2c1a4e-…

Errors & limits

Errors return a JSON body with an error message.

StatusMeaning
400Invalid input - the message says which field
401Missing or invalid key
402Plan does not include the API (or the feature you tried to set)
403Read-only key on a write request
404Page or link not found
429Rate limited - 120 requests per minute per key

Links are saved with the same validation as the dashboard: url must be http(s)://, title ≤ 120 chars, subtitle ≤ 200 chars, at most 100 links per page.

Endpoints

GET/api/v1/me

Your account and every page the key can reach.

{
  "id": "5c0e…", "username": "ella", "name": "Ella", "planTier": "business",
  "pages": [
    { "id": "5c0e…", "username": "ella",    "name": "Ella",  "url": "https://linkdrop.uk/@ella", "main": true,  "tags": [] },
    { "id": "9f2c…", "username": "ella-vip","name": "VIP",   "url": "https://links.ella.com",   "main": false, "tags": ["vip"] }
  ]
}

All links on the page, in display order. position is the zero-based index.

{
  "page": { "id": "5c0e…", "username": "ella", "url": "https://linkdrop.uk/@ella", "main": true, "tags": [] },
  "links": [
    { "id": "lm3k9x2a1b", "position": 0, "title": "My Fanvue", "publicTitle": null, "subtitle": "50% off today",
      "url": "https://www.fanvue.com/ella", "active": true, "kind": null, "imageUrl": null, "accentColor": null,
      "restricted": true }
  ]
}

restricted is true for adult-platform links - those get LinkDrop's protection layer automatically. kind: "post" marks a post-style tile.

One link. Returns { "link": { … } } with the same shape as above.

Add a link. Returns 201 with the created link.

FieldTypeNotes
titlerequiredstringButton label, ≤ 120 chars
urlrequiredstringhttps://…
subtitlestringSmall text under the label, ≤ 200 chars
publicTitlestringLabel shown to visitors when it should differ from title
activebooleanDefault true. false hides the link without deleting it
accentColorstringHex, e.g. #8b5cf6
imageUrlstringPublic https:// image for the button thumbnail
position"top" | numberWhere to insert. Default: bottom
curl -X POST https://linkdrop.uk/api/v1/links \
  -H "Authorization: Bearer ld_live_…" -H "Content-Type: application/json" \
  -d '{ "title": "New drop 🔥", "url": "https://www.fanvue.com/ella", "subtitle": "Only this week", "position": "top" }'

Change any of the fields above. Send only what changes; null clears a field. position moves the link.

curl -X PATCH https://linkdrop.uk/api/v1/links/lm3k9x2a1b \
  -H "Authorization: Bearer ld_live_…" -H "Content-Type: application/json" \
  -d '{ "active": false, "subtitle": null }'

Removes the link. Returns { "ok": true, "id": "…" }. Its past clicks stay in your analytics.

Create up to 100 links in one call. Body: { "links": [ {…}, {…} ], "position": "top" } - each item takes the same fields as POST /links. Returns 201 with { "links": [ … ] } in the order they were inserted. Nothing is saved if any item is invalid.

Replace the whole list - the spreadsheet is the source of truth. Items with an existing id are updated and keep their stats; items without one are created; links missing from the array are deleted. The array order becomes the page order.

curl -X PUT https://linkdrop.uk/api/v1/links \
  -H "Authorization: Bearer ld_live_…" -H "Content-Type: application/json" \
  -d '{ "links": [
        { "id": "lm3k9x2a1b", "title": "My Fanvue", "active": true },
        { "title": "New: custom video", "url": "https://www.fanvue.com/ella/shop" }
      ] }'

Response: { "links": [ … ], "removed": [ "id", … ] }.

{ "ids": [ "…", "…" ] } - reorders without touching content. Ids you leave out keep their relative order after the ones you listed.

Extra link fields

FieldNotes
teasertrue shows the link as a blurred teaser tile
sec.turnstiletrue asks a human check before the link opens (Pro+)
sec.geo{ "mode": "allow" | "block" | "off", "countries": ["US","GB"], "redirect": "https://…" } (Pro+)
imageUrlAlso accepts a data:image/…;base64,… upload (≤ 10 MB body) - it is stored on our CDN and the URL comes back in the response
GET/api/v1/page

The page itself: name, bio, accentColor, avatarUrl, avatarPos, banner, followers, verified, socials, linkCount, plus the summary fields from /me.

PATCH/api/v1/page
FieldNotes
name≤ 80 chars
bio≤ 500 chars
accentColorHex
avatarUrldata:image/… upload or null to remove
bannerdata:image/… upload or null for none
followersFree text shown on the page, e.g. "12.3k"
verifiedboolean - the blue tick
socials{ "instagram": "ella", "x": "ella", "fanvue": "ella", … } - keys: fanvue, onlyfans, instagram, threads, x, tiktok, snapchat, telegram

Username, security rules and Direct Link stay dashboard-only.

GET/api/v1/pages

Every page the key can reach - same list as /me.

POST/api/v1/pages

{ "username": "ella-vip", "name": "VIP", "cloneFrom": "<page id>" } creates an extra link page (Pro+, within your plan's page count). cloneFrom copies design and links from another of your pages. Returns 201 with the new page - use its id as ?page= from then on.

DELETE/api/v1/pages/:id

Deletes an extra page and its stats. The main page cannot be deleted.

GET/api/v1/analytics?days=30

Totals for the last days days (1-365, default 30), bucketed per day in the Europe/Bratislava timezone.

{
  "page": { … }, "days": 30, "since": "2026-08-22T22:00:00.000Z", "truncated": false,
  "totals": { "views": 4210, "clicks": 1388, "ctr": 33 },
  "byDay":  [ { "date": "2026-08-23", "views": 120, "clicks": 41 }, … ],
  "byLink": [ { "id": "lm3k9x2a1b", "title": "My Fanvue", "clicks": 902 }, … ],
  "bySource":  { "instagram": { "views": 3100, "clicks": 1002 }, "direct": { … } },
  "byCountry": { "US": { "views": 1800, "clicks": 610 }, "GB": { … } },
  "byCampaign": { "story-sep20": { "views": 410, "clicks": 190 } },
  "byDevice":  { "mobile": 3900, "tablet": 40, "desktop": 270, "unknown": 0 }
}

Add &campaign=<code> to get the same report for one campaign only.

GET/api/v1/campaigns?days=30

Campaign links need no setup: share your page as https://linkdrop.uk/@ella?c=story-sep20 (or your custom domain) and every view and click from that link carries the code. Codes are lower-case letters, digits, - and _, up to 32 chars. Put a different code in every post, story or ad and this endpoint tells you which one worked.

{
  "howTo": "https://linkdrop.uk/@ella?c=",
  "campaigns": [
    { "code": "story-sep20", "url": "https://linkdrop.uk/@ella?c=story-sep20",
      "views": 410, "clicks": 190, "ctr": 46.3,
      "byLink": [ { "id": "lm3k9x2a1b", "title": "My Fanvue", "clicks": 150 } ],
      "firstSeen": "2026-09-20T08:01:11Z", "lastSeen": "2026-09-20T21:40:02Z" }
  ]
}
GET/api/v1/revenue?days=30

Which link makes money - estimated. Needs Fanvue connected in the dashboard. Fanvue reports account-level net only, so each day's net is split across your earning links (Fanvue links, else all 18+ links) in proportion to that day's clicks. Days that earned with no clicks on any earning link land in unattributedRevenue (renewals, DMs, traffic that never touched LinkDrop). 7-90 days.

{
  "estimated": true, "connected": true, "days": 30, "coverageDays": 27,
  "totalRevenue": 4180.5, "attributedRevenue": 3120.0, "unattributedRevenue": 1060.5,
  "links": [ { "id": "lm3k9x2a1b", "title": "My Fanvue", "url": "https://www.fanvue.com/ella",
               "clicks": 902, "revenue": 2410.2, "revenuePerClick": 2.67 } ]
}
GET/api/v1/events

Raw view and click events, oldest first - the right feed for syncing into a table row by row.

QueryNotes
since, untilISO 8601 timestamps (until exclusive)
typeview or click
limit1-5000, default 500
cursorPass the previous response's nextCursor to get the next page
campaignOnly events from that campaign code
{
  "events": [
    { "id": 8812031, "type": "click", "linkId": "lm3k9x2a1b", "linkTitle": "My Fanvue",
      "source": "instagram", "country": "US", "device": "mobile", "campaign": "story-sep20", "at": "2026-09-20T09:14:02.331Z" }
  ],
  "nextCursor": "8812031"
}

Store the last id you processed and call again with cursor=<that id> - you only ever receive new events, no duplicates. Prefer a webhook if you want events as they happen.

Webhooks

Register an https URL and LinkDrop POSTs a JSON body to it as things happen - no polling. Up to 5 webhooks per account.

EventWhen
viewSomeone opened your page
clickSomeone tapped a link
link.created link.updated link.deleted links.replacedLinks changed through the API (dashboard edits do not fire these yet)
page.created page.updated page.deletedPage changed through the API
POST https://your-endpoint.example/linkdrop
Content-Type: application/json
X-LinkDrop-Event: click
X-LinkDrop-Delivery: 2f6c…            (unique per delivery - use it to dedupe)
X-LinkDrop-Signature: t=1758360000,v1=9a3f…

{ "id": "2f6c…", "type": "click", "createdAt": "2026-09-20T09:14:02.331Z", "pageId": "5c0e…",
  "data": { "type": "click", "linkId": "lm3k9x2a1b", "source": "instagram", "country": "US", "device": "mobile", "campaign": "story-sep20" } }

Verify the signature

Every webhook has a secret (shown once, when you create it). v1 is HMAC-SHA256 of <t>.<raw body> with that secret. Reject if it does not match or t is older than 5 minutes.

const crypto = require('crypto');
function verify(rawBody, header, secret) {
  const t = header.match(/t=(\d+)/)[1], v1 = header.match(/v1=([a-f0-9]+)/)[1];
  const expected = crypto.createHmac('sha256', secret).update(t + '.' + rawBody).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected)) && Date.now() / 1000 - t < 300;
}

Delivery & retries

Answer with any 2xx within 8 seconds. Anything else is retried after 1 s, 10 s and 60 s. After 20 deliveries in a row that failed every retry the webhook is switched off (active: false) - fix your endpoint and re-enable it with PATCH { "active": true }. Deliveries are at-least-once; dedupe on X-LinkDrop-Delivery.

GET/api/v1/webhooks

Your webhooks with failures, lastStatus, lastError, lastDeliveredAt.

POST/api/v1/webhooks
curl -X POST https://linkdrop.uk/api/v1/webhooks \
  -H "Authorization: Bearer ld_live_…" -H "Content-Type: application/json" \
  -d '{ "url": "https://hook.eu1.make.com/abc123", "events": ["click", "link.updated"] }'

{ "webhook": { "id": "…", "url": "…", "events": ["click","link.updated"], "active": true, "secret": "whsec_…" } }

Omit events to subscribe to everything. The URL must be public https - localhost and private networks are refused.

PATCH/api/v1/webhooks/:id

url, events, active. Re-enabling resets the failure counter.

POST/api/v1/webhooks/:id/test

Sends a test event right now and returns what your endpoint answered: { "ok": true, "status": 200, "body": "…" }.

DELETE/api/v1/webhooks/:id

Recipe: Airtable

  1. Create a Links table with fields Title, URL, Subtitle, Active (checkbox) and LinkDrop ID.
  2. Automation → trigger When record created → action Run script:
const KEY = 'ld_live_…';
const { recordId } = input.config();
const table = base.getTable('Links');
const rec = await table.selectRecordAsync(recordId);

const r = await fetch('https://linkdrop.uk/api/v1/links', {
  method: 'POST',
  headers: { 'Authorization': 'Bearer ' + KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    title: rec.getCellValueAsString('Title'),
    url: rec.getCellValueAsString('URL'),
    subtitle: rec.getCellValueAsString('Subtitle') || undefined,
    active: !!rec.getCellValue('Active'),
  }),
});
const data = await r.json();
if (!r.ok) throw new Error(data.error);
await table.updateRecordAsync(recordId, { 'LinkDrop ID': data.link.id });
  1. A second automation on When record updated does the same with PATCH /links/<LinkDrop ID>; a checkbox "Delete" can trigger DELETE.
  2. For a Clicks table, either run a scheduled script every hour that calls /events?cursor=<last id> and creates one record per event, or - live - create an Airtable automation with the When webhook received trigger, register its URL with POST /webhooks for click, and map data.linkId, data.source, data.country, data.campaign into the record.

Recipe: Make / n8n / Zapier

Use the generic HTTP module. Method and URL from the tables above, header Authorization: Bearer ld_live_…, body type JSON. Map the response's link.id back into your source record so later steps can update or delete it. To react to clicks, add a Custom webhook trigger, copy its URL into POST /webhooks and every click starts the scenario with the payload above.

Questions or a missing endpoint? Write to [email protected].