Overview
Every request and answer is JSON (UTF-8). Send your key in the Authorization header.
Your app asks for a link when someone taps “share”; the link opens that same thing for whoever receives it.
iPhone, Android, Huawei and computers each go where they should, with your app scheme or your web page.
App keys can only create and open content links for the app links you pick. Your server key stays on your server.
Each answer tells you how many requests are left this month; errors say exactly what to fix.
Quick start
- 1Create a key
Settings → Developer API: a server key for your backend, and an app key for your mobile app.
- 2Create the app link once
With your store links and your app scheme (for example noor://). This is the address your content links live under.
- 3Make content links from the app
Each “share” tap asks for a content link with the screen’s path. Share the URL you get back.
- 4Open them in the app
When the app is opened by a link, call /resolve with it and open the path you get.
curl https://donelinks.com/api/v1/me \
-H "Authorization: Bearer dl_your_server_key"Keys and permissions
Create keys in Settings → Developer API. A key is shown once, when it is created; only a hash is stored. Revoke a key at any time and it stops at once.
dl_…For your backend only. It does what its permissions allow. Never put it in an app or a web page.
dl_app_…Safe to ship inside your mobile app: it can only create and resolve content links for the app links you chose. It cannot read, change or delete anything.
Permissions of server keys
links:read- List and read your app links and their QR codes.
links:write- Create, change and delete app links (includes links:read).
content:write- Create, list, change and delete content links, and resolve them.
stats:read- Visit statistics (Stats API — plans with statistics API).
Limits
Each account has a number of API requests a month (all its keys together) and a per-minute limit, set by its plan. A batch counts one request per item. Requests refused for a wrong key, permission or plan are not counted.
| Plan | Requests a month | Requests a minute |
|---|---|---|
| Professional | 10,000 | 60 |
| Team | 100,000 | 300 |
| Team Plus | 250,000 | 600 |
| Enterprise | Unlimited | 1,200 |
Every answer carries these headers:
X-RateLimit-Limit- Requests a month on your plan, or “unlimited”.
X-RateLimit-Remaining- Requests left this month.
X-RateLimit-Reset- When the month starts again (Unix seconds).
X-Request-Id- Quote it when you contact support.
429 rate_limited: too many in a minute — wait for Retry-After seconds. 429 quota_exceeded: the month is used up — it starts again on the 1st, or upgrade.
Errors
Errors are JSON with a stable code, a readable message and, when it helps, the field to fix:
{
"error": {
"code": "scope_required",
"message": "This key cannot do that. It needs the \"links:write\" permission.",
"scope": "links:write"
},
"requestId": "req_9QbX2kL0aV3m"
}| Code | Status | Meaning |
|---|---|---|
missing_key / invalid_key | 401 | No key, a wrong key, or a revoked key. |
plan_restricted | 403 | Your plan does not include this (error.feature says which). |
scope_required | 403 | The key lacks the permission (error.scope). |
link_not_allowed | 403 | An app key used for an app link it was not made for. |
unknown_field / invalid_field / invalid_fields | 400 | A field is unknown or not valid (error.field or error.fields). |
nothing_to_open | 400 | A content link needs a path, deepLink, webUrl or params. |
link_not_found / content_not_found | 404 | Not in this account. |
slug_taken | 409 | This slug is used. |
plan_limit_exceeded | 402 | The plan’s number of links is reached. |
idempotency_mismatch | 422 | An Idempotency-Key reused with another request. |
content_expired | 410 | The content link passed its expiresAt. |
rate_limited / quota_exceeded | 429 | Per-minute limit or monthly requests. |
Retries and pages
Safe retries (Idempotency-Key)
Send an Idempotency-Key header (up to 100 letters, digits or _-:.) when creating links or content links. If the network fails and you retry with the same key within 24 hours, you get the first result back instead of a second link (header Idempotent-Replayed: true). Errors are not stored, so a fixed request can be retried.
Pages of results
Lists answer { data, hasMore, nextCursor }. Ask for the next page with ?cursor=<nextCursor>; limit is 1 to 100 (20 by default).
Links
App links are the DoneLinks you know from the dashboard: one link that sends every visitor to the right store. The API creates and manages app links (pages such as link-in-bio or menus stay in the dashboard). All dashboard plan rules apply: number of links, custom slugs, domains and features.
| Method | Path | Permission | What it does |
|---|---|---|---|
| GET | /me | any | The key, its account and this month’s usage |
| GET | /links | links:read | List app links (q= search, cursor pages) |
| POST | /links | links:write | Create an app link |
| GET | /links/{slug} | links:read | One app link |
| PATCH | /links/{slug} | links:write | Change an app link (slug renames it) |
| DELETE | /links/{slug} | links:write | Delete it with its statistics and content links |
| GET | /links/{slug}/qr | links:read | Its QR code (svg or png) |
curl -X POST https://donelinks.com/api/v1/links \
-H "Authorization: Bearer dl_your_server_key" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: noor-shop-app" \
-d '{
"slug": "noor-shop",
"title": "Noor Shop",
"destinations": {
"ios": "https://apps.apple.com/app/id1234567890",
"android": "https://play.google.com/store/apps/details?id=com.noor.shop"
},
"defaultUrl": "https://noor.shop",
"appSchemes": { "ios": "noor://", "android": "noor://" }
}'Link fields
slug- Custom name (plans with custom links). Random when left out.
title- Your own name for the link.
destinations- { ios, ipad, android, huawei, windows, mac, web, fallback } — store and site addresses.
defaultUrl- Where every other visitor goes.
appSchemes- { ios, android } such as "noor://" — content links build their deep links from it.
domain- A verified custom domain of the account.
options- { forwardParams, deferredDeepLink, desktopMode: auto | page | redirect }
campaigns- [{ id, name }] — tracked copies such as /noor-shop/instagram.
preview- { title, description } shown when the link is shared.
qr- QR design (pattern, colors, frame, logo), as saved in the dashboard.
In PATCH send only what changes; null clears a field. Unknown fields are refused so typos never go unnoticed.
Example answer
{
"id": "6720f1c2a9e4b1d3c5e7f901",
"slug": "noor-shop",
"url": "https://donelinks.com/noor-shop",
"title": "Noor Shop",
"domain": null,
"destinations": {
"ios": "https://apps.apple.com/app/id1234567890",
"android": "https://play.google.com/store/apps/details?id=com.noor.shop"
},
"defaultUrl": "https://noor.shop",
"appSchemes": { "ios": "noor://", "android": "noor://" },
"options": { "forwardParams": false, "deferredDeepLink": false, "desktopMode": "auto" },
"campaigns": [],
"preview": null,
"qr": {},
"stats": { "clicks": 0, "qrScans": 0, "storeOpens": 0, "lastClickAt": null },
"createdAt": "2026-10-19T09:30:00.000Z",
"updatedAt": "2026-10-19T09:30:00.000Z"
}QR codes
GET /links/{slug}/qr returns the link’s QR code in its own design. format=svg (default) or png, size 128–2048 (512 by default), campaign=<id> for a campaign’s code. The code opens the link with ?qr=1 so scans are counted apart.
Content links
A content link lives under an app link: https://donelinks.com/noor-shop/Xk7pQ2a, or on your domain go.noor.shop/noor-shop/Xk7pQ2a. The code is 7 letters and digits; letter case matters.
| Method | Path | Permission | What it does |
|---|---|---|---|
| POST | /links/{slug}/content | content:write · app keys | Create a content link |
| POST | /links/{slug}/content/batch | content:write | Create up to 100 |
| GET | /links/{slug}/content | links:read | content:write | List content links |
| GET | /links/{slug}/content/{code} | links:read | content:write | One content link with its counts |
| PATCH | /links/{slug}/content/{code} | content:write | Change it |
| DELETE | /links/{slug}/content/{code} | content:write | Delete it |
curl -X POST https://donelinks.com/api/v1/links/noor-shop/content \
-H "Authorization: Bearer dl_app_your_app_key" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: share-product-123" \
-d '{
"path": "product/123",
"params": { "ref": "user-42" },
"webUrl": "https://noor.shop/p/123",
"preview": {
"title": "Red dress",
"description": "Only 20 JD this week",
"imageUrl": "https://cdn.noor.shop/p/123.jpg"
}
}'Content link fields (at least one of path, deepLink, webUrl, params)
path- The screen in your app, e.g. "product/123". Added to the app link’s appSchemes → noor://product/123.
deepLink- An explicit app link instead: "noor://p/123" or { ios, android }.
params- Up to 20 text values, added to the deep links and returned by /resolve.
webUrl- The same content on the web: for computers and where the app cannot open.
preview- { title, description, imageUrl } — what WhatsApp, Facebook, X and others show.
campaign- One of the app link’s campaign ids, to count the visits for it.
expiresAt- ISO date; after it the link answers 410 (offers, invites).
What a content link does
Android- Opens your app on the content when it is installed (Chrome “intent”), otherwise Google Play. With deferred deep linking on your plan, Google Play passes the link to the app after the install.
iPhone and iPad- With Universal Links on your domain, iOS opens your app directly. Otherwise: the App Store.
Computers- The content’s webUrl; without one, what the app link gives computers (its website or the app page with a QR code of this content).
Chat apps- WhatsApp, Telegram, Facebook, X, LinkedIn… show the content’s own preview.
Content links follow their app link: its password and visit cap, blocking, store links, country routing and statistics. During maintenance mode they keep working.
Example answer
{
"code": "Xk7pQ2a",
"url": "https://donelinks.com/noor-shop/Xk7pQ2a",
"link": "noor-shop",
"path": "product/123",
"deepLink": { "ios": "noor://product/123?ref=user-42", "android": "noor://product/123?ref=user-42" },
"params": { "ref": "user-42" },
"webUrl": "https://noor.shop/p/123",
"preview": { "title": "Red dress", "description": "Only 20 JD this week", "imageUrl": "https://cdn.noor.shop/p/123.jpg" },
"campaign": null,
"expiresAt": null,
"stats": { "clicks": 0, "appOpens": 0, "lastClickAt": null },
"createdAt": "2026-10-19T09:31:00.000Z",
"updatedAt": "2026-10-19T09:31:00.000Z"
}POST /links/{slug}/content/batch creates up to 100 at once. All or nothing: if one item is not valid nothing is created and error.index says which.
curl -X POST https://donelinks.com/api/v1/links/noor-shop/content/batch \
-H "Authorization: Bearer dl_your_server_key" \
-H "Content-Type: application/json" \
-d '{ "items": [
{ "path": "product/1", "webUrl": "https://noor.shop/p/1" },
{ "path": "product/2", "webUrl": "https://noor.shop/p/2" },
{ "path": "product/3", "campaign": "instagram" }
] }'Resolve in the app
When a DoneLink opens your app — through Universal Links / App Links on your domain, or right after an install from Google Play — send that link to /resolve and open what it returns. It also counts the open in the app.
| Method | Path | Permission | What it does |
|---|---|---|---|
| GET | /resolve?url=… | content:resolve · app keys | What a link should open in the app |
curl -G https://donelinks.com/api/v1/resolve \
-H "Authorization: Bearer dl_app_your_app_key" \
--data-urlencode "url=https://go.noor.shop/noor-shop/Xk7pQ2a"Honest note: on iPhone, opening the content right after a first install is not possible without an SDK inside the app (Apple does not pass the link through the App Store). DoneLinks does not use fingerprinting. The app opens normally; links tapped after that open the content.
Share button guide
- 1Create the app link (once)
With your store links and app scheme. If you have a custom domain, put the link on it: Universal Links need your own domain.
- 2In the app: on “share”
Ask for a content link with the screen’s path and a preview, then share the URL.
- 3In the app: when opened by a link
Send the link to /resolve and open the returned path.
- 4Android after an install
Read the Install Referrer once and resolve it (deferred deep linking).
Universal Links (iOS) and App Links (Android)
For links to open your app directly, the app must trust your domain. Add your domain in Settings → Custom domains, then in “Open in app” upload these two files. DoneLinks serves them on your domain.
{
"applinks": {
"details": [
{
"appIDs": ["TEAMID.com.noor.shop"],
"components": [{ "/": "/noor-shop/*" }]
}
]
}
}[{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.noor.shop",
"sha256_cert_fingerprints": ["AB:CD:EF:…"]
}
}]<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" android:host="go.noor.shop" android:pathPrefix="/noor-shop/" />
</intent-filter>Statistics
GET /stats is the Stats API: visits per day, device, country, referrer and UTM for each link (period=yesterday | 30d | 400d, link=<slug>, format=csv). It needs the stats:read permission and a plan with statistics API. Keys made for it before keep working.
| Method | Path | Permission | What it does |
|---|---|---|---|
| GET | /stats | stats:read | Visit statistics (Stats API) |
From Firebase Dynamic Links
Firebase Dynamic Links shut down in August 2025. Here is how its pieces map to DoneLinks:
| Dynamic Link domain (xyz.page.link) | Your domain (go.brand.com) or https://donelinks.com |
| Long link parameters ibi, isi, apn, afl, ifl | App link: destinations and appSchemes |
| link= (the deep link) | Content link: path or deepLink, and webUrl |
| Social tags st, sd, si | Content link: preview { title, description, imageUrl } |
| REST API shortLinks | POST /links/{slug}/content |
| getDynamicLink / handleUniversalLink | GET /resolve |
| Deferred deep links | Android: yes (Install Referrer). iOS: no (see the note above). |
OpenAPI and tools
The whole API is described in OpenAPI 3.1: import it into Postman or Insomnia, generate a client, or give it to an AI assistant.
https://donelinks.com/api/v1/openapi.jsonPostman: Import → Link → paste the OpenAPI address.