Evidence Bridge
How seized trap phones reach your police paperwork — built-in Evidence View, hooks and events for logging reports, and writing an MDT bridge so Send report attaches a phone to a case in your own MDT.
How seized trap phones reach your police paperwork. This page is for server owners and developers who want an officer's Send report to land in their own MDT (the police computer where cases live), or who want phone evidence logged somewhere without an MDT at all.
Running oxide-police? You don't need this page: its bridge ships ready and switched on (see Installation → oxide-police).
TL;DR for other MDTs — Drop a Lua file in server/integrations/evidence/, register it with EvidenceBackend.Register under your MDT's resource name, implement AttachReport, add a row for it under Pressure → Evidence → Evidence bridges in /drugselling settings, and restart.
The Three Layers
| Layer | What it gives you | What you need |
|---|---|---|
| 1. Built in | Officers read a seized phone in game: texts, contacts, meets, number traces | Nothing. Works on every server |
| 2. Hooks | Your own code gets the whole phone as a report every time an officer opens one | A few lines in shared/config.lua, or a small resource of your own |
| 3. MDT bridges | Send report attaches the phone to a case in your MDT | A bridge file for your MDT |
Each layer works without the ones below it. A server with no police resource at all still gets layer 1.
Layer 1: Built In
A trap phone is a normal inventory item, so whatever your police use to search people, confiscate items or store evidence already moves it. When an officer with an evidence job uses a phone that isn't theirs, they get Evidence View: a read-only copy of the phone with every conversation, the contacts, the booked meets (with GPS), and Trace number on other burners' numbers.
Nothing to install or configure. The settings are in Pressure → Evidence (see Configuration → Evidence).
Without a bridge, Send report tells the officer "No case system". Everything else works.
Layer 2: Hooks
Every time an officer opens a seized phone, oxide-drugselling builds a report of the whole phone, including a plain-text copy (report.text) you can paste anywhere. You can catch it three ways.
A config function
Config.OnPhoneExamined in shared/config.lua runs on the server with the officer's server ID and the report. It's never stored in the database, so edits apply after a restart.
Config.OnPhoneExamined = function(src, report)
if not report then return end
print(('[evidence] officer %d examined %s'):format(src, report.number))
endshared/config.lua is also sent to every player's game. Don't put webhook URLs or passwords in it. For anything with a secret, use the event below from your own resource.
Server events
These fire on the server only. Players can't trigger them.
| Event | Arguments | When |
|---|---|---|
oxide-drugselling:evidence:phoneExamined | src, report | An officer opens a seized phone |
oxide-drugselling:evidence:numberTraced | src, number, found | An officer traces a number |
oxide-drugselling:evidence:reportSent | src, caseRef, report, ok | An officer presses Send report, whether or not a bridge accepted it |
Example: post examined phones to a Discord channel
This is a tiny resource of your own, so the webhook stays on the server.
Make a folder resources/evidence_log/.
Create evidence_log/fxmanifest.lua:
fx_version 'cerulean'
game 'gta5'
server_script 'server.lua'Create evidence_log/server.lua:
local WEBHOOK = GetConvar('evidence_webhook', '')
AddEventHandler('oxide-drugselling:evidence:phoneExamined', function(src, report)
if WEBHOOK == '' or type(report) ~= 'table' then return end
-- Discord messages are limited to 2000 characters
local body = report.text or ''
if #body > 1800 then body = body:sub(1, 1800) .. '\n...' end
PerformHttpRequest(WEBHOOK, function() end, 'POST', json.encode({
content = ('Seized phone %s examined by %s\n```\n%s\n```'):format(
report.number or '?', GetPlayerName(src) or tostring(src), body),
}), { ['Content-Type'] = 'application/json' })
end)In server.cfg, add your webhook and start the resource with your other scripts:
set evidence_webhook "https://discord.com/api/webhooks/your/webhook"
ensure evidence_logset keeps the value on the server; players can't read it.
Exports
Other server scripts can pull phone evidence whenever they like: BuildEvidenceReport, GetPhoneRecord, GetPhoneMessages, GetPhoneContacts, GetPhoneByNumber, TraceNumber and GetPhoneIdFromItem. See Exports → Phones and evidence.
Layer 3: MDT Bridges
A bridge is one small Lua file in server/integrations/evidence/. That folder isn't protected, so you can open, copy and add files there. Every .lua file in it is loaded when oxide-drugselling starts.
A bridge file registers itself with one call:
EvidenceBackend.Register('resource-name', {
AttachReport = function(src, caseRef, report) ... end, -- required
CreateCase = function(src, report) ... end, -- optional
FlagNumber = function(number, info) ... end, -- optional
})How a bridge is picked
The list in Pressure → Evidence → Evidence bridges (switch the menu to Advanced) decides which bridge gets reports. It's saved in your database as Config.Integrations.evidence:
Config.Integrations = {
evidence = {
{ name = 'oxide-police', enabled = true },
},
}Every time a bridge is needed, the rows are checked from top to bottom. The first row that:
- is switched on (
enabledisn'tfalse), - has a bridge registered under exactly that
name, and - names a resource that's running right now
is used. Only one bridge is used at a time. So the name in the list, the name passed to EvidenceBackend.Register, and the MDT's resource folder name must all be the same.
Because it's checked each time, an MDT that starts or restarts after oxide-drugselling is picked up as soon as it's running.
The list is stored in your database. Editing shared/config/pressure.lua won't change it on a server that has already started once. Add your row in the settings menu.
The contract
Bridge functions are called with plain arguments (no self, no colon). Any error inside them is caught and logged, and the officer sees "Case system error".
AttachReport (required)
Called when an officer presses Send report and types a case number.
| Argument | Type | What it is |
|---|---|---|
src | number | The officer's server ID |
caseRef | string | The case number they typed, in capital letters, 1 to 40 characters |
report | table | The whole phone. See The report |
Return: true when the report was attached. Otherwise false and a reason.
What the officer sees on the phone:
| Your function... | The phone shows |
|---|---|
returns true | "Report attached" |
returns false, 'Case LSPD-0042 not found' (a sentence with spaces) | Your sentence, as written |
returns false and a single word | "Failed". If the word is one of the phone's own error codes, that code's message instead: 'rejected' shows "Not sent" |
returns false with no reason | "Case system error" |
| throws an error | "Case system error" |
The phone screen is small, so keep reasons short.
CreateCase (optional)
CreateCase(src, report) is part of the contract for MDTs that can open a new case from a report. This version of oxide-drugselling never calls it, so leaving it out changes nothing.
FlagNumber (optional)
Called after an officer's trace finds a phone.
| Argument | Type | What it is |
|---|---|---|
number | string | The traced number, like '555-9876' |
info | table | { traced = true, by = <the officer's character identifier> } |
The return value is ignored. Use it to mark the number in your MDT, add it to a watch list, or log it.
The report
AttachReport, Config.OnPhoneExamined, the phoneExamined event and the BuildEvidenceReport export all get the same table:
| Field | Type | What it is |
|---|---|---|
phoneId | string | The phone's hidden ID |
number | string | Its current number |
owner | string|nil | The registered owner's name. Only filled when Names the registered owner (Config.Evidence.trace.showOwner) is on |
contacts | table | A list of { alias, number, deals, persona }. NPC clients have alias, deals and persona; other burners have alias and number (both the number) |
threads | table | Up to 30 conversations, newest first, each { title, thread, messages }. messages is the last 40, oldest first, each { dir = 'in'|'out', text, time } |
meets | table | Booked meets this phone handles, each { alias, time = 'HH:MM', street, type = 'handoff'|'drop', coords = { x, y, z } } |
text | string | Everything above as plain text |
In threads, thread is 'c:<clientId>' for an NPC client, 'n:<number>' for another burner, and 'sys' for network notices (reputation tier changes, SIM swaps). time is the in-game minute of the day the text was sent (0 to 1439), or nil.
A destroyed phone has no report.
Here's what report.text looks like:
TRAP PHONE 555-1234
Registered owner: unknown
Contacts (2):
- 555-9876
- Tasha (3 deals)
Booked meets (1):
- 21:30 Tasha at Grove St / Forum Dr
Messages:
[Tasha]
20:12 <: u around? need 2 grams. got $40
20:13 >: $55
20:13 <: meet me in the middle. $47
20:14 >: ok, $47 works
20:14 <: bet
20:14 <: when can u meet
20:15 >: meet at 21:30
20:15 <: see u at Grove St / Forum Dr. 21:30
[555-9876]
19:40 >: grove tonightThe shipped oxide-police bridge
server/integrations/evidence/oxide_police.lua in the resource folder is the whole bridge for oxide-police:
if GetResourceState('oxide-police') == 'missing' then return end
local police = exports['oxide-police']
EvidenceBackend.Register('oxide-police', {
AttachReport = function(src, caseRef, report)
-- Case entries are a TEXT column (64 KB).
local ok, result = police:AttachCaseEvidence(src, caseRef, report.text:sub(1, 60000))
if ok then return true end
return false, type(result) == 'string' and result or 'rejected'
end,
})- The first line makes the file do nothing on servers without
oxide-police. - It attaches the plain-text report as a case entry.
oxide-policechecks the officer is on duty and the case belongs to their department, and returns its own message ("Case not found", for example) if not. A refusal without a message shows "Not sent". - It needs an
oxide-policeversion with theAttachCaseEvidenceexport. Version 1.9.0 and older don't have it.
Use it as a starting point for your own.
Worked Example: A Bridge For Your MDT
This example connects a made-up MDT called my_mdt. Pretend its documentation says it has two server exports:
AddCaseNote(caseNumber, text, officerSource)returnstrue, orfalseand a reasonFlagPhoneNumber(number, note)
Your MDT's real export names and arguments will be different. Look them up in its documentation first. If it has no export for adding to a case, ask its author; a bridge can only do what the MDT lets other resources do.
Create the file
In oxide-drugselling/server/integrations/evidence/, create my_mdt.lua. Give it its own name; don't edit oxide_police.lua.
Write the bridge
-- Evidence bridge for my_mdt: attaches seized-phone reports to cases and flags traced numbers.
-- Do nothing on servers that don't have my_mdt.
if GetResourceState('my_mdt') == 'missing' then return end
local mdt = exports['my_mdt']
-- my_mdt notes hold up to 10,000 characters.
local NOTE_LIMIT = 10000
EvidenceBackend.Register('my_mdt', {
AttachReport = function(src, caseRef, report)
local text = report.text
if #text > NOTE_LIMIT then
text = text:sub(1, NOTE_LIMIT - 20) .. '\n[report trimmed]'
end
local ok, reason = mdt:AddCaseNote(caseRef, text, src)
if ok then return true end
return false, type(reason) == 'string' and reason or 'The MDT refused the report'
end,
FlagNumber = function(number, info)
mdt:FlagPhoneNumber(number, 'Traced from a seized trap phone')
end,
})What each part does:
GetResourceState('my_mdt') == 'missing'stops the file on servers wheremy_mdtisn't installed.EvidenceBackend.Register('my_mdt', ...)registers the bridge under the MDT's resource name.AttachReporttrims the report to what the MDT can hold, sends it, and passes the MDT's reason back to the officer if it refuses.FlagNumbermarks traced numbers in the MDT.
Switch it on
- In game, as an admin, run
/drugselling settings. - Switch the menu to Advanced, then open Pressure → Evidence.
- Under Evidence bridges, press Add, type
my_mdtas the Resource, and turn Enabled on. - If you also run
oxide-policeand want reports to go tomy_mdt, remove theoxide-policerow or turn it off. Only the first running bridge in the list is used. - Press Save Changes.
Restart
New bridge files are loaded when oxide-drugselling starts, so restart your server. Make sure my_mdt starts before the Oxide resources, with your other scripts (see Installation → Load Order).
In the server console you should see which bridge is active:
[INFO][oxide-drugselling][evidence] evidence bridge
{"active":"my_mdt"}With oxide-logger the line is written in its own format, with the same active value. "active":"none" means no bridge qualified when oxide-drugselling finished starting: check the three rules in How a bridge is picked. This line is only written once, so an MDT that starts after oxide-drugselling shows as none here but is still used once it's running.
Testing Your Bridge
You need two characters (or a second player): one to own the phone, one with an evidence job.
/drugselling kit and use the Trap Phone once. That character is now its registered owner. Text another burner or wait for a client, so there's something to read.FlagNumber). Open a conversation with another burner number, press Enter, choose Trace number. That other phone must be switched on, charged, carried by an online player, and not inside a no-signal area.If the phone shows "Case system error", the server console has an evidence bridge call failed warning with the error from your function.
The oxide-drugselling:evidence:reportSent event fires after every attempt, with ok set to true or false, which is handy for logging.
Tips
- Keep a copy of your bridge file. Updating
oxide-drugsellingreplaces the resource folder. Copy your file back intoserver/integrations/evidence/after updating. - What a bridge file can use: it runs on the server inside
oxide-drugselling, so it hasexports,GetResourceState,MySQL(fromoxmysql),olink(theo-linkbridge),jsonandlocale. - Keep it quick. The officer's phone waits for
AttachReportto finish. - Trim to your MDT's limits. A busy phone's report can be long. The
oxide-policebridge keeps the first 60,000 characters. - Return sentences, not codes, when refusing. A sentence is shown to the officer as written; a single word shows as "Failed" (or as the phone's own message for that word), and no reason at all shows "Case system error".
- Don't use the other functions of
oxide-drugselling. OnlyEvidenceBackend.Registerand the documented exports are meant for bridges.
Next Steps
API Reference
Server exports, server events, config hooks and drug-resource exports for Oxide Drugselling — live settings, heat, phone lookups, evidence reports, orders, and reference tables for callbacks, network events and NUI messages.
Troubleshooting
Fixes for common Oxide Drugselling problems — settings and permissions, street selling, money, police alerts, corners, the trap phone, meets and dead drops, police tools, pricing and progression, and console lines worth knowing.