Overview
Your Nocturne AI access — quota, usage, and the models your account can call.
API keys
Scoped noct_… keys for calling the gateway from your own apps. Keep them server-side.
Models
One endpoint routes to every model your account can use. Click an id to copy it.
Playground
Test a prompt against POST /api/ai with a real key — nothing here is simulated.
Logs
Recent requests to POST /api/ai across all your keys.
Status
Live health of the underlying AI provider — pinged every 10 minutes, 90 days of history.
Documentation
Call Nocturne AI — and the school/content-filter checker — from your own apps. Authenticated, quota-fair, OpenAI-style JSON.
Authentication
The POST /api/ai endpoint accepts either of two auth methods:
- API key (recommended). Send
Authorization: Bearer noct_…(or headerX-API-Key: noct_…). Create + manage keys on the API keys tab. Keep keys server-side — never ship them in client code. - Account credentials. Include
account+passwordin the JSON body. Convenient for quick tests; prefer keys for anything real.
The account endpoints (/api/ai/usage, /credits, /config) are session-based — they read your signed-in cookie and power the Overview tab. For programmatic quota tracking, read the quota object returned by every /api/ai call.
Chat / completion — POST /api/ai
Send a prompt or a message list, get a completion. Every call counts against your account's daily token quota and is rate-limited to 60 requests/minute.
Request body
| Field | Type | Description |
|---|---|---|
model | string required | A model id you have access to. Your ids are listed on the Models tab (or via GET /api/ai/config). |
prompt | string | A single user prompt. Use this or messages. |
messages | array | [{ "role": "user", "content": "…" }] — 1–50 items. content is a string, or an array of parts to send images (see below). Roles: system, user, assistant. |
system | string optional | System prompt (prepended when using prompt). |
temperature | number optional | 0–2. Sampling temperature. |
top_p | number optional | 0–1. Nucleus sampling. |
max_tokens | number optional | Cap on reply length (also accepts max_completion_tokens). Auto-clamped to your remaining quota. |
level | string optional | Thinking level — the same control as the AI page: off · normal · extended. Higher = the model reasons more before answering. |
reasoning_effort | string optional | Fine-grained effort for reasoning models: low · medium · high (level maps to this for you). |
stream | boolean optional | If true, returns Server-Sent Events instead of one JSON body. |
Example — API key
curl -s https://nocturne.lol/api/ai \
-H "Authorization: Bearer noct_your_api_key" \
-H "Content-Type: application/json" \
-d '{"model":"<model-id>","prompt":"Explain CSS grid in 2 sentences."}'
"level":"extended" for deeper reasoning before the answer (same as the AI page's Extended mode); "off" is fastest, "normal" is the default. Applies to reasoning-capable models.Example — multi-turn + account auth
curl -s https://nocturne.lol/api/ai \
-H "Content-Type: application/json" \
-d '{
"model":"<model-id>",
"account":"your-username","password":"YOUR_PASSWORD",
"system":"You are concise.",
"messages":[{"role":"user","content":"Name three primary colors."}],
"temperature":0.5,"max_tokens":200
}'
Response
{
"ok": true,
"model": "<model-id>",
"response": "Red, green, and blue.",
"usage": { "inputTokens": 18, "outputTokens": 7, "totalTokens": 25 },
"quota": { "used": 25, "cap": 50000, "remaining": 49975 }
}
Reasoning models may also include a reasoning field. Owner accounts get "quota": { "unlimited": true }.
Streaming ("stream": true)
Responds with text/event-stream. Each event is a data: line of JSON:
# incremental text
data: {"type":"delta","text":"Red"}
data: {"type":"delta","text":", green"}
# final totals, then end
data: {"type":"done","usage":{...},"quota":{...}}
data: [DONE]
Images (vision)
Send images to a vision model (marked 👁 on the Models tab). Use an array content with image_url parts (OpenAI format); the URL can be a public https link or a base64 data: URL.
curl -s https://nocturne.lol/api/ai \
-H "Authorization: Bearer noct_your_api_key" -H "Content-Type: application/json" \
-d '{
"model":"<vision-model-id>",
"messages":[{"role":"user","content":[
{"type":"text","text":"What is in this image?"},
{"type":"image_url","image_url":{"url":"https://example.com/cat.jpg"}}
]}]
}'
Up to 8 images per request, ≤6 MB each. A data URL looks like data:image/png;base64,iVBORw0KGgo…. Non-vision models reject images with a 400.
Errors
| Code | Meaning |
|---|---|
400 | Bad request — missing/invalid model, no prompt/messages, or a message too long. |
401 | Missing or invalid auth (bad key / wrong account+password). |
403 | Account suspended. |
429 | Rate limit, or daily quota reached — body includes your quota. |
502 | Upstream AI temporarily unavailable — retry shortly. |
used / cap / remaining ride along on every response and are shown live on the Overview tab.SDK examples
No official client library yet — the API is plain JSON over HTTP, so any HTTP client works. Two common ones:
JavaScript (fetch)
// non-streaming
const r = await fetch("https://nocturne.lol/api/ai", {
method: "POST",
headers: { Authorization: "Bearer noct_your_api_key", "Content-Type": "application/json" },
body: JSON.stringify({ model: "<model-id>", prompt: "Say hi in 5 words." })
});
const data = await r.json();
console.log(data.response);
// streaming
const r = await fetch("https://nocturne.lol/api/ai", {
method: "POST",
headers: { Authorization: "Bearer noct_your_api_key", "Content-Type": "application/json" },
body: JSON.stringify({ model: "<model-id>", prompt: "Count to 5.", stream: true })
});
const reader = r.body.getReader();
const decoder = new TextDecoder();
let buf = "";
while (true) {
const { value, done } = await reader.read();
if (done) break;
buf += decoder.decode(value, { stream: true });
for (const line of buf.split("\n")) {
if (!line.startsWith("data:") || line.includes("[DONE]")) continue;
const evt = JSON.parse(line.slice(5));
if (evt.type === "delta") process.stdout.write(evt.text);
}
buf = "";
}
Python (requests)
import requests
r = requests.post(
"https://nocturne.lol/api/ai",
headers={"Authorization": "Bearer noct_your_api_key"},
json={"model": "<model-id>", "prompt": "Say hi in 5 words."},
)
print(r.json()["response"])
# streaming
import json, requests
with requests.post(
"https://nocturne.lol/api/ai",
headers={"Authorization": "Bearer noct_your_api_key"},
json={"model": "<model-id>", "prompt": "Count to 5.", "stream": True},
stream=True,
) as r:
for line in r.iter_lines(decode_unicode=True):
if not line or not line.startswith("data:") or "[DONE]" in line:
continue
evt = json.loads(line[5:])
if evt.get("type") == "delta":
print(evt["text"], end="", flush=True)
Best practices
stream:true gets text to your UI as it's generated instead of one blocking wait.quota object. Every response carries your live used/cap/remaining — check it instead of polling /api/ai/credits on every call.429 — retry after a short delay rather than immediately hammering the endpoint.GET /api/ai/config doesn't change often — refetch on an interval (e.g. hourly), not per request.Account & usage endpoints
Session-authenticated (your signed-in cookie). These power the Overview and Logs tabs.
GET /api/ai/usage
{ "authenticated": true, "weekTokens": 12840, "weekCost": 0.04, "allTokens": 91120, "allCost": 0.31 }
GET /api/ai/credits
{ "authenticated": true, "dollars": 2.84,
"daily": { "used": 1320, "cap": 50000, "resetAt": 1750000000000 } }
GET /api/ai/config
Lists the models your account can use (id, label, pricing, flags) plus your credit balance — the source for the Models tab.
GET /api/ai/logs · /api/ai/usage/by-model
/logs returns your last 50 requests plus today's rollup (the Logs tab). /usage/by-model returns a 30-day per-model breakdown (tokens, requests, avg latency — the hover stats on Overview).
POST /api/ai/apikeys · GET /api/ai/apikeys · DELETE /api/ai/apikeys/:id
Create, list, and revoke your own keys programmatically — the same calls the API keys tab makes. POST accepts { "label": "my-app" } and returns the raw key. Unlike most providers, you can also re-fetch it later with GET /api/ai/apikeys/:id/reveal — useful if you lose it, though a fresh key is just as easy to create.
Link Checker API
Check whether a URL is blocked by common school/content filters (GoGuardian, Securly, Lightspeed, and more). This has its own dedicated docs now — see the Link Checker tab for the full reference (auth, rate limits, all 16 filters, examples, error codes). Note: the AI assistant can't help with Link Checker questions — the docs there are self-contained.
FAQ
Which model should I use?
Check the Models tab — it's grouped by provider and shows live pricing + vision support for exactly what your account can call. For most text tasks, start with a cheaper "mini"-tier model and only reach for a pricier one if quality falls short.
Is there a free tier?
Every signed-in account gets a daily token quota at no cost (see your cap on the Overview tab). It resets every 24 hours.
Can I call this straight from browser JS?
Yes — /api/ai is CORS-enabled for all origins. But never embed a real key in client-side code; anyone can read it from page source. For a public-facing app, proxy the call through your own backend instead.
Do you store my prompts?
No. The public API logs only aggregate metadata for your own dashboards — model, status, latency, and token counts (that's what powers the Logs tab). Prompt and response text is never persisted by this endpoint.
What happens if I run out of quota mid-stream?
The request is rejected with 429 before any tokens are generated if you're already over budget — you're never billed for a partial response you can't use.
Link Checker API
Check whether a URL is blocked by common school/content filters. A separate, self-contained API — not part of the AI gateway, and not something the AI assistant can help with.
noct_… AI keys. The AI assistant is intentionally unable to answer questions about it; everything you need is on this page.Authentication & rate limits
No account or sign-in required. Every request is rate-limited per IP address:
| Tier | Limit | How |
|---|---|---|
| Anonymous | 30 requests / minute | Default — no header needed. |
| With a key | 300 requests / minute | Send X-API-Key: your-linkcheck-key (or ?key=). These are provisioned by the site owner, separate from your noct_… AI key — email hello@midtsy.xyz for one. |
429 with a Retry-After: 60 header.Check a URL — GET / POST /api/v1/linkcheck
Runs the URL against every vendor's own public classification check. Your URL is only ever sent to each vendor as data — Nocturne never fetches, loads, or renders it. There's no SSRF surface on this endpoint.
Parameters
| Field | Type | Description |
|---|---|---|
url | string required | Query param or JSON body field. A bare domain (discord.com) or a full URL. Max 2048 characters. |
filters | string / array optional | all (default) to check every filter, or a comma-separated list / JSON array of filter ids (see below). |
Example — GET
curl -s "https://nocturne.lol/api/v1/linkcheck?url=discord.com&filters=all"
Example — POST
curl -s https://nocturne.lol/api/v1/linkcheck \
-H "Content-Type: application/json" \
-d '{"url":"discord.com","filters":["securly","goguardian"]}'
Example — with a key (higher limit)
curl -s "https://nocturne.lol/api/v1/linkcheck?url=discord.com" \
-H "X-API-Key: your-linkcheck-key"
Response
{
"url": "http://discord.com/",
"checkedAt": "2026-07-20T12:00:00.000Z",
"summary": { "total": 16, "blocked": 3, "allowed": 12, "errors": 1 },
"results": [
{ "id": "securly", "label": "Securly", "category": "Social Media", "blocked": true, "ms": 240 },
{ "id": "fortiguard", "label": "FortiGuard", "category": "Technology", "blocked": false, "ms": 180 }
]
}
Each result has blocked: true/false/null (null = the vendor's check errored or timed out — see error), category as reported by that vendor, and ms = how long that single check took. Each vendor is checked concurrently with a 12s timeout, so one slow vendor never blocks the rest.
Available filters — GET /api/v1/filters
| id | Label |
|---|---|
aristotle | Aristotle |
barracuda | Barracuda |
blocksi | Blocksi |
cisco | Cisco Umbrella |
deledao | Deledao |
fortiguard | FortiGuard |
goguardian | GoGuardian |
iboss | iBoss |
lanschool | LanSchool |
lightspeed | Lightspeed |
linewize | Linewize |
paloalto | Palo Alto |
qustodio | Qustodio |
securly | Securly |
senso | Senso |
sophos | Sophos |
Errors
| Code | Meaning |
|---|---|
400 | Missing url, URL too long (>2048 chars), invalid URL, or no valid filters selected. |
429 | Rate limit exceeded for your tier — see Retry-After. |
JavaScript example
const r = await fetch("https://nocturne.lol/api/v1/linkcheck?url=discord.com&filters=all");
const data = await r.json();
console.log(data.summary); // { total: 16, blocked: 3, allowed: 12, errors: 1 }
Python example
import requests
r = requests.get("https://nocturne.lol/api/v1/linkcheck", params={"url": "discord.com", "filters": "all"})
print(r.json()["summary"])