Reference

LunaCore API

The website's own JSON API, built for scripts and AI agents alike. Query products, releases with or without their changes, the changelog, the LunaNPC mod API reference, the issue tracker, or search all of it in one call. Every endpoint is listed here, including the ones that need a key.

Base URL https://lunacore.dev/api/v1

Endpoints16
No key needed12
Tiers3

Keys

Reads need nothing. Every GET works without a key, at the guest tier. Reports of unreleased projects stay hidden unless you are staff.

A key raises your tier and unlocks writes. Send it as a bearer token:

curl https://lunacore.dev/api/v1/me -H "Authorization: Bearer $LUNACORE_API_KEY"
  • Personal key. Any account can make one on its profile under Settings. It acts as you, at your tier, and can do on /api/v1 what you can do on the website. One per account; making a new one ends the old one.
  • Staff key. Made by staff on the server. It acts as the system bot account at tier 3. A staff member can apply it in their Settings to link it to their account, so its traffic shows there. Only staff accounts can apply one.

Keys start with luna_, are shown once, work only on/api/v1 and are stored as a SHA-256 hash, so nobody can read them back. Signed in on the website, your session works the same as your personal key.

Tiers and limits

Every request counts once against your tier, whatever it returns. Windows follow the clock: the minute, the hour and the day (Berlin time) each reset at their boundary. The limit parameter on an endpoint only caps how many rows one request returns. Guests are counted per IP address, everyone else per account, and each staff key on its own.

TierWhoper minuteper hourper day
Tier 1Guest. No key and not signed in. Counted per IP address.10100500
Tier 2Registered. Signed in, or sending your personal API key.505002,500
Tier 3Staff. A staff account, or a staff key.1001,0005,000

Each response carries x-ratelimit-tier, x-ratelimit-window,x-ratelimit-limit and x-ratelimit-remaining for the window closest to running out. Over a limit you get 429 with a retry-after in seconds. GET /api/v1/usage shows everything and is free to call.

AI on LunaCore

For transparency, these are the places AI is involved:

  • Writing up reports. When you file a report at /reports/new or post in the Discord report forum, your text, screenshots, chosen project and username are sent to an AI model (Claude Haiku 5.5 by Anthropic, reached through OpenRouter) run by the LunaCore Discord bot. It turns them into a titled, prioritised report. Nothing else from your account is sent.
  • LunaAI. When you chat at /ai, your messages, any images you attach, your username and whether you are staff go to Claude Haiku 5.5 by Anthropic, reached through OpenRouter. It looks things up with the same public data this API serves, and can search the web through OpenRouter when LunaCore doesn't have the answer. Every answer lists its sources. Your chats are saved to your account, and long chats are summarised by the same model to stay under 50,000 tokens.
  • A budget per account. Report write-ups and LunaAI come out of one small daily and weekly budget per account; staff get twice as much. Days and weeks follow Berlin time. The website keeps the books and the Discord bot asks it before every write-up, so both places share one budget. Your profile's Usage tab shows how much is used.
  • Staff triage. Staff may run an AI agent with the system key. It can reply on reports, fix titles and priorities, and close or reopen them. Its replies always show as System with a Staff badge, never as a person.
  • For your own agents. llms.txt describes this API in plain text so an AI can use it without reading the website.

Errors and conventions

Responses are JSON. Timestamps are ISO 8601 in UTC. Errors carry a status code and a message:

{
  "error": "unauthorized"
}
StatusMeaning
400The request is malformed, for example an unknown status value.
401The endpoint needs a key or session and none was valid.
403The key is valid but the account may not do this.
404No such item, or it is not public.
429Your tier's limit is used up. Wait retry-after seconds.

Endpoints 16

Products

GET

/api/v1/products

Tier 1 · Guest

Every product with its latest version, download totals, report counts and links.

Request

curl https://lunacore.dev/api/v1/products

Response, shortened

{
  "count": 2,
  "products": [
    {
      "slug": "LunaNPC",
      "name": "LunaNPC",
      "kind": "Minecraft mod",
      "latest_version": "0.1.0",
      "releases": 1,
      "downloads": {
        "total": 127,
        "last_30_days": 39
      },
      "reports": {
        "open": 3,
        "resolved": 0
      },
      "has_api_reference": true,
      "api": {
        "self": "https://lunacore.dev/api/v1/products/LunaNPC",
        "versions": "https://lunacore.dev/api/v1/products/LunaNPC/versions"
      }
    }
  ]
}
GET

/api/v1/products/{slug}

Tier 1 · Guest

One product. The slug is case-insensitive, e.g. LunaNPC.

GET

/api/v1/products/{slug}/versions

Tier 1 · Guest

Release history, planned versions first, then releases newest first, with download links, per-version download counts and what each version requires ("requires": Minecraft, loader, libraries, with "or_newer"). "released" is false for a planned version.

QueryAccepts
changelogtrue to include each release's changes, default false
limit1-200, default 50

Request

curl "https://lunacore.dev/api/v1/products/LunaNPC/versions?changelog=true"

Response, shortened

{
  "product": "LunaNPC",
  "latest": "0.1.0",
  "count": 1,
  "versions": [
    {
      "version": "0.1.0",
      "title": "LunaNPC 0.1.0",
      "minecraft": "26.2",
      "current": true,
      "downloads": 127,
      "download_url": "https://lunacore.dev/LunaNPC/download/0.1.0",
      "changes": [
        {
          "tag": "Added",
          "text": "First public release of LunaNPC"
        }
      ]
    }
  ]
}
POST

/api/v1/products/{slug}/versions

Tier 3 · Staff

Add a planned version that is not released yet, so reports can already be resolved in it. Publishing that version later releases it.

JSON bodyAccepts
versionstring, e.g. 2.0.0

Request

curl -X POST https://lunacore.dev/api/v1/products/LunaNPC/versions \
  -H "Authorization: Bearer $LUNACORE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"version": "2.0.0"}'

Response, shortened

{
  "product": "LunaNPC",
  "version": "2.0.0",
  "released": false,
  "released_at": null,
  "url": "https://lunacore.dev/LunaNPC#v2.0.0",
  "download": null,
  "created": true
}
GET

/api/v1/products/{slug}/versions/{version}

Tier 1 · Guest

One release with its changes. Use "latest" for the current release.

GET

/api/v1/products/{slug}/reference

Tier 1 · Guest

The product's mod API reference: types, functions, parameters and enums. Every type, function and field says the version it arrived in ("since"); functions carry their "history" of signature changes. An archived version lists what changed after it in "newer_changes".

QueryAccepts
versionAPI version or "latest" (the default); archived versions are listed in "versions"
typeonly this type, e.g. NpcTrade; adds the type's full change history
compacttrue for names, signatures and "since" only

Request

curl "https://lunacore.dev/api/v1/products/LunaNPC/reference?type=NpcTrade&compact=true"

Response, shortened

{
  "product": "LunaNPC",
  "version": "0.2.0",
  "latest": "0.2.0",
  "archived": false,
  "types": [
    {
      "name": "NpcTrade",
      "kind": "record",
      "group": "jobs",
      "since": "0.1.0",
      "summary": "One trade offer an NPC can make: input stacks for output stacks, an XP roll, and an optional stock limit…",
      "functions": [
        {
          "name": "isValid",
          "signature": "public boolean isValid()",
          "since": "0.1.0"
        }
      ],
      "url": "https://api.lunacore.dev/LunaNPC#type/NpcTrade"
    }
  ],
  "enums": []
}
GET

/api/v1/products/{slug}/reference/changes

Tier 1 · Guest

What changed in the mod API between versions: every added, changed or removed type, function, field and enum constant, with breaking flags and before/after signatures. No range = the whole history, oldest first.

QueryAccepts
versiononly what this version changed, e.g. 0.2.0 or "latest"
fromonly changes after this version (use with to for an upgrade path)
toonly changes up to and including this version
typeonly this type, e.g. NpcRegistry
memberonly this function, field or constant, e.g. getOrCreate
kindtype | function | field | enum-constant
changeadded | changed | removed
breakingtrue for breaking changes only

Request

curl "https://lunacore.dev/api/v1/products/LunaNPC/reference/changes?version=latest&breaking=true&kind=function"

Response, shortened

{
  "product": "LunaNPC",
  "latest": "0.2.0",
  "versions": [
    "0.2.0",
    "0.1.0"
  ],
  "range": {
    "version": "0.2.0",
    "from": null,
    "to": null
  },
  "summary": {
    "total": 3,
    "added": 0,
    "changed": 3,
    "removed": 0,
    "breaking": 3
  },
  "changes": [
    {
      "version": "0.2.0",
      "from": "0.1.0",
      "type": "NpcRegistry",
      "member": "getOrCreate",
      "kind": "function",
      "change": "changed",
      "breaking": true,
      "before": "default NpcDefinition getOrCreate(String name)",
      "after": "default NpcDefinition getOrCreate(String name, NpcEditAccess access)",
      "url": "https://api.lunacore.dev/LunaNPC#type/NpcRegistry"
    }
  ]
}

Changelog

GET

/api/v1/changelog

Tier 1 · Guest

Changelog entries across every product, newest first. Each change has a tag: Added, Changed, Fixed or Removed.

QueryAccepts
productproduct slug, e.g. LunaNPC or LunaCore (the website)
kindrelease (entries with a version) | note (everything else)
sinceISO 8601 timestamp; only entries published after it
beforeentry id; only older entries, for paging
limit1-100, default 20

Request

curl "https://lunacore.dev/api/v1/changelog?product=LunaCore&limit=1"

Response, shortened

{
  "count": 1,
  "next_before": 6,
  "entries": [
    {
      "id": 6,
      "product": "LunaCore",
      "title": "LunaCore - Website redesign",
      "version": null,
      "changes": [
        {
          "tag": "Added",
          "text": "Global issue tracker across all projects"
        }
      ],
      "url": "https://lunacore.dev/changelog?product=LunaCore#entry-6"
    }
  ]
}
GET

/api/v1/changelog/{id}

Tier 1 · Guest

One changelog entry.

Search

Reports

GET

/api/v1/reports

Tier 1 · Guest

List reports, most recent activity first.

QueryAccepts
statusopen (default) | resolved | all
typeissue | suggestion
productproduct slug, e.g. LunaNPC
sinceISO 8601 timestamp; only reports created, resolved or commented on after it
limit1-200, default 50

Request

curl "https://lunacore.dev/api/v1/reports?status=open&limit=1"

Response, shortened

{
  "generated_at": "2026-10-22T09:00:00.000Z",
  "count": 1,
  "reports": [
    {
      "id": 3,
      "product": "LunaNPC",
      "type": "suggestion",
      "title": "Allow Pillagers to Equip Weapons",
      "priority": "low",
      "status": "open",
      "comment_count": 1,
      "awaiting_staff": false,
      "url": "https://lunacore.dev/LunaNPC/reports/3",
      "api": "https://lunacore.dev/api/v1/reports/3"
    }
  ]
}
GET

/api/v1/reports/{id}

Tier 1 · Guest

One report with its full description, images, comment thread and events (resolved, reopened, type, priority and title changes), each in time order.

Request

curl https://lunacore.dev/api/v1/reports/2

Response, shortened

{
  "id": 2,
  "product": "LunaNPC",
  "type": "issue",
  "title": "Fast ground NPC circles while pursuing a retreating player",
  "description": "When a ground NPC has very high movement speed…",
  "priority": "medium",
  "status": "open",
  "images": [],
  "comments": [
    {
      "id": "1",
      "author": "System",
      "username": "system",
      "staff": true,
      "body": "Thanks for reporting!"
    }
  ]
}
POST

/api/v1/reports/{id}/comments

Tier 2 · Registered

Reply on a report. Staff, or the report's author. The author is notified.

JSON bodyAccepts
bodystring, the reply text

Request

curl -X POST https://lunacore.dev/api/v1/reports/2/comments \
  -H "Authorization: Bearer $LUNACORE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"body": "Fixed in 0.2.0, thanks!"}'

Response, shortened

{
  "id": "7",
  "created_at": "2026-10-22T09:05:00.000Z"
}
PATCH

/api/v1/reports/{id}

Tier 3 · Staff

Rename, re-prioritise, retype, close or reopen a report. Send only the fields to change. Closing notifies the author; with resolved_in it says which version fixes the bug or ships the suggestion.

JSON bodyAccepts
titlestring, 3-200 chars
prioritylow | medium | high | emergency
typeissue | suggestion
statusopen | resolved
resolved_in{ product, version } of an existing version, released or planned; null clears it. Needs status resolved

Request

curl -X PATCH https://lunacore.dev/api/v1/reports/2 \
  -H "Authorization: Bearer $LUNACORE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status": "resolved", "resolved_in": {"product": "LunaNPC", "version": "2.0.0"}}'

Response, shortened

{
  "id": 2,
  "title": "Fast ground NPC circles while pursuing a retreating player",
  "priority": "medium",
  "status": "resolved",
  "resolved_in": {
    "product": "LunaNPC",
    "version": "2.0.0",
    "released": false,
    "url": "https://lunacore.dev/LunaNPC#v2.0.0",
    "download": null
  },
  "comments": []
}

Account

GET

/api/v1/me

Tier 2 · Registered

Which account the request is acting as, and its tier.

Request

curl https://lunacore.dev/api/v1/me -H "Authorization: Bearer $LUNACORE_API_KEY"

Response, shortened

{
  "username": "raeydzone",
  "lunaid": 1,
  "role": "staff",
  "tier": 3,
  "via": "personal key"
}
GET

/api/v1/usage

Tier 1 · Guest

Your tier, request limits and what is left, plus your AI budget when signed in. Does not count against the limits. Not counted against your limits.

Request

curl https://lunacore.dev/api/v1/usage -H "Authorization: Bearer $LUNACORE_API_KEY"

Response, shortened

{
  "tier": {
    "level": 2,
    "name": "Registered"
  },
  "via": "personal key",
  "account": "someone",
  "requests": {
    "minute": {
      "used": 3,
      "limit": 50,
      "remaining": 47
    },
    "hour": {
      "used": 40,
      "limit": 500,
      "remaining": 460
    },
    "day": {
      "used": 90,
      "limit": 2500,
      "remaining": 2410
    }
  },
  "ai_budget": {
    "tier": "member",
    "allowed": true,
    "day": {
      "spent_usd": 0.004,
      "limit_usd": 0.1
    },
    "week": {
      "spent_usd": 0.012,
      "limit_usd": 0.5
    }
  }
}