For developers (and curious creators)
Limen API
The API lets your own programs talk to Limen. Instead of clicking “Create link” in the dashboard, a script, website or Discord bot can create links for you automatically.
How it works, in plain words
- You create an API key in Settings → API. It's like a password for your programs.
- Your program sends a request to a Limen address (an “endpoint”) with the key attached.
- Limen answers with data in JSON (a simple text format programs can read), like the new link.
Keep keys secret. Use them only on a server or in a bot — never inside a public website's code, or anyone could see and use your key. If a key leaks, revoke it in Settings and make a new one.
Your first request
Every request needs this header (replace the example key with yours):
Authorization: Bearer lmn_your_key_hereCreate a link from a terminal:
curl -X POST https://limen.ad/api/v1/links \
-H "Authorization: Bearer lmn_your_key_here" \
-H "Content-Type: application/json" \
-d '{"title": "My texture pack", "url": "https://example.com/pack.zip"}'Limen answers with your new link:
{
"data": {
"id": "…",
"slug": "k7m2qpx",
"title": "My texture pack",
"destination_url": "https://example.com/pack.zip",
"steps": 2,
"url": "https://limen.ad/s/k7m2qpx",
"premium_url": "https://limen.ad/p/k7m2qpx",
"stats": { "views": 0, "unlocks": 0, "premium_unlocks": 0 },
"created_at": "2026-09-17T12:00:00.000Z"
}
}Endpoints
POST/api/v1/links
Create a link. Send JSON with:
title— required, up to 80 charactersurl— required, the real destination (https://…)slug— optional custom name, 3–32 letters, numbers or dashessteps— optional, 1, 2 or 3 (default 2)
GET/api/v1/links?limit=20&offset=0
List your links, newest first. limit is up to 100; use offset to get the next page.
GET/api/v1/links/{slug}
Get one link with its views, unlocks and Premium unlocks.
PATCH/api/v1/links/{slug}
Change a link without losing its stats or its address. Send only what you want to change: title, url, steps or slug. Handy when a download link dies: point it somewhere new and everyone who already shared your Limen link is fine.
DELETE/api/v1/links/{slug}
Delete a link. It stops working immediately.
GET/api/v1/stats?days=28
Totals for your account over 7, 28 or 90 days: views, unlocks, Premium unlocks, checkouts, Premium earnings and your balance. Money is in cents (999 = €9.99).
Webhooks: get told when something happens
Instead of asking us every minute, let us tell you. Add an address in Settings → Notifications (or with the API) and we send it a message the moment one of these happens:
link.unlocked— someone finished the stepslink.premium_unlocked— a Premium member opened itpremium.sale— you earned from a Premium payment
Using Discord? Paste a channel webhook address (Server Settings → Integrations → Webhooks) and we post a ready-made message in that channel. Nothing to build.
POST/api/v1/webhooks
Add an address: url (https), and optionally events (defaults to all). The reply contains a secret, shown only once.
GET/api/v1/webhooks
List your addresses, including whether the last message got through.
DELETE/api/v1/webhooks/{id}
Stop sending to that address.
Each message looks like this:
POST https://your-server.com/limen
X-Limen-Event: link.unlocked
X-Limen-Signature: t=1758140000000,v1=9f86d0…
{
"id": "evt_abc123",
"type": "link.unlocked",
"created_at": "2026-09-17T20:08:37.005Z",
"data": {
"link": { "id": "…", "slug": "mod-pack", "title": "Ultimate mod pack" },
"stats": { "views": 132, "unlocks": 44, "premium_unlocks": 7 }
}
}Check the signature so nobody can fake it. Take the part after t=, join it to the raw body with a dot, and hash it with your secret:
import { createHmac, timingSafeEqual } from "node:crypto";
const [t, v1] = req.headers["x-limen-signature"].split(",").map((p) => p.split("=")[1]);
const expected = createHmac("sha256", process.env.LIMEN_WEBHOOK_SECRET)
.update(`${t}.${rawBody}`)
.digest("hex");
const valid = timingSafeEqual(Buffer.from(expected), Buffer.from(v1)) &&
Date.now() - Number(t) < 5 * 60 * 1000; // ignore old messagesAnswer with any 2xx status within 5 seconds. We try once more after a failure; if an address keeps failing it is switched off, and you can turn it back on in Settings.
Example: a Discord bot command
In a Node.js Discord bot, a /lock command could turn any link into a Limen link:
const res = await fetch("https://limen.ad/api/v1/links", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.LIMEN_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ title: "Server resource pack", url: link }),
});
const { data, error } = await res.json();
if (error) return interaction.reply(error.message);
await interaction.reply(`Here's your link: ${data.url}`);Errors and limits
When something goes wrong you get an error code and a message you can show to people:
{ "error": { "code": "validation_error", "message": "Enter a full link starting with https://" } }- 401 — missing, wrong or revoked API key
- 404 — that link isn't in your account
- 409 — the custom name is already taken
- 422 — something in your data isn't valid (the message says what)
- 429 — too many requests: the limit is 60 per minute per key
Every reply tells you where you stand: X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (when the minute resets). A 429 also has Retry-After in seconds.