erlc-v2
v1.0.1
Published
Premium, lightweight JavaScript wrapper for the ER:LC API v2.
Maintainers
Readme
erlc-v2
JavaScript client for the ER:LC API v2.
Built for Node 18+.
Stable Release
This wrapper is now on a stable release track (1.0.0+).
Responsibility and API Safety
You are responsible for how you use this wrapper and API key(s). If you use it recklessly (for example, aggressive request spam, ignoring rate limits, or abusive automation) and get rate-limited, blocked, or banned by PRC/Cloudflare, that is on you.
This project is provided as-is. Always monitor your integration and respect PRC API rules and headers.
Install
npm install erlc-v2Quick Start (CommonJS)
const { Client } = require("erlc-v2");
const client = new Client({
serverKey: "YOUR_SERVER_KEY",
// globalKey: "YOUR_GLOBAL_KEY", // optional
});
client.on("disconnect", ({ reason, error }) => {
console.error("Disconnected:", reason, error?.message);
});
async function main() {
const snapshot = await client.server.fetch({
players: true,
staff: true,
queue: true,
});
console.log("Server:", snapshot.name);
console.log("Players:", `${snapshot.currentPlayers}/${snapshot.maxPlayers}`);
console.log("Players payload:", snapshot.players.length);
}
main()
.catch(console.error)
.finally(() => client.destroy());Quick Start (ESM)
import { Client } from "erlc-v2";
const client = new Client({
serverKey: "YOUR_SERVER_KEY",
});Options
new Client({
serverKey: string, // required
globalKey?: string, // optional
logging?: boolean, // default: false
logger?: { info, warn, error, debug },
cache?: {
enabled?: boolean, // default: true
ttlMs?: number, // default: 1500
maxSize?: number, // default: 500
provider?: "memory" | "redis", // default: auto (redis if redis config is present)
redisUrl?: string, // optional (requires `npm i redis`)
redisPrefix?: string, // default: "erlc-v2:cache"
redisClient?: object, // optional pre-configured Redis client
},
rateLimit?: {
enabled?: boolean, // default: true
strictSerial?: boolean, // default: true (global one-at-a-time queue)
bucketLimit?: number, // default: 1
totalLimit?: number, // default: 1
unauthLimit?: number, // default: 3
},
polling?: {
enabled?: boolean, // default: true
intervalMs?: number, // default: 2500 (min enforced: 250)
bypassCache?: boolean, // default: true
},
});Legacy aliases (perBucketConcurrency, globalConcurrency, unauthorizedThreshold) are still accepted.
Redis Cache (Optional)
You can use Redis instead of in-memory cache by passing either cache.redisUrl or cache.redisClient.
If you use redisUrl, install the Redis client package:
npm i redisExample with connection URL:
const { Client } = require("erlc-v2");
const client = new Client({
serverKey: "YOUR_SERVER_KEY",
cache: {
provider: "redis",
redisUrl: "redis://localhost:6379",
redisPrefix: "myapp:erlc",
ttlMs: 2000,
},
});Example with your own Redis client instance:
const { createClient } = require("redis");
const { Client } = require("erlc-v2");
(async () => {
const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();
const client = new Client({
serverKey: "YOUR_SERVER_KEY",
cache: {
redisClient: redis,
redisPrefix: "myapp:erlc",
},
});
})();API
Core:
await client.server.fetch(flags, requestOptions?)client.destroy()client.cache.clear()
Convenience methods:
await client.players.list()await client.map.render(options?)await client.map.renderUser(userId, options?)await client.staff.list()await client.logs.kills()await client.logs.joins()await client.logs.commands()await client.logs.modCalls()await client.vehicles.list()await client.queue.get()await client.commands.execute(":h Hey everyone!")(v1 endpoint)
Fetch Flags
players->Playersstaff->StaffjoinLogs->JoinLogsqueue->QueuekillLogs->KillLogscommandLogs->CommandLogsmodCalls->ModCallsvehicles->Vehicles
Request Options
bypassCache?: booleancacheTtlMs?: numberdedupe?: boolean
Command Execution (v1)
client.commands.execute(command) sends a POST request to /v1/server/command.
Command execution is FIFO-queued client-side, so commands run one-at-a-time in order.
Blocked by client policy (request will be rejected before hitting the API):
:view:to:tocar:toatv:logs:mods:adminshelpers/:helpers:administrators:moderators:killlogs:kl:cmds:commands
Example:
await client.commands.execute(":h Hey everyone!");
await client.commands.execute(":log recentban He was trolling!");Map Rendering
Render an ER:LC map (3121x3121) with player markers that use Roblox avatars.
const result = await client.map.render();
// png buffer
console.log(result.buffer);
console.log(result.players.length);client.map.render() renders the full map with all players currently in the server.
Render an official season/type map preset:
const fallBlank = await client.map.render({
season: "fall",
type: "blank",
});
const fallPostals = await client.map.render({
season: "fall",
type: "postals",
});
const winterBlank = await client.map.render({
season: "winter", // alias: "snow"
type: "blank",
});
const winterPostals = await client.map.render({
season: "winter",
type: "postals",
});Use your own map image URL:
const customMap = await client.map.render({
mapUrl: "https://example.com/my-map.png", // mainly used in cases where we fail to add the most recent map when its released (sizing should be 3121x3121)
});Render only one player by Roblox user ID:
const single = await client.map.renderUser(123456789, {
season: "winter",
type: "postals",
});Options:
userId?: number | stringuserIds?: Array<number | string>players?: any[](use your own pre-fetched player payload)mapUrl?: string(custom map URL; if set, this overridesseason/type)season?: string("fall"or"winter"/"snow"for official presets)type?: string("blank"or"postals"for official presets)mapSeason?: string(alias ofseason)mapType?: string(alias oftype)coordinateBounds?: { minX, maxX, minY, maxY, invertY? }clampToMap?: boolean(default:true)robloxHeadshotSize?: string(default:"150x150")marker?: { outerRadius, innerRadius, tipLength, tipWidth, fillColor, shadow }
Map size is fixed to 3121x3121.
Result shape:
buffer(image/png)map({ url, season, type, width, height })players(rendered marker metadata)skipped(players skipped because coordinates were unavailable/invalid)requestedUserIds(IDs requested viauserId/userIds)unmatchedUserIds(requested IDs not found in current player payload)
Events
readyplayerJoinplayerLeavekillvehicleSpawnvehicleDespawnqueueUpdatestaffUpdatemodCallcommandLoglogCommand(only when command starts with:log)serverUpdateerrordisconnect({ reason, error })
Alias event names are also supported with client.on(...):
onReadyonJoinonLeaveonKillonVehicleSpawnonVehicleDespawnonQueueUpdateonStaffUpdateonModCallonCommandLogonLogCommandonServerUpdateonErroronDisconnect
Shortcut methods are available too:
client.onJoin((payload) => console.log("join", payload));
client.onLeave((payload) => console.log("leave", payload));
client.onVehicleSpawn((payload) => console.log("spawn", payload));
client.onLogCommand(({ command, parsed }) => {
console.log("raw command:", command.Command);
console.log("keyword:", parsed.keyword);
console.log("args:", parsed.args);
});logCommand / onLogCommand fires when a command starts with :log.
Events are deduped per poll cycle so the same log entry is not emitted repeatedly.
Rate Limits
Requests are automatically bucketed using API response headers:
X-RateLimit-BucketX-RateLimit-LimitX-RateLimit-RemainingX-RateLimit-Reset
On 429, the client blocks the affected bucket until retry time/reset.
By default, requests are strictly serialized (strictSerial: true) so this client does not send parallel requests.
This is intentionally conservative for anti-abuse/rate-limit safety.
Errors
The client normalizes errors into classes:
ERLCErrorERLCHttpErrorERLCAPIErrorRateLimitErrorKeyExpiredError(2002)KeyBannedError(2004)InvalidGlobalKeyError(2003)ServerOfflineError(3002)RestrictedError(9998)ModuleOutOfDateError(9999)
Terminal key errors (2002, 2004) trigger disconnect and stop polling.
Repeated 403 responses can also trigger disconnect (reason: "unauthorized").
Notes
- API base URL:
https://api.policeroleplay.community Server-Keyis required for requestsAuthorizationis optional (globalKey)
