API · v1
Change a campaign's link through the API
List the Instagram accounts and campaigns of your DM Link account and change where a campaign's link points, from any other system.
How it works
When a DM Link campaign sends its link in a DM, it does not send the final address directly. It sends a DM Link tracked link, like https://dmlink.app/r/abc123, that redirects to the configured destination.
The API changes that destination. So:
- people who get the DM from now on land on the new destination;
- people who got the DM earlier and tap it later also land on the new destination;
- the campaign keeps running unchanged, nobody has to edit it.
The code at the end of the tracked link (abc123) is the slug. It identifies which link to change. A typical use: a bot that manages WhatsApp groups swaps the link to the next group when the current one fills up.
1. Get an API key
- Sign in to DM Link as the owner or an admin of the account.
- Go to Settings → API keys.
- In “What will you use this key for?”, write a name just so you recognize it later (e.g. my WhatsApp groups system) and click Create key.
- Copy the key right away. It starts with
dml_and is shown only once. After that not even DM Link can show it again.
On the same screen you see the active keys, deactivate any of them (it stops working immediately) and follow the history of the latest link changes made through the API.
One account, one key, every Instagram connected to it
- A key belongs to the DM Link account where it was created.
- It sees and changes links of every Instagram account connected to that DM Link account (up to 3 on Infinity). You do not need one key per Instagram.
- It does not see other DM Link accounts. If the Instagram you want is connected to another DM Link account, either connect it to this one or create a key in that account.
To check that a key belongs to the right account, call GET /api/v1/instagram-accounts: if the Instagram you expect is not in the list, the key belongs to another account.
2. Base URL and authentication
Base URL
https://dmlink.app/api/v1Send the key on every call
Authorization: Bearer dml_YOUR_KEYX-API-Key: dml_YOUR_KEY is accepted too. Responses are always JSON: {"success": true, "data": ...} or {"success": false, "error": {"code": "...", "message": "..."}}.
| Method | Path | What it does |
|---|---|---|
GET | /api/v1/instagram-accounts | List the Instagram accounts connected to the account |
GET | /api/v1/campaigns | List the campaigns and their links, for every Instagram |
GET | /api/v1/campaigns?instagram=@username | List only one Instagram's campaigns |
GET | /api/v1/links/{slug} | See where a link points now |
PATCH (or PUT) | /api/v1/links/{slug} | Change where a link points |
3. List the account's Instagram accounts
Returns the Instagram accounts connected to the key's account, in the order they were connected. Use it to let someone pick which Instagram to work with.
curl https://dmlink.app/api/v1/instagram-accounts \
-H "Authorization: Bearer $DMLINK_API_KEY"Response
{
"success": true,
"data": [
{
"id": "cm9z8y7x6w5v4u",
"username": "mybrand",
"name": "My Brand",
"needsReconnect": false,
"campaignCount": 1
},
{
"id": "cm1q2w3e4r5t6y",
"username": "another.profile",
"name": null,
"needsReconnect": false,
"campaignCount": 2
}
]
}| Field | Meaning |
|---|---|
id | The Instagram account's id in DM Link (never changes). |
username | The Instagram handle, without the @. |
name | Profile name (may be null). |
needsReconnect | true when Meta dropped the connection: this Instagram's campaigns stop answering until the owner reconnects it in DM Link. Changing links keeps working. |
campaignCount | How many campaigns this Instagram has (active and paused). |
4. List the campaigns of an Instagram account
Returns the campaigns, newest first, with their tracked links and which Instagram each one belongs to. Use it to find the slug of the link you want to change.
- Without
?instagram=: campaigns of every Instagram in the account. - With
?instagram=: only that Instagram's. Accepts theusername(with or without@, any case) or theidfrom step 3. If the Instagram is not in the key's account, the answer is404 not_found. - Both lists only include connected Instagram accounts. One the owner disconnected disappears from both until it is reconnected.
About each campaign's links:
position: 1is the campaign's main button.position: 2is the second button, when there is one.- The order is stable: a link always keeps its position, even after its destination changes.
curl "https://dmlink.app/api/v1/campaigns?instagram=mybrand" \
-H "Authorization: Bearer $DMLINK_API_KEY"Response
{
"success": true,
"data": [
{
"id": "cm1a2b3c4d5e6f",
"name": "VIP group",
"active": true,
"instagram": { "id": "cm9z8y7x6w5v4u", "username": "mybrand" },
"links": [
{
"slug": "abc123",
"position": 1,
"label": "Primary campaign link",
"destinationUrl": "https://chat.whatsapp.com/CURRENT_GROUP",
"trackedUrl": "https://dmlink.app/r/abc123",
"updatedAt": "2026-10-02T12:00:00.000Z"
}
]
}
]
}5. See where a link points
curl https://dmlink.app/api/v1/links/abc123 \
-H "Authorization: Bearer $DMLINK_API_KEY"Response
{
"success": true,
"data": {
"campaignId": "cm1a2b3c4d5e6f",
"slug": "abc123",
"position": 1,
"label": "Primary campaign link",
"destinationUrl": "https://chat.whatsapp.com/CURRENT_GROUP",
"trackedUrl": "https://dmlink.app/r/abc123",
"updatedAt": "2026-10-02T12:00:00.000Z"
}
}6. Change where a link points
Body (JSON)
{ "destinationUrl": "https://chat.whatsapp.com/NEW_GROUP" }- The address must be complete, start with
https://and have at most 2048 characters. WhatsApp invite links (https://chat.whatsapp.com/...) are accepted as they come, including anything after the?. - The change applies immediately:
dmlink.app/r/{slug}redirects to the new destination on the very next request. - Sending the same destination again is not an error: the answer is
"changed": falseand nothing changes, so the call is safe to retry. Addresses are compared normalized, sohttps://site.comandhttps://site.com/count as the same. - The address is stored normalized (e.g.
https://site.combecomeshttps://site.com/). That does not change where the link goes.
curl
curl -X PATCH https://dmlink.app/api/v1/links/abc123 \
-H "Authorization: Bearer $DMLINK_API_KEY" \
-H "Content-Type: application/json" \
-d '{"destinationUrl": "https://chat.whatsapp.com/NEW_GROUP"}'JavaScript (Node)
// Node 18+ (built-in fetch)
async function changeCampaignLink(slug, newUrl) {
const res = await fetch(`https://dmlink.app/api/v1/links/${slug}`, {
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.DMLINK_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ destinationUrl: newUrl }),
});
const json = await res.json();
if (!json.success) {
throw new Error(`DM Link refused (${res.status}): ${json.error.code} - ${json.error.message}`);
}
return json.data; // { changed, previousDestinationUrl, destinationUrl, ... }
}Python
import os
import requests
def change_campaign_link(slug: str, new_url: str) -> dict:
res = requests.patch(
f"https://dmlink.app/api/v1/links/{slug}",
headers={"Authorization": f"Bearer {os.environ['DMLINK_API_KEY']}"},
json={"destinationUrl": new_url},
timeout=15,
)
body = res.json()
if not body.get("success"):
raise RuntimeError(f"DM Link refused ({res.status_code}): {body['error']}")
return body["data"] # { changed, previousDestinationUrl, destinationUrl, ... }Response
{
"success": true,
"data": {
"campaignId": "cm1a2b3c4d5e6f",
"changed": true,
"previousDestinationUrl": "https://chat.whatsapp.com/CURRENT_GROUP",
"slug": "abc123",
"position": 1,
"label": "Primary campaign link",
"destinationUrl": "https://chat.whatsapp.com/NEW_GROUP",
"trackedUrl": "https://dmlink.app/r/abc123",
"updatedAt": "2026-10-02T12:05:00.000Z"
}
}Errors and limits
| Status | error.code | Meaning / what to do |
|---|---|---|
| 400 | invalid_json | The body is not valid JSON. Send {"destinationUrl": "https://..."} with Content-Type: application/json. |
| 400 | invalid_destination | The new address is not a complete https:// URL (or is longer than 2048 characters). |
| 401 | unauthorized | Missing, wrong or deactivated key. Check the Authorization: Bearer dml_... header. |
| 403 | plan_required | The account is not on the Infinity plan. Its keys stop working while it is off Infinity and work again once it is back. |
| 404 | not_found | No link with this slug (or Instagram for this ?instagram=) in the key's account. List the Instagram accounts and campaigns to get the right one. |
| 429 | rate_limited | More than 60 calls per minute with the same key (the counter resets every clock minute). Wait and try again. |
Error messages (error.message) are in English; use error.code, which is fixed, to decide what to do in code. If you list campaigns often, cache the result instead of asking every time.
Response fields
| Field | Meaning |
|---|---|
changed | true if the destination changed; false if it already was that one. |
previousDestinationUrl | Where the link pointed before this call. |
destinationUrl | Where the link points now. |
trackedUrl | The tracked link sent in the DMs (never changes). |
instagram | In the campaign list: id and username of the campaign's Instagram. |
campaignId / position | The link's campaign and its position in it: 1 = main button, 2 = second button. A link's position never changes. |
Example: a full integration flow
- Once, at setup: call
GET /api/v1/instagram-accountsand pick the Instagram; thenGET /api/v1/campaigns?instagram={username}, pick the campaign bynameand store the link'sslug(usually the one withposition: 1). - Your system watches whatever decides the destination (e.g. how many people are in the current WhatsApp group).
- When it is time to switch, it calls
PATCH /api/v1/links/{slug}with the new address. - If the answer is
"success": true, done: DM Link is already sending everyone to the new destination. On an error, retry later (the call is safe to repeat).
Keep it safe
- Treat the key like a password. Keep it in an environment variable (e.g.
DMLINK_API_KEY), never in code or on GitHub. It works on every campaign of every Instagram connected to the account. - Leaked? Deactivate it in Settings → API keys and create another. The old one stops immediately.
- History: every change made through the API is logged (when, which campaign, from which address to which, by which key) in Settings → API keys.