Evidence Bridge Guide

How phone evidence reaches your police paperwork — the built-in Forensics layer, the evidence report event, report destinations that attach reports to your own MDT, your own search authorizations, a worked example and testing.

How phone evidence reaches your police paperwork. This guide is for server owners and developers who want extraction reports to land in their own MDT (the police computer where cases live), or who want to decide search authorizations with their own system.

If you run oxide-police, you don't need this guide: its bridge ships with the phone and is on by default.

The Three Layers

LayerWhat it gives youWhat you need
1. Built inSeizure, search authorizations, extraction, reports, carrier records and traces in the Forensics appNothing. Works on every server
2. Events and exportsYour own code gets every reportA small resource of your own
3. Report destinationsEvery report is also attached to a case in your MDTA few lines in a resource of your own

Each layer works without the ones below it.

Layer 1: Built In

Police (the jobs in Evidence → Police jobs of /phone settings) who open a phone registered to someone else see its number, who its SIM is registered to and its custody state on the lock screen, never its content. The phone is logged as seized and stops getting calls and texts while police hold it.

In the Forensics app, police:

  • see the phones they carry, and lodge, return or destroy them;
  • issue and revoke search authorizations (only Authorizers);
  • extract a phone they carry, when a search authorization covers it. The report is kept in Forensics and can be copied as plain text;
  • read carrier records for a number (calls and texts, with the area each party was in, never content);
  • trace a number that is switched on right now to a rough area on the map.

Deleted texts, contacts, calls, mail, photos and notes are still found by an extraction for Recover deleted content for days. A secure wipe (the phone_wiper item) defeats that. Carrier records are kept per number, so they survive a wiped or destroyed phone.

Layer 2: Events and Exports

This server event fires once a report is stored, before it goes to any destination. Players can't trigger it.

EventArgumentsWhen
oxide:phone:server:evidenceReportreportId, reportAn extraction finished
AddEventHandler('oxide:phone:server:evidenceReport', function(reportId, report)
    print(('[evidence] report %d for case %s'):format(reportId, report.caseRef))
end)

Layer 3: Report Destinations

A destination is registered from your own server resource with one export:

exports['oxide-phone']:RegisterEvidenceBackend('my-mdt', {
    AttachReport = function(src, caseRef, report) ... end,   -- required
    FlagNumber   = function(number, info) ... end,           -- optional
})

Only AttachReport works the way it does in oxide-drugselling's evidence bridges: the same arguments, and true, or false and a reason, back. An AttachReport that only reads report.text works with both. The rest is different:

  • Registering. A phone destination registers with this export from a resource of your own. An oxide-drugselling bridge is a file inside oxide-drugselling's server/integrations/evidence/ folder.
  • Who gets a report. The phone sends each report to every running destination in its list. oxide-drugselling sends to the first one only.
  • FlagNumber. The phone's info has found, and by is the officer's name. oxide-drugselling's has no found, and by is the officer's character identifier.
  • The report. Each resource sends its own report table, with different fields. Both have text.

The contract

FunctionArgumentsReturn
AttachReportsrc (the officer's server ID), caseRef (what the officer typed), reporttrue, or false and a short reason. The reason is shown to the officer in Forensics
FlagNumbernumber, info ({ traced = true, found, by }: found is whether the trace located the phone, by the officer's name)nothing; called when an officer traces a number

A destination that errors or returns false never loses the report: it stays in Forensics, the failure is recorded on it, and the officer can press Send again.

The report

FieldContents
kind'phone', or 'trap' for an oxide-drugselling trap phone
caseRefThe officer's case reference, upper case
officerThe officer's name
authorizationWhat authorized the search, for example #12 LSPD-2026-0042 or Warrant #7
atWhen, in milliseconds since 1970
device{ deviceId, number, simRegistered, simOwner, deviceOwner, custody, powered, wiped }
recoveryDaysThe recovery window the extraction used
contacts, threads, calls, mail, media, notesEverything found, including deleted items still inside the recovery window. Messages in threads carry deleted = true; the other rows carry the time they were deleted in deleted_at
aliases, notifications, shares, albums, voicemails, blocks, places, feedMail aliases, stored notifications, nearby shares, Photos albums, voicemail, blocked numbers, saved places and Feed posts, under the same recovery window
appsWhat the phone did in apps from other resources (such as Hookr, Leakd and Glitch), one section per app
darkchatRecovered Dark Chat messages (room, handle, body, created_at). Empty unless Dark Chat recovery is above 0
custodyThe custody log, oldest first (action, actor, caseRef, note, at)
textA plain-text copy of all of it. The copy a destination receives is cut to at most 60,000 bytes; Forensics keeps the full text

Most MDTs only need report.text.

A 'trap' report is built by oxide-drugselling and has its own content fields. It always has kind, caseRef, officer, authorization, at, device and text.

Turning a destination on

Destinations are listed in Evidence → Report destinations of /phone settings. The name must be the name of the resource that registers it, and that resource must be running. oxide-police is in the list by default.

Your Own Search Authorizations

Set Evidence → Search authorization to Hook and register a check from your own server resource:

exports['oxide-phone']:SetAuthorizationCheck(function(src, target)
    -- target = { deviceId?, number?, ownerCharId? }
    local warrant = MyCourts.FindWarrant(target.ownerCharId)
    if warrant then return true, ('Warrant %s'):format(warrant.id) end
    return false
end)

Return true and, optionally, a label for the report (up to 80 characters). The check is cleared when your resource stops. While the mode is Hook and no check is registered, every search is refused.

target.ownerCharId is the character the phone's SIM is registered to, or, without a registered SIM, the phone's registered owner.

With Built in, an active oxide-police warrant for that same character also counts, when oxide-police runs.

Worked Example: A Destination For Your MDT

Make a folder resources/phone_mdt_bridge/.

Create phone_mdt_bridge/fxmanifest.lua:

phone_mdt_bridge/fxmanifest.lua
fx_version 'cerulean'
game 'gta5'
server_script 'server.lua'
dependency 'oxide-phone'

Create phone_mdt_bridge/server.lua (replace my-mdt and its export with your MDT's):

phone_mdt_bridge/server.lua
exports['oxide-phone']:RegisterEvidenceBackend('phone_mdt_bridge', {
    AttachReport = function(src, caseRef, report)
        if GetResourceState('my-mdt') ~= 'started' then return false, 'MDT offline' end
        local ok = exports['my-mdt']:AddCaseNote(caseRef, report.text)
        if not ok then return false, 'Case not found' end
        return true
    end,
})

Start it after the phone in server.cfg:

server.cfg
ensure oxide-phone
ensure phone_mdt_bridge

In /phone settings → Evidence → Report destinations, add a row named phone_mdt_bridge.

Testing

As an officer on duty, carry someone else's phone and open it: the lock screen shows Evidence.

As an authorizer, issue an authorization for that phone's number in Forensics → Authorizations.

In Forensics → Phones, extract it with a case reference that exists in your MDT.

Forensics → Reports shows the report and, for each destination, Delivered or the reason it was not.