Oversite CAD ExploreSign in

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.

PermissionNameLets the bot
readRead the serverPlayers in game, units on duty, open calls, unit status and shifts.
peopleMatch Discord and RobloxFind a member’s Roblox account from their Discord ID, and the other way round.
lookupPolice lookupsLook up plates and people, and read police records.
callsCallsPut 911 calls on the dispatch board and clear them.
unitsUnit statusSet a unit’s 10-code status.
recordsWrite police recordsCitations, warnings, arrests, BOLOs and warrants. Warrants still wait for a supervisor.
vehiclesVehicle papersSet a plate’s registration, insurance and stolen flag.
bankBankRead balances, and pay or charge members in the Civilian economy.
finesFinesLook up the fines on people’s citations and arrests, and mark them paid. For an economy of your own.
alertsCAD alertsPop up an alert in the CAD for one or more departments.
gameIn-game commandsRun 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/.

RequestNeedsWhat it does
GET menothingThe bot’s name, its permissions, and the server it belongs to.
GET serverreadName, whether ER:LC is answering, players, max players, open calls, join code and the CAD link.
GET playersreadEveryone 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 callsreadOpen 911 calls with their key, who called, where, and the units on them.
GET unitsreadUnits on duty by department, with callsign and 10-code status.
GET shifts?days=7&roblox_id=readTime on duty, counted from the in-game team (1 to 30 days; one person with roblox_id).
GET duty-shifts?since=readShifts 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=pendingreadLeave (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=peopleA member’s Roblox account from their Discord ID. Also takes roblox_id or roblox_name.
GET lookup?q=lookupSearch people and plates, with their warrant and BOLO flags.
GET person?roblox_name=lookupOne 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}lookupA plate: model, color, owner, registration, insurance, stolen, flags and its records.
GET penallookupThe server’s charges: code, title, fine, jail time and class, for writing records.
GET records?kind=&status=openlookupPolice records, newest first (citation, warning, arrest, warrant, bolo, stop).
POST callscallsPut a call on the dispatch board: { "department": "pd", "location": "Postal 214", "details": "Shots fired" }.
POST calls/clearcallsClear a call for everyone: { "key": "…" } from GET calls.
POST units/statusunitsSet 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 recordsrecordsWrite a record as the bot, like the MDT does. Warrants wait for a supervisor. See below.
POST records/{id}/actrecordsServe or recall a warrant, or clear a BOLO: { "action": "serve" }. Sends record.updated.
POST licensesrecordsSuspend, 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}vehiclesChange 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=bankA member’s balance in the Civilian economy.
POST bankbankPay (positive) or charge (negative): { "discord_id": "…", "amount": 500, "memo": "Paycheck" }. Charges never go below zero unless "force": true.
GET fines?discord_id=&status=unpaidfinesSomeone’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}finesOne fine: charges, amount, officer, when it was issued, and whether and how it was paid.
POST fines/{id}/payfinesMark a fine paid once your economy took the money: { "note": "Paid with UnbelievaBoat", "by": "Kesh" } (both optional). Sends fine.paid.
POST fines/{id}/unpayfinesUndo a fine marked paid by mistake. Sends fine.unpaid.
POST fines/pay-allfinesMark all of someone’s unpaid fines paid: { "discord_id": "…" }. The answer has how many and the total.
POST alertsalertsA popup in the CAD: { "departments": ["pd", "fd"], "title": "Storm warning", "text": "…" }.
POST commandsgameRun 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.

EventWhenNeeds
call.createdA 911 call comes inread
call.updatedUnits are assigned to a call or move on itread
call.clearedA call is clearedread
unit.statusA unit changes statusread
panicA unit presses panicread
shift.startedSomeone goes on dutyread
shift.endedSomeone goes off dutyread
player.joinedA player joins the serverread
player.leftA player leaves the serverread
alprThe plate reader flags a carlookup
record.createdPolice write a record (a warrant once it’s approved)lookup
record.updatedA warrant is served, recalled or denied, a BOLO is cleared, or a stop endslookup
monitor.alertSomeone on an ankle monitor leaves their area, enters a no-go zone or comes backlookup
license.updatedPolice suspend, revoke or reinstate a driver’s license or firearms permitlookup
case.filedSomeone files a civil suit in Courtread
case.updatedA judge takes a case, sets a hearing or rules, or a case is settled or withdrawnread
rdm.flaggedRDM watch flags a possible random deathmatch (or adds a kill to a flag)read
arrest.loggedSomeone is arrested in game (sent to jail): who, the officer closest to them and whereread
fine.issuedA citation or arrest comes with a finefines
fine.paidA fine is paid (by a bot, in the MDT or from the Civilian bank)fines
fine.unpaidA fine marked paid by mistake is marked unpaid againfines
loa.requestedSomeone asks for leave (LOA) from a department or the staff teamread
loa.decidedA leave request is approved or deniedread
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.issued event, to take the money straight away or send the player a bill.
  • Look up what someone owes with GET fines?discord_id=…, for a /fines command.
  • Mark a fine paid with POST fines/{id}/pay (or all of them with POST 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'));
});