Connect a bot to Oversite
For people who build or run Discord bots for ER:LC servers: department, roleplay, economy and moderation bots. Read the server’s CAD, change it, and get its events as they happen.
01Getting started
In shortThe server’s owner connects your bot in their Oversite settings, under Bots, and gives you its key. Send the key with every request.
Every request goes to https://www.oversitescad.com/api/bot/v1/ with the header Authorization: Bot <key>. Bodies and answers are JSON. A key belongs to one server and one bot, and works for what the owner ticked, nothing more.
curl https://www.oversitescad.com/api/bot/v1/server \
-H "Authorization: Bot ovb_..."
Errors come back as { "error": "…" } with a status: 401 unknown key, 402 the server’s plan doesn’t cover this bot, 403 missing permission (the answer names it in permission) or the feature is switched off, 404 not found, 429 over 120 requests a minute. The error text is written for people, so you can show it in Discord as it is.
Keep the key secret, like a bot token. If it leaks, the owner makes a new one from the same page and the old one stops working.
02Permissions
The owner ticks these for your bot. Ask for the ones you need.
| Permission | Name | Lets the bot |
|---|---|---|
read | Read the server | Players in game, units on duty, open calls, unit status and shifts. |
people | Match Discord and Roblox | Find a member’s Roblox account from their Discord ID, and the other way round. |
lookup | Police lookups | Look up plates and people, and read police records. |
calls | Calls | Put 911 calls on the dispatch board and clear them. |
units | Unit status | Set a unit’s 10-code status. |
records | Write police records | Citations, warnings, arrests, BOLOs and warrants. Warrants still wait for a supervisor. |
vehicles | Vehicle papers | Set a plate’s registration, insurance and stolen flag. |
bank | Bank | Read balances, and pay or charge members in the Civilian economy. |
fines | Fines | Look up the fines on people’s citations and arrests, and mark them paid. For an economy of your own. |
alerts | CAD alerts | Pop up an alert in the CAD for one or more departments. |
game | In-game commands | Run ER:LC commands like :h, :pm, :kick and :ban. They show in your logs under the bot’s name. |
03Endpoints
All paths are under /api/bot/v1/.
| Request | Needs | What it does |
|---|---|---|
GET me | nothing | The bot’s name, its permissions, and the server it belongs to. |
GET server | read | Name, whether ER:LC is answering, players, max players, open calls, join code and the CAD link. |
GET players | read | Everyone in game: name, Roblox ID, team, department, callsign, postal and street. With Match Discord and Roblox, each also has discord_id when they have an Oversite account. |
GET calls | read | Open 911 calls with their key, who called, where, and the units on them. |
GET units | read | Units on duty by department, with callsign and 10-code status. |
GET shifts?days=7&roblox_id= | read | Time on duty, counted from the in-game team (1 to 30 days; one person with roblox_id). |
GET duty-shifts?since= | read | Shifts people started and ended on their department’s MDT, changed since since (ms; send back the now it answered). Deleted ones come with deleted: true. An Oversite bot linked for one department gets only that one. |
GET loa?status=pending | read | Leave (LOA) requests from departments and the staff team: who, which department, first and last day, reason and status. Leave out status for recent ones too. Or get them as they happen with the loa.requested event. |
GET people?discord_id= | people | A member’s Roblox account from their Discord ID. Also takes roblox_id or roblox_name. |
GET lookup?q= | lookup | Search people and plates, with their warrant and BOLO flags. |
GET person?roblox_name= | lookup | One person’s criminal record, for a /search command: wanted, officer safety markers (like FI, Firearms), open warrants, BOLOs, arrests, citations and warnings with counts, their vehicles and license. Also takes discord_id or roblox_id. |
GET plates/{plate} | lookup | A plate: model, color, owner, registration, insurance, stolen, flags and its records. |
GET penal | lookup | The server’s charges: code, title, fine, jail time and class, for writing records. |
GET records?kind=&status=open | lookup | Police records, newest first (citation, warning, arrest, warrant, bolo, stop). |
POST calls | calls | Put a call on the dispatch board: { "department": "pd", "location": "Postal 214", "details": "Shots fired" }. |
POST calls/clear | calls | Clear a call for everyone: { "key": "…" } from GET calls. |
POST units/status | units | Set a unit’s status: { "unit": "1A-12", "status": "10-8" }. "10 8", "10-08" and "ten-eight" work too. The answer has the code and what it means here ("label": "Available"), so the bot can say back the CAD’s own words. |
POST records | records | Write a record as the bot, like the MDT does. Warrants wait for a supervisor. See below. |
POST records/{id}/act | records | Serve or recall a warrant, or clear a BOLO: { "action": "serve" }. Sends record.updated. |
POST licenses | records | Suspend, revoke or reinstate a license: { "roblox_name": "Kesh", "license": "driver", "status": "Suspended", "reason": "DUI", "days": 7 }. license is driver or firearm; status is Suspended, Revoked or Valid; days only for a suspension (leave it out for until it’s reinstated); officer names who asked. Only someone seen in the server. |
POST plates/{plate} | vehicles | Change papers on a plate Oversite has seen: { "reg": "Expired", "ins": "Lapsed", "stolen": true }. reg is Valid, Expired or Suspended; ins is Active, Lapsed or None. |
GET bank?discord_id= | bank | A member’s balance in the Civilian economy. |
POST bank | bank | Pay (positive) or charge (negative): { "discord_id": "…", "amount": 500, "memo": "Paycheck" }. Charges never go below zero unless "force": true. |
GET fines?discord_id=&status=unpaid | fines | Someone’s fines from citations and arrests, and owed in total. Also takes roblox_id or roblox_name; status is unpaid (the default), paid or all. |
GET fines/{id} | fines | One fine: charges, amount, officer, when it was issued, and whether and how it was paid. |
POST fines/{id}/pay | fines | Mark a fine paid once your economy took the money: { "note": "Paid with UnbelievaBoat", "by": "Kesh" } (both optional). Sends fine.paid. |
POST fines/{id}/unpay | fines | Undo a fine marked paid by mistake. Sends fine.unpaid. |
POST fines/pay-all | fines | Mark all of someone’s unpaid fines paid: { "discord_id": "…" }. The answer has how many and the total. |
POST alerts | alerts | A popup in the CAD: { "departments": ["pd", "fd"], "title": "Storm warning", "text": "…" }. |
POST commands | game | Run an ER:LC command: { "command": ":h Server restart in 5 minutes" }. |
Live data (players, calls, units) comes from ER:LC through Oversite, so when ER:LC isn’t answering, live is false and the lists are empty.
04Discord and Roblox
Your bot knows people by their Discord ID; the CAD knows them by Roblox. Anywhere a request needs a person (people, bank, records), send one of discord_id, roblox_id or roblox_name. People are matched through their Oversite account, so someone who has never signed in to Oversite can’t be found by Discord ID.
GET /api/bot/v1/people?discord_id=123456789012345678
{ "person": { "discord_id": "123456789012345678", "roblox_id": "7001", "roblox_name": "Hype", "member": "member" } }05Writing police records
Records written by your bot show in the MDT under the bot’s name. The fields match the MDT’s forms:
POST /api/bot/v1/records
{
"kind": "citation",
"discord_id": "123456789012345678",
"plate": "ABC 123",
"charges": ["T-102"],
"reason": "72 in a 45"
}
kind is citation, warning, arrest, warrant, bolo or stop. Charges are codes from GET penal (like "T-102"), or a title when only one charge matches it ("Hit and run"); fines come from the code. A charge that isn't in the server's laws can be written out: { "custom": "Solicitation to commit murder", "fine": 5000, "jail": 60 }. Name the person with subject (their Roblox username) or any of the ways above. Warrants from a bot always wait for a supervisor to approve them in the MDT.
06Events
In shortGive the owner a web address. Oversite sends a signed POST to it the moment something happens, for the events they tick.
| Event | When | Needs |
|---|---|---|
call.created | A 911 call comes in | read |
call.updated | Units are assigned to a call or move on it | read |
call.cleared | A call is cleared | read |
unit.status | A unit changes status | read |
panic | A unit presses panic | read |
shift.started | Someone goes on duty | read |
shift.ended | Someone goes off duty | read |
player.joined | A player joins the server | read |
player.left | A player leaves the server | read |
alpr | The plate reader flags a car | lookup |
record.created | Police write a record (a warrant once it’s approved) | lookup |
record.updated | A warrant is served, recalled or denied, a BOLO is cleared, or a stop ends | lookup |
monitor.alert | Someone on an ankle monitor leaves their area, enters a no-go zone or comes back | lookup |
license.updated | Police suspend, revoke or reinstate a driver’s license or firearms permit | lookup |
case.filed | Someone files a civil suit in Court | read |
case.updated | A judge takes a case, sets a hearing or rules, or a case is settled or withdrawn | read |
rdm.flagged | RDM watch flags a possible random deathmatch (or adds a kill to a flag) | read |
arrest.logged | Someone is arrested in game (sent to jail): who, the officer closest to them and where | read |
fine.issued | A citation or arrest comes with a fine | fines |
fine.paid | A fine is paid (by a bot, in the MDT or from the Civilian bank) | fines |
fine.unpaid | A fine marked paid by mistake is marked unpaid again | fines |
loa.requested | Someone asks for leave (LOA) from a department or the staff team | read |
loa.decided | A leave request is approved or denied | read |
POST https://your-bot.example.com/oversite
X-Oversite-Event: call.created
X-Oversite-Signature: t=1760600000,v1=5f2c...
{
"id": "evt_3f9a1c2b7d4e6a80",
"event": "call.created",
"server": { "name": "Liberty County RP", "slug": "lcrp" },
"at": "2026-10-16T04:00:00.000Z",
"data": { "key": "41@1760599990", "team": "Police", "description": "Shots fired", "where": "Postal 214" }
}
Answer with any 2xx within 6 seconds. A failed delivery is tried once more 15 seconds later. Shifts start once someone has been on a department team for a minute. Players, calls and shifts are read from ER:LC about every minute when nobody has the CAD open, and within seconds when someone does.
Check the signature on every event, so nobody else can pretend to be Oversite. It’s an HMAC-SHA256 of t + "." + the raw body, keyed with the signing secret the owner gave you:
// Node.js (Express): use the raw body, not parsed JSON
const crypto = require('crypto');
app.post('/oversite', express.raw({ type: 'application/json' }), (req, res) => {
const [t, v1] = req.get('x-oversite-signature').split(',').map(p => p.split('=')[1]);
const want = crypto.createHmac('sha256', process.env.OVERSITE_SECRET).update(t + '.' + req.body).digest('hex');
const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300;
if (!fresh || !crypto.timingSafeEqual(Buffer.from(want), Buffer.from(v1))) return res.sendStatus(401);
const event = JSON.parse(req.body);
// event.event, event.data ...
res.sendStatus(200);
});
# Python (Flask)
import hmac, hashlib, time
@app.post("/oversite")
def oversite():
t, v1 = [p.split("=")[1] for p in request.headers["X-Oversite-Signature"].split(",")]
want = hmac.new(SECRET.encode(), f"{t}.".encode() + request.get_data(), hashlib.sha256).hexdigest()
if abs(time.time() - int(t)) > 300 or not hmac.compare_digest(want, v1):
return "", 401
event = request.get_json()
return "", 200
The owner can press Send a test event in their settings; it arrives as test with "test": true.
07Your own economy
In shortPick Economy bot when connecting. Your bot takes the money for fines in your own economy, and Oversite keeps track of what is paid.
Every citation and arrest in the MDT can come with a fine. When a server runs its own economy (a Discord economy bot, say) instead of the Civilian tab’s bank, the bot that runs it can:
- Hear about each new fine with the
fine.issuedevent, to take the money straight away or send the player a bill. - Look up what someone owes with
GET fines?discord_id=…, for a/finescommand. - Mark a fine paid with
POST fines/{id}/pay(or all of them withPOST fines/pay-all) once it took the money. The MDT then shows it paid, with who marked it.
// /payfines: take what they owe from your economy, then tell Oversite
const H = { Authorization: 'Bot ' + process.env.OVERSITE_KEY, 'content-type': 'application/json' };
const r = await fetch(API + 'fines?discord_id=' + user.id, { headers: H }).then(r => r.json());
if (!r.owed) return reply('You have no unpaid fines.');
if (await balanceOf(user.id) < r.owed) return reply('You owe $' + r.owed + ' and can’t cover it.');
await takeMoney(user.id, r.owed);
await fetch(API + 'fines/pay-all', { method: 'POST', headers: H, body: JSON.stringify({ discord_id: user.id, note: 'Paid in Discord' }) });
reply('Paid $' + r.owed + ' in fines.');
A fine paid any other way sends fine.paid too: a Sergeant or above marking it paid in the MDT, or the player paying it from the Civilian bank. Each fine looks like this:
{ "id": 412, "kind": "citation", "subject": "Kesh", "roblox_id": "1234567", "charges": [{ "code": "T-101", "title": "Speeding", "fine": 250, "count": 1 }],
"fine": 250, "amount": 250, "officer": "22HYPE22", "issued_at": 1761400000000, "due_at": null,
"paid": false, "paid_at": null, "paid_by": null, "note": null }
amount is what is owed now: with the Civilian tab’s bank on, an overdue fine there has a late fee added.
08A Justice (DOJ) bot
In shortPick Justice (DOJ) bot when connecting it. Every department writes in the same CAD, so one bot hears every arrest, citation and warrant and can post it to each department’s Discord.
Subscribe to record.created and record.updated. record.created comes when an officer of any department files an arrest, citation, warning or BOLO, and when a supervisor approves a warrant (so a warrant only goes out once it’s real). record.updated comes when a warrant is served, recalled or denied, a BOLO is cleared or a stop ends, with action and action_by.
// post every new record to each department's log channel
if (event.event === 'record.created') {
const r = event.data; // r.kind, r.status, r.subject, r.charges, r.fine, r.jail, r.by, r.unit
for (const id of [LAPD_LOG, SHERIFF_LOG, HIGHWAY_LOG]) client.channels.cache.get(id)?.send(
`**${r.kind}** · ${r.subject} · ${(r.charges || []).map(c => c.title).join(', ')} · by ${r.by}`);
}
// /search player:<roblox name>
const s = await oversite('/person?roblox_name=' + encodeURIComponent(name));
// s.wanted, s.warrants, s.counts.arrests, s.counts.citations, s.records
Your bot can also file records itself with POST records, and serve warrants with POST records/{id}/act. Warrants it writes wait for a supervisor in the MDT.
09A slash command, start to finish
A discord.js command that shows who is on duty, using the read permission:
const API = 'https://www.oversitescad.com/api/bot/v1';
const oversite = path => fetch(API + path, { headers: { Authorization: 'Bot ' + process.env.OVERSITE_KEY } }).then(r => r.json());
client.on('interactionCreate', async i => {
if (!i.isChatInputCommand() || i.commandName !== 'onduty') return;
const { live, departments, error } = await oversite('/units');
if (error) return i.reply({ content: error, ephemeral: true });
if (!live) return i.reply('The game server isn’t answering right now.');
await i.reply(departments.map(d => `**${d.name}**: ${d.units.map(u => u.callsign || u.name).join(', ') || 'nobody'}`).join('\n'));
});