Limen

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

  1. You create an API key in Settings → API. It's like a password for your programs.
  2. Your program sends a request to a Limen address (an “endpoint”) with the key attached.
  3. 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_here

Create 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 characters
  • url — required, the real destination (https://…)
  • slug — optional custom name, 3–32 letters, numbers or dashes
  • steps — 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 steps
  • link.premium_unlocked — a Premium member opened it
  • premium.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 messages

Answer 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.