npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

erlc-v2

v1.1.2

Published

Premium, lightweight JavaScript wrapper for the ER:LC API v2.

Readme

erlc-v2

JavaScript client for the ER:LC API v2.

Built for Node 18+.

Important Upgrade Notice

Update to the newest version as soon as possible:

npm install erlc-v2@latest

Older releases may still call the deprecated api.policeroleplay.community host. The ER:LC API is moving to https://api.erlc.gg, and requests to the old host may start failing on May 11, 2026.

New Features

  • client.commands.execute() now uses /v2/server/command
  • emergency calls are supported
  • vehicle lookup helpers are built in
  • you can start a small local API with api: { port: 3001 }
  • event webhooks are supported and signature-checked before they fire events

Stable Release

This wrapper is on a stable release track (1.0.0+).

Responsibility and API Safety

Use your keys like a normal person. If you spam requests, ignore rate limits, or build dumb abuse tools and PRC or Cloudflare blocks you, that is on you.

This project is provided as-is. Keep an eye on your own integration and follow the PRC API rules.

Install

npm install erlc-v2

Quick Start (CommonJS)

const { Client } = require("erlc-v2");

const client = new Client({
  serverKey: "YOUR_SERVER_KEY",
  polling: {
    enabled: true,
  },
});

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,
    vehicles: true,
    emergencyCalls: true,
  });

  console.log("Server:", snapshot.name);
  console.log("Players:", `${snapshot.currentPlayers}/${snapshot.maxPlayers}`);
  console.log("Vehicles:", snapshot.vehicles.length);
  console.log("Emergency calls:", snapshot.emergencyCalls.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
    redisUrl?: string,
    redisPrefix?: string, // default: "erlc-v2:cache"
    redisClient?: object,
  },
  rateLimit?: {
    enabled?: boolean, // default: true
    strictSerial?: boolean, // default: true
    bucketLimit?: number, // default: 1
    totalLimit?: number, // default: 1
    unauthLimit?: number, // default: 3
  },
  polling?: {
    enabled?: boolean, // default: true
    intervalMs?: number, // default: 2500
    bypassCache?: boolean, // default: true
  },
  api?: {
    enabled?: boolean, // default: false unless port is set
    host?: string, // default: "127.0.0.1"
    port?: number, // required if you want the local API server
    path?: string, // default: "/erlc"
    webhookPath?: string, // default: `${path}/events`
    publicUrl?: string, // optional, used to build webhookUrl in client.api.info()
    token?: string, // optional bearer token for built-in routes
    logRequests?: boolean, // default: true, logs route/webhook hits to the console
  },
});

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 redis

Example 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?)
  • await client.commands.execute(command)
  • 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.logs.emergencyCalls()
  • await client.vehicles.list()
  • await client.vehicles.search(filters)
  • await client.vehicles.findByPlate(plate)
  • await client.vehicles.findByOwner(owner)
  • await client.vehicles.findOne(filters)
  • await client.queue.get()

Fetch Flags

  • players -> Players
  • staff -> Staff
  • joinLogs -> JoinLogs
  • queue -> Queue
  • killLogs -> KillLogs
  • commandLogs -> CommandLogs
  • modCalls -> ModCalls
  • emergencyCalls -> EmergencyCalls
  • vehicles -> Vehicles

Request Options

  • bypassCache?: boolean
  • cacheTtlMs?: number
  • dedupe?: boolean

Vehicle Search Helpers

Find one exact plate:

const car = await client.vehicles.findByPlate("LINCOLN7");

if (car) {
  console.log(car.Owner, car.Name, car.Plate);
}

Search across plate, owner, name, color, and texture:

const matches = await client.vehicles.search({
  query: "lincoln",
});

const ownerCars = await client.vehicles.findByOwner("lando");

const blackTahoes = await client.vehicles.search({
  name: "tahoe",
  color: "black",
});

Exact matching is supported too:

const exact = await client.vehicles.findOne({
  plate: "A12BCD",
  owner: "SomePlayer",
  exact: true,
});

Emergency Calls

const calls = await client.logs.emergencyCalls();

for (const call of calls) {
  console.log(call.CallNumber, call.Team, call.Description);
}

client.on("emergencyCall", ({ emergencyCall }) => {
  console.log("New emergency call:", emergencyCall.Description);
});

Command Execution

client.commands.execute(command) sends a POST request to /v2/server/command. Command execution is FIFO-queued client-side, so commands run one-at-a-time in order.

Blocked by client policy:

  • :view
  • :to
  • :tocar
  • :toatv
  • :logs
  • :mods
  • :admins
  • helpers / :helpers
  • :administrators
  • :moderators
  • :killlogs
  • :kl
  • :cmds
  • :commands

Example:

const result = await client.commands.execute(":h Hey everyone!");
console.log(result.message);

Built-in Local API Server

If you want the wrapper to expose a small HTTP server, it can do that too.

const client = new Client({
  serverKey: process.env.ERLC_SERVER_KEY,
  api: {
    port: 3001,
    host: "127.0.0.1",
    path: "/erlc",
    publicUrl: "https://hooks.example.com",
    token: process.env.ERLC_LOCAL_API_TOKEN,
  },
});

client.api.info();

If api.port is set, the local API auto-starts with the client. You can also call await client.api.start() yourself.

By default it logs incoming route hits and verified webhook payloads to the console.

If you want to react to ER:LC webhooks in your own code, use:

  • client.onWebhook(...)
  • client.onWebhookEmergencyCall(...)

Those only fire after the webhook signature checks out.

Built-in routes:

  • GET /erlc
  • GET /erlc/health
  • GET /erlc/server
  • GET /erlc/players
  • GET /erlc/vehicles
  • GET /erlc/vehicles/:plate
  • GET /erlc/emergency-calls
  • POST /erlc/command
  • POST /erlc/events

/erlc/vehicles accepts query params like search, plate, owner, name, color, texture, exact, and limit.

If you set api.token, send either:

  • Authorization: Bearer YOUR_TOKEN
  • X-API-Token: YOUR_TOKEN

Event Webhook Support

The built-in API can take ER:LC event webhooks and verify the signatures for you.

const client = new Client({
  serverKey: process.env.ERLC_SERVER_KEY,
  api: {
    port: 3001,
    publicUrl: "https://hooks.example.com",
  },
});

client.onWebhook((payload) => {
  console.log("Webhook type:", payload.type);
  console.log("Event name:", payload.event);
  console.log("Command:", payload.command);
  console.log("Args:", payload.args);
  console.log("Origin:", payload.origin);
});

client.onWebhookEmergencyCall((payload) => {
  console.log("Event:", payload.event);
  console.log("Origin:", payload.origin);
  console.log("Data:", payload.data);
  // react to emergency calls here
});

Useful flattened webhook fields:

  • payload.type
  • payload.event
  • payload.origin
  • payload.server
  • payload.eventTimestamp
  • payload.data
  • payload.command
  • payload.args
  • payload.argument
  • payload.entry
  • payload.events

This webhook is for custom ; commands and emergency calls. It is not for normal : commands from the command endpoint.

For custom ; commands, payload.command, payload.args, and payload.origin are usually the fields you want.

If your public URL is https://hooks.example.com and your API path is the default, set this in your ER:LC server settings:

https://hooks.example.com/erlc/events

If you want the longer request shape with type information, use:

https://hooks.example.com/erlc/events?long=true

Domain, Hosting, and Reverse Proxy Notes

The event webhook has to hit a public HTTPS URL. A local port by itself is not enough.

Important:

  • Most Discord bot hosts are bad for this because they do not let you expose your own API cleanly.
  • If your host does not allow inbound HTTP traffic, PRC will never reach your webhook.
  • You need something public in front of your wrapper.

Common setups:

  • Buy a domain and point it at a VPS.
  • Run the wrapper on a VPS and put NGINX or Caddy in front of it.
  • Run it somewhere private and use Cloudflare Tunnel.

Common places people use for domains:

  • Cloudflare Registrar: https://www.cloudflare.com/products/registrar/
  • Namecheap: https://www.namecheap.com/
  • Porkbun: https://porkbun.com/

Common places people use for public hosting or a VPS:

  • DigitalOcean: https://www.digitalocean.com/
  • Hetzner: https://www.hetzner.com/
  • Railway: https://railway.com/
  • Render: https://render.com/
  • Fly.io: https://fly.io/

Common reverse proxy or edge options:

  • NGINX: https://nginx.org/
  • Caddy: https://caddyserver.com/
  • Cloudflare Tunnel: https://www.cloudflare.com/products/tunnel/

Those are just examples. Use whatever actually gives you inbound HTTPS and a process you control.

Map Rendering

Render an ER:LC map (3121x3121) with player markers that use Roblox avatars.

const result = await client.map.render();

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 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",
});

Render only one player by Roblox user ID:

const single = await client.map.renderUser(123456789, {
  season: "winter",
  type: "postals",
});

Options:

  • userId?: number | string
  • userIds?: Array<number | string>
  • players?: any[]
  • mapUrl?: string
  • season?: string
  • type?: string
  • mapSeason?: string
  • mapType?: string
  • coordinateBounds?: { minX, maxX, minY, maxY, invertY? }
  • clampToMap?: boolean
  • robloxHeadshotSize?: string
  • 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
  • skipped
  • requestedUserIds
  • unmatchedUserIds

Events

  • ready
  • playerJoin
  • playerLeave
  • kill
  • vehicleSpawn
  • vehicleDespawn
  • queueUpdate
  • staffUpdate
  • modCall
  • emergencyCall
  • commandLog
  • logCommand
  • serverUpdate
  • webhook
  • webhookEmergencyCall
  • error
  • disconnect

Alias event names are also supported with client.on(...):

  • onReady
  • onJoin
  • onLeave
  • onKill
  • onVehicleSpawn
  • onVehicleDespawn
  • onQueueUpdate
  • onStaffUpdate
  • onModCall
  • onEmergencyCall
  • onCommandLog
  • onLogCommand
  • onServerUpdate
  • onApiRequest
  • onWebhook
  • onWebhookEmergencyCall
  • onError
  • onDisconnect

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-Bucket
  • X-RateLimit-Limit
  • X-RateLimit-Remaining
  • X-RateLimit-Reset

On 429, the client blocks the affected bucket until retry time or reset.

By default, requests are serialized (strictSerial: true) so this client does not spray parallel requests at the API.

Errors

The client normalizes errors into classes:

  • ERLCError
  • ERLCHttpError
  • ERLCAPIError
  • RateLimitError
  • KeyExpiredError (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.erlc.gg
  • server-key is required for requests
  • Authorization is optional (globalKey)