erlc-v2 1.1.0 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,609 +1,612 @@
1
- # erlc-v2
2
-
3
- JavaScript client for the ER:LC API v2.
4
-
1
+ # erlc-v2
2
+
3
+ JavaScript client for the ER:LC API v2.
4
+
5
5
  Built for Node 18+.
6
6
 
7
- Quick note: sorry this update took a while. There is a lot in this version, and I did not have a ton of time to get to it, so it ended up taking longer than I wanted.
8
-
9
- ## New Features
10
-
11
- - `client.commands.execute()` now uses `/v2/server/command`
12
- - emergency calls are supported
13
- - vehicle lookup helpers are built in
14
- - you can start a small local API with `api: { port: 3001 }`
15
- - event webhooks are supported and signature-checked before they fire events
16
-
17
- ## Stable Release
18
-
19
- This wrapper is on a stable release track (`1.0.0+`).
20
-
21
- ## Responsibility and API Safety
22
-
23
- 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.
24
-
25
- This project is provided as-is. Keep an eye on your own integration and follow the PRC API rules.
26
-
27
- ## Install
28
-
29
- ```bash
30
- npm install erlc-v2
31
- ```
32
-
33
- ## Quick Start (CommonJS)
34
-
35
- ```js
36
- const { Client } = require("erlc-v2");
37
-
38
- const client = new Client({
39
- serverKey: "YOUR_SERVER_KEY",
40
- polling: {
41
- enabled: true,
42
- },
43
- });
44
-
45
- client.on("disconnect", ({ reason, error }) => {
46
- console.error("Disconnected:", reason, error?.message);
47
- });
48
-
49
- async function main() {
50
- const snapshot = await client.server.fetch({
51
- players: true,
52
- staff: true,
53
- queue: true,
54
- vehicles: true,
55
- emergencyCalls: true,
56
- });
7
+ ## Important Upgrade Notice
57
8
 
58
- console.log("Server:", snapshot.name);
59
- console.log("Players:", `${snapshot.currentPlayers}/${snapshot.maxPlayers}`);
60
- console.log("Vehicles:", snapshot.vehicles.length);
61
- console.log("Emergency calls:", snapshot.emergencyCalls.length);
62
- }
63
-
64
- main()
65
- .catch(console.error)
66
- .finally(() => client.destroy());
67
- ```
68
-
69
- ## Quick Start (ESM)
70
-
71
- ```js
72
- import { Client } from "erlc-v2";
73
-
74
- const client = new Client({
75
- serverKey: "YOUR_SERVER_KEY",
76
- });
77
- ```
78
-
79
- ## Options
80
-
81
- ```ts
82
- new Client({
83
- serverKey: string, // required
84
- globalKey?: string, // optional
85
- logging?: boolean, // default: false
86
- logger?: { info, warn, error, debug },
87
- cache?: {
88
- enabled?: boolean, // default: true
89
- ttlMs?: number, // default: 1500
90
- maxSize?: number, // default: 500
91
- provider?: "memory" | "redis", // default: auto
92
- redisUrl?: string,
93
- redisPrefix?: string, // default: "erlc-v2:cache"
94
- redisClient?: object,
95
- },
96
- rateLimit?: {
97
- enabled?: boolean, // default: true
98
- strictSerial?: boolean, // default: true
99
- bucketLimit?: number, // default: 1
100
- totalLimit?: number, // default: 1
101
- unauthLimit?: number, // default: 3
102
- },
103
- polling?: {
104
- enabled?: boolean, // default: true
105
- intervalMs?: number, // default: 2500
106
- bypassCache?: boolean, // default: true
107
- },
108
- api?: {
109
- enabled?: boolean, // default: false unless port is set
110
- host?: string, // default: "127.0.0.1"
111
- port?: number, // required if you want the local API server
112
- path?: string, // default: "/erlc"
113
- webhookPath?: string, // default: `${path}/events`
114
- publicUrl?: string, // optional, used to build webhookUrl in client.api.info()
115
- token?: string, // optional bearer token for built-in routes
116
- logRequests?: boolean, // default: true, logs route/webhook hits to the console
117
- },
118
- });
119
- ```
120
-
121
- Legacy aliases (`perBucketConcurrency`, `globalConcurrency`, `unauthorizedThreshold`) are still accepted.
122
-
123
- ## Redis Cache (Optional)
124
-
125
- You can use Redis instead of in-memory cache by passing either `cache.redisUrl` or `cache.redisClient`.
126
-
127
- If you use `redisUrl`, install the Redis client package:
9
+ Update to the newest version as soon as possible:
128
10
 
129
11
  ```bash
130
- npm i redis
131
- ```
132
-
133
- Example with connection URL:
134
-
135
- ```js
136
- const { Client } = require("erlc-v2");
137
-
138
- const client = new Client({
139
- serverKey: "YOUR_SERVER_KEY",
140
- cache: {
141
- provider: "redis",
142
- redisUrl: "redis://localhost:6379",
143
- redisPrefix: "myapp:erlc",
144
- ttlMs: 2000,
145
- },
146
- });
147
- ```
148
-
149
- Example with your own Redis client instance:
150
-
151
- ```js
152
- const { createClient } = require("redis");
153
- const { Client } = require("erlc-v2");
154
-
155
- (async () => {
156
- const redis = createClient({ url: process.env.REDIS_URL });
157
- await redis.connect();
158
-
159
- const client = new Client({
160
- serverKey: "YOUR_SERVER_KEY",
161
- cache: {
162
- redisClient: redis,
163
- redisPrefix: "myapp:erlc",
164
- },
165
- });
166
- })();
167
- ```
168
-
169
- ## API
170
-
171
- Core:
172
-
173
- - `await client.server.fetch(flags, requestOptions?)`
174
- - `await client.commands.execute(command)`
175
- - `client.destroy()`
176
- - `client.cache.clear()`
177
-
178
- Convenience methods:
179
-
180
- - `await client.players.list()`
181
- - `await client.map.render(options?)`
182
- - `await client.map.renderUser(userId, options?)`
183
- - `await client.staff.list()`
184
- - `await client.logs.kills()`
185
- - `await client.logs.joins()`
186
- - `await client.logs.commands()`
187
- - `await client.logs.modCalls()`
188
- - `await client.logs.emergencyCalls()`
189
- - `await client.vehicles.list()`
190
- - `await client.vehicles.search(filters)`
191
- - `await client.vehicles.findByPlate(plate)`
192
- - `await client.vehicles.findByOwner(owner)`
193
- - `await client.vehicles.findOne(filters)`
194
- - `await client.queue.get()`
195
-
196
- ### Fetch Flags
197
-
198
- - `players` -> `Players`
199
- - `staff` -> `Staff`
200
- - `joinLogs` -> `JoinLogs`
201
- - `queue` -> `Queue`
202
- - `killLogs` -> `KillLogs`
203
- - `commandLogs` -> `CommandLogs`
204
- - `modCalls` -> `ModCalls`
205
- - `emergencyCalls` -> `EmergencyCalls`
206
- - `vehicles` -> `Vehicles`
207
-
208
- ### Request Options
209
-
210
- - `bypassCache?: boolean`
211
- - `cacheTtlMs?: number`
212
- - `dedupe?: boolean`
213
-
214
- ## Vehicle Search Helpers
215
-
216
- Find one exact plate:
217
-
218
- ```js
219
- const car = await client.vehicles.findByPlate("LINCOLN7");
220
-
221
- if (car) {
222
- console.log(car.Owner, car.Name, car.Plate);
223
- }
224
- ```
225
-
226
- Search across plate, owner, name, color, and texture:
227
-
228
- ```js
229
- const matches = await client.vehicles.search({
230
- query: "lincoln",
231
- });
232
-
233
- const ownerCars = await client.vehicles.findByOwner("lando");
234
-
235
- const blackTahoes = await client.vehicles.search({
236
- name: "tahoe",
237
- color: "black",
238
- });
239
- ```
240
-
241
- Exact matching is supported too:
242
-
243
- ```js
244
- const exact = await client.vehicles.findOne({
245
- plate: "A12BCD",
246
- owner: "SomePlayer",
247
- exact: true,
248
- });
249
- ```
250
-
251
- ## Emergency Calls
252
-
253
- ```js
254
- const calls = await client.logs.emergencyCalls();
255
-
256
- for (const call of calls) {
257
- console.log(call.CallNumber, call.Team, call.Description);
258
- }
259
-
260
- client.on("emergencyCall", ({ emergencyCall }) => {
261
- console.log("New emergency call:", emergencyCall.Description);
262
- });
263
- ```
264
-
265
- ## Command Execution
266
-
267
- `client.commands.execute(command)` sends a POST request to `/v2/server/command`.
268
- Command execution is FIFO-queued client-side, so commands run one-at-a-time in order.
269
-
270
- Blocked by client policy:
271
-
272
- - `:view`
273
- - `:to`
274
- - `:tocar`
275
- - `:toatv`
276
- - `:logs`
277
- - `:mods`
278
- - `:admins`
279
- - `helpers` / `:helpers`
280
- - `:administrators`
281
- - `:moderators`
282
- - `:killlogs`
283
- - `:kl`
284
- - `:cmds`
285
- - `:commands`
286
-
287
- Example:
288
-
289
- ```js
290
- const result = await client.commands.execute(":h Hey everyone!");
291
- console.log(result.message);
292
- ```
293
-
294
- ## Built-in Local API Server
295
-
296
- If you want the wrapper to expose a small HTTP server, it can do that too.
297
-
298
- ```js
299
- const client = new Client({
300
- serverKey: process.env.ERLC_SERVER_KEY,
301
- api: {
302
- port: 3001,
303
- host: "127.0.0.1",
304
- path: "/erlc",
305
- publicUrl: "https://hooks.example.com",
306
- token: process.env.ERLC_LOCAL_API_TOKEN,
307
- },
308
- });
309
-
310
- client.api.info();
12
+ npm install erlc-v2@latest
311
13
  ```
312
14
 
313
- If `api.port` is set, the local API auto-starts with the client. You can also call `await client.api.start()` yourself.
15
+ 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.
314
16
 
315
- By default it logs incoming route hits and verified webhook payloads to the console.
17
+ ## New Features
316
18
 
19
+ - `client.commands.execute()` now uses `/v2/server/command`
20
+ - emergency calls are supported
21
+ - vehicle lookup helpers are built in
22
+ - you can start a small local API with `api: { port: 3001 }`
23
+ - event webhooks are supported and signature-checked before they fire events
24
+
25
+ ## Stable Release
26
+
27
+ This wrapper is on a stable release track (`1.0.0+`).
28
+
29
+ ## Responsibility and API Safety
30
+
31
+ 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.
32
+
33
+ This project is provided as-is. Keep an eye on your own integration and follow the PRC API rules.
34
+
35
+ ## Install
36
+
37
+ ```bash
38
+ npm install erlc-v2
39
+ ```
40
+
41
+ ## Quick Start (CommonJS)
42
+
43
+ ```js
44
+ const { Client } = require("erlc-v2");
45
+
46
+ const client = new Client({
47
+ serverKey: "YOUR_SERVER_KEY",
48
+ polling: {
49
+ enabled: true,
50
+ },
51
+ });
52
+
53
+ client.on("disconnect", ({ reason, error }) => {
54
+ console.error("Disconnected:", reason, error?.message);
55
+ });
56
+
57
+ async function main() {
58
+ const snapshot = await client.server.fetch({
59
+ players: true,
60
+ staff: true,
61
+ queue: true,
62
+ vehicles: true,
63
+ emergencyCalls: true,
64
+ });
65
+
66
+ console.log("Server:", snapshot.name);
67
+ console.log("Players:", `${snapshot.currentPlayers}/${snapshot.maxPlayers}`);
68
+ console.log("Vehicles:", snapshot.vehicles.length);
69
+ console.log("Emergency calls:", snapshot.emergencyCalls.length);
70
+ }
71
+
72
+ main()
73
+ .catch(console.error)
74
+ .finally(() => client.destroy());
75
+ ```
76
+
77
+ ## Quick Start (ESM)
78
+
79
+ ```js
80
+ import { Client } from "erlc-v2";
81
+
82
+ const client = new Client({
83
+ serverKey: "YOUR_SERVER_KEY",
84
+ });
85
+ ```
86
+
87
+ ## Options
88
+
89
+ ```ts
90
+ new Client({
91
+ serverKey: string, // required
92
+ globalKey?: string, // optional
93
+ logging?: boolean, // default: false
94
+ logger?: { info, warn, error, debug },
95
+ cache?: {
96
+ enabled?: boolean, // default: true
97
+ ttlMs?: number, // default: 1500
98
+ maxSize?: number, // default: 500
99
+ provider?: "memory" | "redis", // default: auto
100
+ redisUrl?: string,
101
+ redisPrefix?: string, // default: "erlc-v2:cache"
102
+ redisClient?: object,
103
+ },
104
+ rateLimit?: {
105
+ enabled?: boolean, // default: true
106
+ strictSerial?: boolean, // default: true
107
+ bucketLimit?: number, // default: 1
108
+ totalLimit?: number, // default: 1
109
+ unauthLimit?: number, // default: 3
110
+ },
111
+ polling?: {
112
+ enabled?: boolean, // default: true
113
+ intervalMs?: number, // default: 2500
114
+ bypassCache?: boolean, // default: true
115
+ },
116
+ api?: {
117
+ enabled?: boolean, // default: false unless port is set
118
+ host?: string, // default: "127.0.0.1"
119
+ port?: number, // required if you want the local API server
120
+ path?: string, // default: "/erlc"
121
+ webhookPath?: string, // default: `${path}/events`
122
+ publicUrl?: string, // optional, used to build webhookUrl in client.api.info()
123
+ token?: string, // optional bearer token for built-in routes
124
+ logRequests?: boolean, // default: true, logs route/webhook hits to the console
125
+ },
126
+ });
127
+ ```
128
+
129
+ Legacy aliases (`perBucketConcurrency`, `globalConcurrency`, `unauthorizedThreshold`) are still accepted.
130
+
131
+ ## Redis Cache (Optional)
132
+
133
+ You can use Redis instead of in-memory cache by passing either `cache.redisUrl` or `cache.redisClient`.
134
+
135
+ If you use `redisUrl`, install the Redis client package:
136
+
137
+ ```bash
138
+ npm i redis
139
+ ```
140
+
141
+ Example with connection URL:
142
+
143
+ ```js
144
+ const { Client } = require("erlc-v2");
145
+
146
+ const client = new Client({
147
+ serverKey: "YOUR_SERVER_KEY",
148
+ cache: {
149
+ provider: "redis",
150
+ redisUrl: "redis://localhost:6379",
151
+ redisPrefix: "myapp:erlc",
152
+ ttlMs: 2000,
153
+ },
154
+ });
155
+ ```
156
+
157
+ Example with your own Redis client instance:
158
+
159
+ ```js
160
+ const { createClient } = require("redis");
161
+ const { Client } = require("erlc-v2");
162
+
163
+ (async () => {
164
+ const redis = createClient({ url: process.env.REDIS_URL });
165
+ await redis.connect();
166
+
167
+ const client = new Client({
168
+ serverKey: "YOUR_SERVER_KEY",
169
+ cache: {
170
+ redisClient: redis,
171
+ redisPrefix: "myapp:erlc",
172
+ },
173
+ });
174
+ })();
175
+ ```
176
+
177
+ ## API
178
+
179
+ Core:
180
+
181
+ - `await client.server.fetch(flags, requestOptions?)`
182
+ - `await client.commands.execute(command)`
183
+ - `client.destroy()`
184
+ - `client.cache.clear()`
185
+
186
+ Convenience methods:
187
+
188
+ - `await client.players.list()`
189
+ - `await client.map.render(options?)`
190
+ - `await client.map.renderUser(userId, options?)`
191
+ - `await client.staff.list()`
192
+ - `await client.logs.kills()`
193
+ - `await client.logs.joins()`
194
+ - `await client.logs.commands()`
195
+ - `await client.logs.modCalls()`
196
+ - `await client.logs.emergencyCalls()`
197
+ - `await client.vehicles.list()`
198
+ - `await client.vehicles.search(filters)`
199
+ - `await client.vehicles.findByPlate(plate)`
200
+ - `await client.vehicles.findByOwner(owner)`
201
+ - `await client.vehicles.findOne(filters)`
202
+ - `await client.queue.get()`
203
+
204
+ ### Fetch Flags
205
+
206
+ - `players` -> `Players`
207
+ - `staff` -> `Staff`
208
+ - `joinLogs` -> `JoinLogs`
209
+ - `queue` -> `Queue`
210
+ - `killLogs` -> `KillLogs`
211
+ - `commandLogs` -> `CommandLogs`
212
+ - `modCalls` -> `ModCalls`
213
+ - `emergencyCalls` -> `EmergencyCalls`
214
+ - `vehicles` -> `Vehicles`
215
+
216
+ ### Request Options
217
+
218
+ - `bypassCache?: boolean`
219
+ - `cacheTtlMs?: number`
220
+ - `dedupe?: boolean`
221
+
222
+ ## Vehicle Search Helpers
223
+
224
+ Find one exact plate:
225
+
226
+ ```js
227
+ const car = await client.vehicles.findByPlate("LINCOLN7");
228
+
229
+ if (car) {
230
+ console.log(car.Owner, car.Name, car.Plate);
231
+ }
232
+ ```
233
+
234
+ Search across plate, owner, name, color, and texture:
235
+
236
+ ```js
237
+ const matches = await client.vehicles.search({
238
+ query: "lincoln",
239
+ });
240
+
241
+ const ownerCars = await client.vehicles.findByOwner("lando");
242
+
243
+ const blackTahoes = await client.vehicles.search({
244
+ name: "tahoe",
245
+ color: "black",
246
+ });
247
+ ```
248
+
249
+ Exact matching is supported too:
250
+
251
+ ```js
252
+ const exact = await client.vehicles.findOne({
253
+ plate: "A12BCD",
254
+ owner: "SomePlayer",
255
+ exact: true,
256
+ });
257
+ ```
258
+
259
+ ## Emergency Calls
260
+
261
+ ```js
262
+ const calls = await client.logs.emergencyCalls();
263
+
264
+ for (const call of calls) {
265
+ console.log(call.CallNumber, call.Team, call.Description);
266
+ }
267
+
268
+ client.on("emergencyCall", ({ emergencyCall }) => {
269
+ console.log("New emergency call:", emergencyCall.Description);
270
+ });
271
+ ```
272
+
273
+ ## Command Execution
274
+
275
+ `client.commands.execute(command)` sends a POST request to `/v2/server/command`.
276
+ Command execution is FIFO-queued client-side, so commands run one-at-a-time in order.
277
+
278
+ Blocked by client policy:
279
+
280
+ - `:view`
281
+ - `:to`
282
+ - `:tocar`
283
+ - `:toatv`
284
+ - `:logs`
285
+ - `:mods`
286
+ - `:admins`
287
+ - `helpers` / `:helpers`
288
+ - `:administrators`
289
+ - `:moderators`
290
+ - `:killlogs`
291
+ - `:kl`
292
+ - `:cmds`
293
+ - `:commands`
294
+
295
+ Example:
296
+
297
+ ```js
298
+ const result = await client.commands.execute(":h Hey everyone!");
299
+ console.log(result.message);
300
+ ```
301
+
302
+ ## Built-in Local API Server
303
+
304
+ If you want the wrapper to expose a small HTTP server, it can do that too.
305
+
306
+ ```js
307
+ const client = new Client({
308
+ serverKey: process.env.ERLC_SERVER_KEY,
309
+ api: {
310
+ port: 3001,
311
+ host: "127.0.0.1",
312
+ path: "/erlc",
313
+ publicUrl: "https://hooks.example.com",
314
+ token: process.env.ERLC_LOCAL_API_TOKEN,
315
+ },
316
+ });
317
+
318
+ client.api.info();
319
+ ```
320
+
321
+ If `api.port` is set, the local API auto-starts with the client. You can also call `await client.api.start()` yourself.
322
+
323
+ By default it logs incoming route hits and verified webhook payloads to the console.
324
+
317
325
  If you want to react to ER:LC webhooks in your own code, use:
318
326
 
319
327
  - `client.onWebhook(...)`
320
- - `client.onWebhookCommand(...)`
321
328
  - `client.onWebhookEmergencyCall(...)`
322
329
 
323
330
  Those only fire after the webhook signature checks out.
324
-
325
- Built-in routes:
326
-
327
- - `GET /erlc`
328
- - `GET /erlc/health`
329
- - `GET /erlc/server`
330
- - `GET /erlc/players`
331
- - `GET /erlc/vehicles`
332
- - `GET /erlc/vehicles/:plate`
333
- - `GET /erlc/emergency-calls`
334
- - `POST /erlc/command`
335
- - `POST /erlc/events`
336
-
337
- `/erlc/vehicles` accepts query params like `search`, `plate`, `owner`, `name`, `color`, `texture`, `exact`, and `limit`.
338
-
339
- If you set `api.token`, send either:
340
-
341
- - `Authorization: Bearer YOUR_TOKEN`
342
- - `X-API-Token: YOUR_TOKEN`
343
-
344
- ## Event Webhook Support
345
-
346
- The built-in API can take ER:LC event webhooks and verify the signatures for you.
347
-
348
- ```js
349
- const client = new Client({
350
- serverKey: process.env.ERLC_SERVER_KEY,
351
- api: {
352
- port: 3001,
353
- publicUrl: "https://hooks.example.com",
354
- },
355
- });
356
-
331
+
332
+ Built-in routes:
333
+
334
+ - `GET /erlc`
335
+ - `GET /erlc/health`
336
+ - `GET /erlc/server`
337
+ - `GET /erlc/players`
338
+ - `GET /erlc/vehicles`
339
+ - `GET /erlc/vehicles/:plate`
340
+ - `GET /erlc/emergency-calls`
341
+ - `POST /erlc/command`
342
+ - `POST /erlc/events`
343
+
344
+ `/erlc/vehicles` accepts query params like `search`, `plate`, `owner`, `name`, `color`, `texture`, `exact`, and `limit`.
345
+
346
+ If you set `api.token`, send either:
347
+
348
+ - `Authorization: Bearer YOUR_TOKEN`
349
+ - `X-API-Token: YOUR_TOKEN`
350
+
351
+ ## Event Webhook Support
352
+
353
+ The built-in API can take ER:LC event webhooks and verify the signatures for you.
354
+
355
+ ```js
356
+ const client = new Client({
357
+ serverKey: process.env.ERLC_SERVER_KEY,
358
+ api: {
359
+ port: 3001,
360
+ publicUrl: "https://hooks.example.com",
361
+ },
362
+ });
363
+
357
364
  client.onWebhook((payload) => {
358
365
  console.log("Webhook type:", payload.type);
359
366
  console.log("Event name:", payload.event);
360
- });
361
-
362
- client.onWebhookCommand((payload) => {
363
367
  console.log("Command:", payload.command);
364
368
  console.log("Args:", payload.args);
365
369
  console.log("Origin:", payload.origin);
366
- // react to in-game ; commands here
367
370
  });
368
371
 
369
372
  client.onWebhookEmergencyCall((payload) => {
370
373
  console.log("Event:", payload.event);
371
- console.log("Origin:", payload.origin);
372
- console.log("Data:", payload.data);
373
- // react to emergency calls here
374
- });
375
- ```
376
-
377
- Useful flattened webhook fields:
378
-
379
- - `payload.type`
380
- - `payload.event`
381
- - `payload.origin`
382
- - `payload.server`
383
- - `payload.eventTimestamp`
384
- - `payload.data`
385
- - `payload.command`
386
- - `payload.args`
374
+ console.log("Origin:", payload.origin);
375
+ console.log("Data:", payload.data);
376
+ // react to emergency calls here
377
+ });
378
+ ```
379
+
380
+ Useful flattened webhook fields:
381
+
382
+ - `payload.type`
383
+ - `payload.event`
384
+ - `payload.origin`
385
+ - `payload.server`
386
+ - `payload.eventTimestamp`
387
+ - `payload.data`
388
+ - `payload.command`
389
+ - `payload.args`
387
390
  - `payload.argument`
388
391
  - `payload.entry`
389
392
  - `payload.events`
390
393
 
391
- For in-game custom commands, `onWebhookCommand(...)` is usually the one you want. Most of the time `payload.command`, `payload.args`, and `payload.origin` are enough.
392
-
393
- If your public URL is `https://hooks.example.com` and your API path is the default, set this in your ER:LC server settings:
394
-
395
- ```txt
396
- https://hooks.example.com/erlc/events
397
- ```
398
-
399
- If you want the longer request shape with type information, use:
400
-
401
- ```txt
402
- https://hooks.example.com/erlc/events?long=true
403
- ```
404
-
405
- ## Domain, Hosting, and Reverse Proxy Notes
406
-
407
- The event webhook has to hit a public HTTPS URL. A local port by itself is not enough.
408
-
409
- Important:
410
-
411
- - Most Discord bot hosts are bad for this because they do not let you expose your own API cleanly.
412
- - If your host does not allow inbound HTTP traffic, PRC will never reach your webhook.
413
- - You need something public in front of your wrapper.
414
-
415
- Common setups:
416
-
417
- - Buy a domain and point it at a VPS.
418
- - Run the wrapper on a VPS and put NGINX or Caddy in front of it.
419
- - Run it somewhere private and use Cloudflare Tunnel.
420
-
421
- Common places people use for domains:
422
-
423
- - Cloudflare Registrar: `https://www.cloudflare.com/products/registrar/`
424
- - Namecheap: `https://www.namecheap.com/`
425
- - Porkbun: `https://porkbun.com/`
426
-
427
- Common places people use for public hosting or a VPS:
428
-
429
- - DigitalOcean: `https://www.digitalocean.com/`
430
- - Hetzner: `https://www.hetzner.com/`
431
- - Railway: `https://railway.com/`
432
- - Render: `https://render.com/`
433
- - Fly.io: `https://fly.io/`
434
-
435
- Common reverse proxy or edge options:
436
-
437
- - NGINX: `https://nginx.org/`
438
- - Caddy: `https://caddyserver.com/`
439
- - Cloudflare Tunnel: `https://www.cloudflare.com/products/tunnel/`
440
-
441
- Those are just examples. Use whatever actually gives you inbound HTTPS and a process you control.
442
-
443
- ## Map Rendering
444
-
445
- Render an ER:LC map (`3121x3121`) with player markers that use Roblox avatars.
446
-
447
- ```js
448
- const result = await client.map.render();
449
-
450
- console.log(result.buffer);
451
- console.log(result.players.length);
452
- ```
453
-
454
- `client.map.render()` renders the full map with all players currently in the server.
455
-
456
- Render an official season/type map preset:
457
-
458
- ```js
459
- const fallBlank = await client.map.render({
460
- season: "fall",
461
- type: "blank",
462
- });
463
-
464
- const winterPostals = await client.map.render({
465
- season: "winter",
466
- type: "postals",
467
- });
468
- ```
469
-
470
- Use your own map image URL:
471
-
472
- ```js
473
- const customMap = await client.map.render({
474
- mapUrl: "https://example.com/my-map.png",
475
- });
476
- ```
477
-
478
- Render only one player by Roblox user ID:
479
-
480
- ```js
481
- const single = await client.map.renderUser(123456789, {
482
- season: "winter",
483
- type: "postals",
484
- });
485
- ```
486
-
487
- Options:
488
-
489
- - `userId?: number | string`
490
- - `userIds?: Array<number | string>`
491
- - `players?: any[]`
492
- - `mapUrl?: string`
493
- - `season?: string`
494
- - `type?: string`
495
- - `mapSeason?: string`
496
- - `mapType?: string`
497
- - `coordinateBounds?: { minX, maxX, minY, maxY, invertY? }`
498
- - `clampToMap?: boolean`
499
- - `robloxHeadshotSize?: string`
500
- - `marker?: { outerRadius, innerRadius, tipLength, tipWidth, fillColor, shadow }`
501
-
502
- Map size is fixed to `3121x3121`.
503
-
504
- Result shape:
505
-
506
- - `buffer` (`image/png`)
507
- - `map` (`{ url, season, type, width, height }`)
508
- - `players`
509
- - `skipped`
510
- - `requestedUserIds`
511
- - `unmatchedUserIds`
512
-
513
- ## Events
514
-
515
- - `ready`
516
- - `playerJoin`
517
- - `playerLeave`
518
- - `kill`
519
- - `vehicleSpawn`
520
- - `vehicleDespawn`
521
- - `queueUpdate`
522
- - `staffUpdate`
523
- - `modCall`
524
- - `emergencyCall`
525
- - `commandLog`
526
- - `logCommand`
394
+ This webhook is for custom `;` commands and emergency calls. It is not for normal `:` commands from the command endpoint.
395
+
396
+ For custom `;` commands, `payload.command`, `payload.args`, and `payload.origin` are usually the fields you want.
397
+
398
+ If your public URL is `https://hooks.example.com` and your API path is the default, set this in your ER:LC server settings:
399
+
400
+ ```txt
401
+ https://hooks.example.com/erlc/events
402
+ ```
403
+
404
+ If you want the longer request shape with type information, use:
405
+
406
+ ```txt
407
+ https://hooks.example.com/erlc/events?long=true
408
+ ```
409
+
410
+ ## Domain, Hosting, and Reverse Proxy Notes
411
+
412
+ The event webhook has to hit a public HTTPS URL. A local port by itself is not enough.
413
+
414
+ Important:
415
+
416
+ - Most Discord bot hosts are bad for this because they do not let you expose your own API cleanly.
417
+ - If your host does not allow inbound HTTP traffic, PRC will never reach your webhook.
418
+ - You need something public in front of your wrapper.
419
+
420
+ Common setups:
421
+
422
+ - Buy a domain and point it at a VPS.
423
+ - Run the wrapper on a VPS and put NGINX or Caddy in front of it.
424
+ - Run it somewhere private and use Cloudflare Tunnel.
425
+
426
+ Common places people use for domains:
427
+
428
+ - Cloudflare Registrar: `https://www.cloudflare.com/products/registrar/`
429
+ - Namecheap: `https://www.namecheap.com/`
430
+ - Porkbun: `https://porkbun.com/`
431
+
432
+ Common places people use for public hosting or a VPS:
433
+
434
+ - DigitalOcean: `https://www.digitalocean.com/`
435
+ - Hetzner: `https://www.hetzner.com/`
436
+ - Railway: `https://railway.com/`
437
+ - Render: `https://render.com/`
438
+ - Fly.io: `https://fly.io/`
439
+
440
+ Common reverse proxy or edge options:
441
+
442
+ - NGINX: `https://nginx.org/`
443
+ - Caddy: `https://caddyserver.com/`
444
+ - Cloudflare Tunnel: `https://www.cloudflare.com/products/tunnel/`
445
+
446
+ Those are just examples. Use whatever actually gives you inbound HTTPS and a process you control.
447
+
448
+ ## Map Rendering
449
+
450
+ Render an ER:LC map (`3121x3121`) with player markers that use Roblox avatars.
451
+
452
+ ```js
453
+ const result = await client.map.render();
454
+
455
+ console.log(result.buffer);
456
+ console.log(result.players.length);
457
+ ```
458
+
459
+ `client.map.render()` renders the full map with all players currently in the server.
460
+
461
+ Render an official season/type map preset:
462
+
463
+ ```js
464
+ const fallBlank = await client.map.render({
465
+ season: "fall",
466
+ type: "blank",
467
+ });
468
+
469
+ const winterPostals = await client.map.render({
470
+ season: "winter",
471
+ type: "postals",
472
+ });
473
+ ```
474
+
475
+ Use your own map image URL:
476
+
477
+ ```js
478
+ const customMap = await client.map.render({
479
+ mapUrl: "https://example.com/my-map.png",
480
+ });
481
+ ```
482
+
483
+ Render only one player by Roblox user ID:
484
+
485
+ ```js
486
+ const single = await client.map.renderUser(123456789, {
487
+ season: "winter",
488
+ type: "postals",
489
+ });
490
+ ```
491
+
492
+ Options:
493
+
494
+ - `userId?: number | string`
495
+ - `userIds?: Array<number | string>`
496
+ - `players?: any[]`
497
+ - `mapUrl?: string`
498
+ - `season?: string`
499
+ - `type?: string`
500
+ - `mapSeason?: string`
501
+ - `mapType?: string`
502
+ - `coordinateBounds?: { minX, maxX, minY, maxY, invertY? }`
503
+ - `clampToMap?: boolean`
504
+ - `robloxHeadshotSize?: string`
505
+ - `marker?: { outerRadius, innerRadius, tipLength, tipWidth, fillColor, shadow }`
506
+
507
+ Map size is fixed to `3121x3121`.
508
+
509
+ Result shape:
510
+
511
+ - `buffer` (`image/png`)
512
+ - `map` (`{ url, season, type, width, height }`)
513
+ - `players`
514
+ - `skipped`
515
+ - `requestedUserIds`
516
+ - `unmatchedUserIds`
517
+
518
+ ## Events
519
+
520
+ - `ready`
521
+ - `playerJoin`
522
+ - `playerLeave`
523
+ - `kill`
524
+ - `vehicleSpawn`
525
+ - `vehicleDespawn`
526
+ - `queueUpdate`
527
+ - `staffUpdate`
528
+ - `modCall`
529
+ - `emergencyCall`
530
+ - `commandLog`
531
+ - `logCommand`
527
532
  - `serverUpdate`
528
533
  - `webhook`
529
- - `webhookCommand`
530
534
  - `webhookEmergencyCall`
531
- - `error`
532
- - `disconnect`
533
-
534
- Alias event names are also supported with `client.on(...)`:
535
-
536
- - `onReady`
537
- - `onJoin`
538
- - `onLeave`
539
- - `onKill`
540
- - `onVehicleSpawn`
541
- - `onVehicleDespawn`
542
- - `onQueueUpdate`
543
- - `onStaffUpdate`
544
- - `onModCall`
545
- - `onEmergencyCall`
546
- - `onCommandLog`
547
- - `onLogCommand`
535
+ - `error`
536
+ - `disconnect`
537
+
538
+ Alias event names are also supported with `client.on(...)`:
539
+
540
+ - `onReady`
541
+ - `onJoin`
542
+ - `onLeave`
543
+ - `onKill`
544
+ - `onVehicleSpawn`
545
+ - `onVehicleDespawn`
546
+ - `onQueueUpdate`
547
+ - `onStaffUpdate`
548
+ - `onModCall`
549
+ - `onEmergencyCall`
550
+ - `onCommandLog`
551
+ - `onLogCommand`
548
552
  - `onServerUpdate`
549
553
  - `onApiRequest`
550
554
  - `onWebhook`
551
- - `onWebhookCommand`
552
555
  - `onWebhookEmergencyCall`
553
- - `onError`
554
- - `onDisconnect`
555
-
556
- Shortcut methods are available too:
557
-
558
- ```js
559
- client.onJoin((payload) => console.log("join", payload));
560
- client.onLeave((payload) => console.log("leave", payload));
561
- client.onVehicleSpawn((payload) => console.log("spawn", payload));
562
- client.onLogCommand(({ command, parsed }) => {
563
- console.log("raw command:", command.Command);
564
- console.log("keyword:", parsed.keyword);
565
- console.log("args:", parsed.args);
566
- });
567
- ```
568
-
569
- `logCommand` / `onLogCommand` fires when a command starts with `:log`.
570
-
571
- Events are deduped per poll cycle so the same log entry is not emitted repeatedly.
572
-
573
- ## Rate Limits
574
-
575
- Requests are automatically bucketed using API response headers:
576
-
577
- - `X-RateLimit-Bucket`
578
- - `X-RateLimit-Limit`
579
- - `X-RateLimit-Remaining`
580
- - `X-RateLimit-Reset`
581
-
582
- On `429`, the client blocks the affected bucket until retry time or reset.
583
-
584
- By default, requests are serialized (`strictSerial: true`) so this client does not spray parallel requests at the API.
585
-
586
- ## Errors
587
-
588
- The client normalizes errors into classes:
589
-
590
- - `ERLCError`
591
- - `ERLCHttpError`
592
- - `ERLCAPIError`
593
- - `RateLimitError`
594
- - `KeyExpiredError` (`2002`)
595
- - `KeyBannedError` (`2004`)
596
- - `InvalidGlobalKeyError` (`2003`)
597
- - `ServerOfflineError` (`3002`)
598
- - `RestrictedError` (`9998`)
599
- - `ModuleOutOfDateError` (`9999`)
600
-
601
- Terminal key errors (`2002`, `2004`) trigger disconnect and stop polling.
602
-
603
- Repeated `403` responses can also trigger disconnect (`reason: "unauthorized"`).
604
-
605
- ## Notes
606
-
607
- - API base URL: `https://api.policeroleplay.community`
608
- - `server-key` is required for requests
556
+ - `onError`
557
+ - `onDisconnect`
558
+
559
+ Shortcut methods are available too:
560
+
561
+ ```js
562
+ client.onJoin((payload) => console.log("join", payload));
563
+ client.onLeave((payload) => console.log("leave", payload));
564
+ client.onVehicleSpawn((payload) => console.log("spawn", payload));
565
+ client.onLogCommand(({ command, parsed }) => {
566
+ console.log("raw command:", command.Command);
567
+ console.log("keyword:", parsed.keyword);
568
+ console.log("args:", parsed.args);
569
+ });
570
+ ```
571
+
572
+ `logCommand` / `onLogCommand` fires when a command starts with `:log`.
573
+
574
+ Events are deduped per poll cycle so the same log entry is not emitted repeatedly.
575
+
576
+ ## Rate Limits
577
+
578
+ Requests are automatically bucketed using API response headers:
579
+
580
+ - `X-RateLimit-Bucket`
581
+ - `X-RateLimit-Limit`
582
+ - `X-RateLimit-Remaining`
583
+ - `X-RateLimit-Reset`
584
+
585
+ On `429`, the client blocks the affected bucket until retry time or reset.
586
+
587
+ By default, requests are serialized (`strictSerial: true`) so this client does not spray parallel requests at the API.
588
+
589
+ ## Errors
590
+
591
+ The client normalizes errors into classes:
592
+
593
+ - `ERLCError`
594
+ - `ERLCHttpError`
595
+ - `ERLCAPIError`
596
+ - `RateLimitError`
597
+ - `KeyExpiredError` (`2002`)
598
+ - `KeyBannedError` (`2004`)
599
+ - `InvalidGlobalKeyError` (`2003`)
600
+ - `ServerOfflineError` (`3002`)
601
+ - `RestrictedError` (`9998`)
602
+ - `ModuleOutOfDateError` (`9999`)
603
+
604
+ Terminal key errors (`2002`, `2004`) trigger disconnect and stop polling.
605
+
606
+ Repeated `403` responses can also trigger disconnect (`reason: "unauthorized"`).
607
+
608
+ ## Notes
609
+
610
+ - API base URL: `https://api.erlc.gg`
611
+ - `server-key` is required for requests
609
612
  - `Authorization` is optional (`globalKey`)