> For the complete documentation index, see [llms.txt](https://noctaly-bot.gitbook.io/noctaly-api/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://noctaly-bot.gitbook.io/noctaly-api/libraries/javascript.md).

# JavaScript

## @noctaly/sdk

Official Node.js SDK for the [Noctaly](https://noctaly.com) public API.

📖 [**API Reference**](https://noctaly-bot.gitbook.io/noctaly-api) · [npm](https://www.npmjs.com/package/@noctaly/sdk) · [GitHub](https://github.com/Noctalycom/node-sdk)

### Installation

```bash
npm install @noctaly/sdk
# or
pnpm add @noctaly/sdk
# or
yarn add @noctaly/sdk
```

> **Requires Node.js ≥ 18**

### Quick Start

```ts
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}`);
```

***

### API Reference

#### `new NoctalyClient(options)`

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

***

#### `client.guilds(guildID)` → `GuildResource`

Returns a `GuildResource` scoped to the given Discord guild.

**`guild.users(userID)` → `UserResource`**

Returns a `UserResource` scoped to the given guild member.

All methods return `Promise<NoctalyResponse<T>>`:

```ts
interface NoctalyResponse<T> {
  data: T;
  rateLimit?: RateLimitInfo;
}
```

***

#### UserResource

**`user.getLevelProfile()`**

```
GET /guilds/{guildID}/users/{userID}/level-profile
```

```ts
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.                   |

***

**`user.getEcoProfile(options?)`**

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

```ts
// 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.    |

***

**`user.updateXP(body)`**

```
POST /guilds/{guildID}/users/{userID}/xp
```

```ts
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`. |

***

**`user.updateLevel(body)`**

```
POST /guilds/{guildID}/users/{userID}/level
```

```ts
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. |

***

**`user.updateMoney(body)`**

```
POST /guilds/{guildID}/users/{userID}/money
```

```ts
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`.    |

***

#### UserItemsResource - `user.items`

**`user.items.add(body)`**

```
POST /guilds/{guildID}/users/{userID}/items
```

```ts
const { data } = await user.items.add({ itemID: "uuid", quantity: 3 });
// data: { quantity: number }  ← new total quantity in inventory
```

**`user.items.remove(body)`**

```
DELETE /guilds/{guildID}/users/{userID}/items
```

```ts
const { data } = await user.items.remove({ itemID: "uuid", quantity: 1 });
// data: { quantity: number }  ← remaining quantity (0 if fully removed)
```

**`user.items.set(body)`**

```
PUT /guilds/{guildID}/users/{userID}/items
```

```ts
const { data } = await user.items.set({ itemID: "uuid", quantity: 10 });
// Passing 0 removes the item entirely.
// data: { quantity: number }
```

**`user.items.use(itemID, body?)`**

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

```ts
const { data } = await user.items.use("item-uuid", { quantity: 2 });
// data: UseItemResult
```

`UseItemResult` 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. |

***

#### UserChestsResource - `user.chests`

**`user.chests.add(body)`**

```
POST /guilds/{guildID}/users/{userID}/chests
```

```ts
const { data } = await user.chests.add({ chestID: "uuid", quantity: 2 });
// data: { quantity: number }
```

**`user.chests.remove(body)`**

```
DELETE /guilds/{guildID}/users/{userID}/chests
```

```ts
const { data } = await user.chests.remove({ chestID: "uuid", quantity: 1 });
// data: { quantity: number }
```

**`user.chests.set(body)`**

```
PUT /guilds/{guildID}/users/{userID}/chests
```

```ts
const { data } = await user.chests.set({ chestID: "uuid", quantity: 0 });
// Passing 0 removes the chest entirely.
// data: { quantity: number }
```

***

#### GuildItemsResource - `guild.items`

**`guild.items.list()`**

```
GET /guilds/{guildID}/items
```

```ts
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). |

***

#### GuildChestsResource - `guild.chests`

**`guild.chests.list()`**

```
GET /guilds/{guildID}/chests
```

```ts
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). |

***

### Error Handling

All methods throw a `NoctalyError` on non-2xx responses.

```ts
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.                                      |

***

### Rate Limits

Every successful response exposes the endpoint's rate-limit info:

```ts
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.
