Official Node.js SDK for the Noctaly public API.
📖 API Reference · npm · GitHub
npm install @noctaly/sdk
# or
pnpm add @noctaly/sdk
# or
yarn add @noctaly/sdkRequires Node.js ≥ 18
import { NoctalyClient } from "@noctaly/sdk";
const noctaly = new NoctalyClient({ apiKey: "your-api-key" });
const user = noctaly.guilds("GUILD_ID").users("USER_ID");
// Get leveling profile
const { data: profile } = await user.getLevelProfile();
console.log(`Level ${profile.level} - ${profile.totalXp} total XP`);
// Give 500 XP (with server multipliers applied)
const { data: xpResult } = await user.updateXP({ xp: 500, multiply: true });
console.log(`XP given: ${xpResult.xp}`);| Option | Type | Default | Description |
|---|---|---|---|
apiKey |
string |
- | Your Noctaly API key. |
baseURL |
string |
https://noctaly.com/api/v1 |
Override the base URL. Useful for staging environments. |
retry |
boolean |
true |
Automatically retry requests that receive a 429 rate-limit response. |
maxRetries |
number |
3 |
Maximum number of automatic retries before the error is thrown. |
timeout |
number |
30000 |
Request timeout in milliseconds. Set to 0 to disable. |
When retry is true, the SDK waits the retryAfter duration from the API response before each retry, capped at 60 seconds.
Returns a GuildResource scoped to the given Discord guild.
Returns a UserResource scoped to the given guild member.
All methods return Promise<NoctalyResponse<T>>:
interface NoctalyResponse<T> {
data: T;
rateLimit?: RateLimitInfo;
}GET /guilds/{guildID}/users/{userID}/level-profile
const { data } = await user.getLevelProfile();
// data: LevelProfile| Field | Type | Description |
|---|---|---|
level |
number |
Current level. |
xp |
number |
XP accumulated in the current level. |
neededXp |
number |
Remaining XP needed to reach next level. |
totalXp |
number |
Total XP across all levels. |
messages |
number |
Total messages sent. |
voiceMinutes |
number |
Total minutes spent in voice channels. |
reactions |
number |
Total reactions added. |
GET /guilds/{guildID}/users/{userID}/eco-profile
Only money is returned by default. Pass options to include optional relations.
The return type is narrowed automatically based on the options you pass.
// Basic
const { data } = await user.getEcoProfile();
// data: { money: number }
// With all relations
const { data } = await user.getEcoProfile({ items: true, chests: true, dailyStreak: true });
// data: { money: number, items: UserItem[], chests: UserChest[], dailyStreak: DailyStreak | null }| Option | Type | Description |
|---|---|---|
items |
boolean |
Include the member's item inventory. |
chests |
boolean |
Include the member's chest inventory. |
dailyStreak |
boolean |
Include the member's daily streak. |
POST /guilds/{guildID}/users/{userID}/xp
const { data } = await user.updateXP({ xp: 1000, multiply: true });
// data: { xp: number } ← actual XP given after multipliers| Field | Type | Required | Description |
|---|---|---|---|
xp |
number |
✓ | XP to give (positive) or remove (negative). Range: ±100 M. |
multiply |
boolean |
Apply server and booster-role multipliers. Default: false. |
POST /guilds/{guildID}/users/{userID}/level
const { data } = await user.updateLevel({ level: 5 });
// data: { level: number }| Field | Type | Required | Description |
|---|---|---|---|
level |
number |
✓ | Levels to give (positive) or remove (negative). ±1 000. |
POST /guilds/{guildID}/users/{userID}/money
const { data } = await user.updateMoney({ money: 5000, multiply: true });
// data: { money: number }| Field | Type | Required | Description |
|---|---|---|---|
money |
number |
✓ | Money to give (positive) or remove (negative). Range: ±1 000 M. |
multiply |
boolean |
Apply server and booster-role multipliers. Default: false. |
POST /guilds/{guildID}/users/{userID}/items
const { data } = await user.items.add({ itemID: "uuid", quantity: 3 });
// data: { quantity: number } ← new total quantity in inventoryDELETE /guilds/{guildID}/users/{userID}/items
const { data } = await user.items.remove({ itemID: "uuid", quantity: 1 });
// data: { quantity: number } ← remaining quantity (0 if fully removed)PUT /guilds/{guildID}/users/{userID}/items
const { data } = await user.items.set({ itemID: "uuid", quantity: 10 });
// Passing 0 removes the item entirely.
// data: { quantity: number }POST /guilds/{guildID}/users/{userID}/items/{itemID}/use
Triggers the item's configured actions. Only works for CUSTOM items with at least one action. The item is consumed unless it has the KEEP_AFTER_USE flag.
const { data } = await user.items.use("item-uuid", { quantity: 2 });
// data: UseItemResultUseItemResult fields:
| Field | Type | Description |
|---|---|---|
xp |
number |
Total XP awarded (negative if removed). |
money |
number |
Total money awarded (negative if removed). |
addedRoles |
string[] |
Role IDs added to the member. |
removedRoles |
string[] |
Role IDs removed from the member. |
addedItems |
Record<string, number> |
Map of item UUID → quantity added. |
itemsRemoved |
Record<string, number> |
Map of item UUID → quantity removed. |
addedChests |
Record<string, number> |
Map of chest UUID → quantity added. |
chestsRemoved |
Record<string, number> |
Map of chest UUID → quantity removed. |
rolesDuration |
Record<string, number> |
Map of role ID → Unix timestamp when the temporary role expires. |
POST /guilds/{guildID}/users/{userID}/chests
const { data } = await user.chests.add({ chestID: "uuid", quantity: 2 });
// data: { quantity: number }DELETE /guilds/{guildID}/users/{userID}/chests
const { data } = await user.chests.remove({ chestID: "uuid", quantity: 1 });
// data: { quantity: number }PUT /guilds/{guildID}/users/{userID}/chests
const { data } = await user.chests.set({ chestID: "uuid", quantity: 0 });
// Passing 0 removes the chest entirely.
// data: { quantity: number }GET /guilds/{guildID}/items
const { data } = await guild.items.list();
// data: { items: Item[] }Item fields:
| Field | Type | Description |
|---|---|---|
id |
string |
UUID of the item. |
name |
string |
Display name. |
description |
string | null |
Optional description. |
type |
string |
Item type (e.g. "CUSTOM"). |
flags |
string[] |
Item flags (e.g. ["KEEP_AFTER_USE"]). |
emoji |
string | null |
Emoji string. |
emojiType |
"UNICODE" | "CUSTOM" | null |
Emoji type. |
iconURL |
string | null |
Custom icon URL. |
buyPrice |
number |
Buy price. |
sellPrice |
number |
Sell price. |
cooldown |
number |
Cooldown between uses in seconds (0 = none). |
durability |
number | null |
Full durability per item (0 = no durability). |
quantity |
number | null |
Quantity in the shop (null = unlimited). |
quantityLimit |
number | null |
Per-member quantity limit (null = unlimited). |
GET /guilds/{guildID}/chests
const { data } = await guild.chests.list();
// data: { chests: Chest[] }Chest fields:
| Field | Type | Description |
|---|---|---|
id |
string |
UUID of the chest. |
name |
string |
Display name. |
primaryColor |
string |
Primary hex color (without #). |
secondaryColor |
string |
Secondary hex color (without #). |
iconURL |
string | null |
Custom icon URL. |
flags |
string[] |
Chest flags (e.g. ["ANIMATED"]). |
buyPrice |
number |
Buy price. |
sellPrice |
number |
Sell price. |
itemDrawCount |
number |
Number of items drawn when the chest is opened. |
quantity |
number | null |
Quantity in the shop (null = unlimited). |
quantityLimit |
number | null |
Per-member quantity limit (null = unlimited). |
All methods throw a NoctalyError on non-2xx responses.
import { NoctalyError } from "@noctaly/sdk";
try {
await user.updateXP({ xp: 500 });
} catch (err) {
if (err instanceof NoctalyError) {
console.error(err.code); // "rate_limited" | "not_found" | …
console.error(err.status); // 429 | 404 | …
console.error(err.message); // Human-readable message
console.error(err.retryAfter); // seconds (rate_limited only)
console.error(err.global); // boolean (rate_limited only)
console.error(err.details); // ValidationIssue[] (validation_error only)
}
}| Code | Status | Description |
|---|---|---|
unauthenticated |
401 | No API key provided. |
unauthorized |
403 | API key doesn't have access to this guild. |
rate_limited |
429 | Rate limit exceeded. Check retryAfter and global. |
not_found |
404 | The requested member or resource was not found. |
module_disabled |
400 | The relevant module is disabled in this guild. |
validation_error |
400 | Invalid request body. Check details for field errors. |
unexpected_error |
500 | Server-side error. |
Every successful response exposes the endpoint's rate-limit info:
const { data, rateLimit } = await user.getLevelProfile();
if (rateLimit) {
console.log(`${rateLimit.remaining}/${rateLimit.limit} remaining`);
console.log(`Resets at ${new Date(rateLimit.reset * 1000).toISOString()}`);
console.log(`Resets in ${rateLimit.resetAfter}s`);
}| Field | Type | Description |
|---|---|---|
limit |
number |
Maximum requests allowed in the window. |
remaining |
number |
Requests remaining in the current window. |
reset |
number |
Unix timestamp (seconds) when the window resets. |
resetAfter |
number |
Seconds until the window resets. |
The global rate limit is 50 req/s across all routes. Exceeding it results in a NoctalyError with code: "rate_limited" and global: true.
When retry: true (the default), the SDK automatically handles 429 responses by waiting retryAfter seconds and retrying up to maxRetries times before throwing.