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.
Exports, events and hooks for developers who want other resources to work with oxide-drugselling. Everything here is server-side.
To send seized-phone reports to your own MDT, see Evidence Bridge.
Server Exports
Settings
GetSetting
Reads a live setting (the value the resource is using right now, from the database).
local selling = exports['oxide-drugselling']:GetSetting('Selling')
print(selling.minPoliceOnline)| Parameter | Type | Description |
|---|---|---|
key | string | A top-level Config key, like 'Selling', 'Heat' or 'Phone' |
Returns: any — the setting's current value, or nil if there's no such key.
SetSetting
Changes a setting, saves it to the database and sends it to every player's game, like saving it in the menu.
exports['oxide-drugselling']:SetSetting('Witnesses', {
enabled = true, radius = 15.0, crowdLimit = 6, snitchChance = 0.05,
})| Parameter | Type | Description |
|---|---|---|
key | string | A top-level Config key |
value | any | The whole new value for that key |
Returns: boolean — true when the setting was saved, false when it was refused.
This export doesn't run the settings menu's checks. Pass the complete value in the right shape: a table replaces the whole setting, not just the fields you include. It refuses, logs and returns false for a key that isn't a setting, for the file-only settings (they stay in the config files), and for a value containing a broken number (NaN or infinity).
Heat
GetHeat
Reads current heat, after decay.
local here = exports['oxide-drugselling']:GetHeat('area', GetEntityCoords(GetPlayerPed(src)))
local dealer = exports['oxide-drugselling']:GetHeat('player', charId)| Parameter | Type | Description |
|---|---|---|
kind | string | 'area', 'corner', 'player' or 'number' |
key | any | For 'area': coords (vector3) or a map-square key like '2:-7'. For 'player': the character identifier. For 'number': a phone number like '555-1234'. For 'corner': 'c:' plus the map-square key of the spot where the corner was started, like 'c:2:-7' |
Returns: number — the heat, from 0 to Config.Heat.max.
AddHeat
Adds (or, with a negative amount, removes) heat. Does nothing while heat is switched off.
-- A shootout nearby draws attention
exports['oxide-drugselling']:AddHeat('area', coords, 10)| Parameter | Type | Description |
|---|---|---|
kind | string | 'area', 'corner', 'player' or 'number' |
key | any | Same as GetHeat |
amount | number | Heat to add. The result stays between 0 and Config.Heat.max |
Returns: boolean — true.
Phones and evidence
GetPhoneByNumber
Finds a trap phone by its number.
local phone = exports['oxide-drugselling']:GetPhoneByNumber('555-1234')
if phone and not phone.burned then
print(phone.phoneId)
end| Parameter | Type | Description |
|---|---|---|
number | string | The phone number, with its dash |
Returns: table|nil — { phoneId = string, number = string, burned = boolean }, or nil if no phone has that number. After a SIM swap, the old number returns nil.
GetPhoneIdFromItem
Gets the phone ID from a trap_phone inventory item.
local phoneId = exports['oxide-drugselling']:GetPhoneIdFromItem(item)| Parameter | Type | Description |
|---|---|---|
item | table | An inventory item row with a metadata table |
Returns: string|nil — the phone ID, or nil for a blank phone that hasn't been used yet.
GetPhoneRecord
The phone's saved details.
local record = exports['oxide-drugselling']:GetPhoneRecord(phoneId)| Parameter | Type | Description |
|---|---|---|
phoneId | string | The phone ID |
Returns: table|nil:
| Field | Type | Description |
|---|---|---|
phoneId | string | The phone ID |
number | string | Its current number |
burned | boolean | true if it was destroyed |
powered | boolean | true if it's switched on |
battery | number | Battery right now, 0 to 100 |
owner | string|nil | The registered owner's name (its first user) |
ownerCharId | string|nil | The registered owner's character identifier |
reputation | number | The phone's reputation |
This returns the owner whatever Config.Evidence.trace.showOwner is set to. Keep it away from players if you want burners to stay anonymous.
GetPhoneMessages
A phone's texts. Reading them this way doesn't mark them as read.
-- Every conversation
local threads = exports['oxide-drugselling']:GetPhoneMessages(phoneId)
-- One conversation
local messages = exports['oxide-drugselling']:GetPhoneMessages(phoneId, { thread = 'n:555-9876' })| Parameter | Type | Description |
|---|---|---|
phoneId | string | The phone ID |
opts | table|nil | { thread = string } for one conversation |
Returns:
- Without
opts.thread: a list of up to 30 conversations, newest first:{ title, thread, messages }. - With
opts.thread: that conversation's last 40 messages, oldest first.
Each message is { dir = 'in'|'out', text = string, time = number|nil }, where time is the in-game minute of the day it was sent (0 to 1439).
Conversation IDs (thread): 'c:<clientId>' for an NPC client, 'n:<number>' for another burner, 'sys' for network notices.
GetPhoneContacts
The phone's contacts, as its Contacts screen shows them.
local contacts = exports['oxide-drugselling']:GetPhoneContacts(phoneId)| Parameter | Type | Description |
|---|---|---|
phoneId | string | The phone ID |
Returns: table — a list sorted by name. NPC clients look like:
{
id = 42, -- client ID
alias = 'Tasha',
kind = 'client', -- 'client' or 'bulk' (undercover buyers also show as 'client')
persona = 'Regular',
trust = 'Good', -- the translated trust label
hours = 'Evenings',
last = 'fire', -- what they thought of the last batch, or ''
deals = 3,
knowsNumber = true,
}Burner numbers the phone has texted look like { number = '555-9876', alias = '555-9876', kind = 'number' }.
TraceNumber
Where a burner number's phone is, blurred like a police trace. No cooldown, and no blip is shown to anyone.
local coords = exports['oxide-drugselling']:TraceNumber('555-9876')| Parameter | Type | Description |
|---|---|---|
number | string | The number to trace |
Returns: vector3|nil — a point within Config.Evidence.trace.radius of the phone, or nil if the phone is off, dead, not carried by an online player, or inside a no-signal area (Config.Phone.indoorsNoSignal doesn't stop a trace).
BuildEvidenceReport
Everything on a phone, as data and as plain text. This is the same report officers send from Evidence View.
local report = exports['oxide-drugselling']:BuildEvidenceReport(phoneId)
if report then
print(report.text)
end| Parameter | Type | Description |
|---|---|---|
phoneId | string | The phone ID |
Returns: table|nil — the report (see Evidence Bridge → The report for every field), or nil if the phone doesn't exist or was destroyed.
Orders
GetActiveOrders
Orders a phone is working on: ones it took, and meets passed to it from another phone.
local orders = exports['oxide-drugselling']:GetActiveOrders(phoneId)
for _, order in ipairs(orders) do
print(order.id, order.status, order.data.quantity, order.data.label)
end| Parameter | Type | Description |
|---|---|---|
phoneId | string | The phone ID |
Returns: table — a list of { id, status, clientId, data }.
status | Meaning |
|---|---|
open | The client texted and is waiting for an answer |
booking | The price is agreed and the client is waiting for a time |
booked | A meet is booked |
dropped | The package is at the dead drop, waiting for the client |
Useful data fields: label (what the client called it), drug, items, quantity, offer (the agreed or current price), bulk (true for bulk buyers), and meet once booked (type = 'handoff' or 'drop', minute = in-game minute of the day, street, coords). A police sting order also has data.sting. Don't show that to players.
Server Events
All events are plain server events (TriggerEvent, not network events), so players can't fire them. Listen with AddEventHandler in any server script.
oxide-drugselling:drugSold
Fired after every completed sale, whichever way it was made, once the sale is settled. For a drop sale that's when the client collects the package; with Left at the drop payment the cash may still be waiting at the drop. Hand-overs to an undercover buyer don't fire it.
AddEventHandler('oxide-drugselling:drugSold', function(src, saleData)
-- saleData.method: 'street' | 'corner' | 'phone' | 'drop' | 'bulk'
print(('%s sold %d x %s for $%d'):format(src, saleData.quantity, saleData.itemName, saleData.finalPrice))
end)saleData field | Type | Description |
|---|---|---|
source | number | The seller's server ID |
charId | string | The seller's character identifier |
drugType | string | The drug's key in Config.Drugs |
itemName | string | The item sold |
quantity | number | Units sold |
xpUnits | number | Progression units for this sale: quantity times the item's xpUnits, or quantity when that row leaves it out |
finalPrice | number | The sale price, after any gang territory bonus. For a dead drop, the cash paid or left at the drop |
metaKey | string | The stack's group key |
mode | string | Config.DirtyMoney.mode ('auto', 'account' or 'item') |
method | string | 'street', 'corner', 'phone', 'drop' or 'bulk' |
For a drop sale, source is the player carrying the phone that handled the meet when the client collected (or, if nobody is, whoever holds the phone that took the order). If neither is online, the sale isn't reported.
oxide-drugselling:evidence:phoneExamined
Fired each time an officer opens a seized phone in Evidence View.
AddEventHandler('oxide-drugselling:evidence:phoneExamined', function(src, report)
-- src: the officer. report: see the Evidence Bridge page
end)oxide-drugselling:evidence:numberTraced
Fired when an officer traces a number from a seized phone.
AddEventHandler('oxide-drugselling:evidence:numberTraced', function(src, number, found)
-- found: true if the trace came back with a location
end)oxide-drugselling:evidence:reportSent
Fired when an officer sends a report from a seized phone, whether or not an MDT accepted it.
AddEventHandler('oxide-drugselling:evidence:reportSent', function(src, caseRef, report, ok)
-- caseRef: the case number the officer typed. ok: true if the MDT bridge accepted it
end)oxide-drugselling:stingCompleted
Fired when a dealer hands over to an undercover buyer at a hand-off meet, or when an undercover buyer collects a dealer's dead drop.
AddEventHandler('oxide-drugselling:stingCompleted', function(orderId, dealerSrc, coords)
-- dealerSrc: the dealer's server ID. For a dead drop it's whoever carries the phone that
-- handled the meet when it was collected, or nil if nobody does. coords: where it happened
end)oxide-drugselling:orderClosed
Fired whenever a phone order ends.
AddEventHandler('oxide-drugselling:orderClosed', function(orderId, status)
-- status: 'done' | 'failed' | 'expired' | 'declined'
end)status | When |
|---|---|
done | Sold: a hand-off, a collected dead drop, or a sting hand-off or drop |
failed | Cancelled, a no-show, a robbery, a drop bag taken by anyone (the dealer included), or the phone was destroyed |
expired | The client gave up waiting, or the player said "Not now" |
declined | The player declined or said "Don't got it", or the client walked after too many counters |
Config Hooks
Two functions in shared/config.lua run on the server and get the same data as the events above. They're never stored in the database, so edits to them apply after a restart.
| Hook | Arguments | Same data as |
|---|---|---|
Config.OnDrugSold | (saleData) | oxide-drugselling:drugSold |
Config.OnPhoneExamined | (src, report) | oxide-drugselling:evidence:phoneExamined |
Both are run safely: an error in your code is logged and doesn't stop the sale or the phone opening. shared/config.lua is also sent to players' games, so keep secrets such as webhook URLs out of it and use the events from your own resource instead.
Config.OnDrugSold = function(saleData)
local turfId = exports['op-crime']:getPlayerTurfZone(saleData.source)
if turfId then
TriggerEvent('op-crime:drugSold', saleData.source, turfId, saleData.finalPrice)
end
endExports Your Drug Resource Can Provide
oxide-drugselling calls these on a drug's own resource when its entry in shared/config/drugs.lua asks for them. If the resource isn't running, the call is skipped and the sale goes through.
GetPlayerLevel
Named by progression.levelExport. Used for requiredLevel, which is checked on every way of selling.
exports('GetPlayerLevel', function(source)
return 3 -- the player's level for this drug
end)Return a number. Anything else counts as level 1. If the export throws an error, a warning is logged and the sale is allowed.
AddXP
Named by progression.addXpExport. Called after every sale with xpPerUnit * quantity * xpUnits, rounded to a whole number. xpUnits comes from the sold item's row and is 1 when the row doesn't set it (see Configuration).
exports('AddXP', function(source, amount)
end)AddSold
Named by progression.addSoldExport. Called after every sale with the number of items sold (quantity), whatever the item's xpUnits.
exports('AddSold', function(source, quantity)
end)GetStrainData (trait_bonus)
Named by a trait_bonus modifier's exportName. Gets the value of the item's sourceField, and returns a table whose traitsPath field holds the traits.
exports('GetStrainData', function(strainId)
return {
traits = { thc = 85, potency = 72 },
}
end)PriceFor (export_multiplier)
Named by an export_multiplier modifier's exportName. oxide-pills provides one called PriceFor.
exports('PriceFor', function(metadata, context)
-- metadata: the item's metadata
-- context: { source = seller, coords = sale position, npcModel = buyer model hash when there is a buyer }
local multiplier = 1.2
local details = {
{ label = 'Club kid', value = 'loves it', bonus = 0.2, color = 'text-emerald-400', type = 'variant' },
}
return multiplier, details -- details are optional tags for the sell menu
end)A missing export, an error, or a return that isn't a number of 0 or more counts as 1.0, and is logged once.
Server Callbacks (Reference)
These are the requests the resource's own interface sends to the server, registered with o-link's callbacks. They're listed so you know what exists; other resources shouldn't call them.
| Callback | Used for | Rate limit |
|---|---|---|
oxide-drugselling:server:getSellableItems | Opening a street sale: checks the buyer and police, returns the sell menu items | 3 s |
oxide-drugselling:server:initSale | A street offer | 5 s |
oxide-drugselling:server:acceptCounter | Accepting a street counter-offer | 5 s |
oxide-drugselling:corners:start / :stop | Starting and stopping work on a corner | 2 s (start) |
oxide-drugselling:corners:arrived / :dismiss | A corner buyer arriving or being sent away | — |
oxide-drugselling:corners:offer / :acceptCounter | A corner offer and counter-offer | 3 s / 5 s |
oxide-drugselling:phone:action | Every button on the phone and in Evidence View | 0.25 s (texts 1.5 s) |
oxide-drugselling:sting:action | Every button on the sting phone | 3 s (posting) |
oxide-drugselling:meet:open / :complete / :cancelPanel | A meet hand-off | 2 s |
oxide-drugselling:meets:mine | The meets for the phones a player carries | — |
oxide-drugselling:drop:open / :place / :take / :cancelPanel | Leaving and taking dead drop bags | 1.5 s |
oxide-drugselling:evidence:extracted | Finishing a timed phone extraction | — |
oxide-drugselling:settings:fetch / :save / :reset | The settings menu (admins only, checked on the server) | — |
The server also asks a player's game three things through client callbacks: oxide-drugselling:client:clock (the in-game time), oxide-drugselling:client:findPoint (a pavement point for a meet) and oxide-drugselling:client:describePoint (the street name at a drop spot).
Network Events (Reference)
The resource's own network events, listed so you know what exists. Other resources shouldn't trigger them.
| Direction | Event | Used for |
|---|---|---|
| Client → server | oxide-drugselling:server:syncNpcAction | Asks the server to replay a buyer's movement or animation on nearby players' games. Only NPCs the sender controls and stands next to are relayed, at most 30 a second, with a payload the server rebuilds |
| Server → client | oxide-drugselling:client:replayNpcAction | The replay itself, sent to players within 75 m of the seller |
| Client → server | oxide-drugselling:phone:quickOpen | The Open trap phone key (rate limit 1 s) |
| Client → server | oxide-drugselling:phone:indoors | Whether the player is inside an interior, for No signal indoors |
| Client → server | oxide-drugselling:drops:resync | A player who just loaded asks for their drop list again |
| Server → client | oxide-drugselling:drops:list | The dead drop bags and cash within Visible within + 40 m of that player, sent when that set changes |
| Server → client | oxide-drugselling:client:dispatchRelay | A server-built police alert, handed to a police officer's game for a dispatch resource that only takes alerts there |
| Server → client | oxide-drugselling:corners:buyer / :buyerGone / :ended | A corner buyer is on the way, left, or the corner closed |
| Server → client | oxide-drugselling:meet:spawn / :despawn | A meet client arrives or leaves |
| Server → client | oxide-drugselling:phone:open / :push / :ring / :charging | The phone screen, a change to refresh, a ring nearby, a battery pack in use |
| Server → client | oxide-drugselling:evidence:extract / :trace | A timed extraction, a trace on the map |
| Server → client | oxide-drugselling:sting:heatmap | The sting phone's heat map |
| Server → client | oxide:drugselling:openSettings | Opens the settings menu, sent by /drugselling settings, corners and drops |
| Client → server | oxide-drugselling:modelUnavailable | A model the player's game couldn't load, passed on to online admins |
NUI Messages (Reference)
For reference when reading the interface. Other resources don't need these.
| Direction | Name | Purpose |
|---|---|---|
| Lua → UI | openSellPanel | Opens the sell menu: { items, isGangNpc, panelTitle, bands, request?, deal? } |
| Lua → UI | sellThinking / sellResult / close | The waiting state, the outcome, and closing |
| Lua → UI | phoneOpen / phonePush / phoneClose | Opening the phone, a change to refresh, closing |
| Lua → UI | phonePeek | A text that arrived while the phone was put away |
| Lua → UI | markers | The dead drop markers to draw on screen |
| Lua → UI | openSettings / closeSettings | The settings menu |
| UI → Lua | sellConfirm / sellAcceptCounter / sellClose | Sell menu buttons |
| UI → Lua | phoneAction / phoneClose / phoneClock / phoneGps / phoneMove | Phone buttons, closing, the clock, setting a waypoint, walking while the phone is open |
| UI → Lua | saveSettings / resetSetting / closeSettings / getItemCatalog / startPlacement / startZoneEdit / teleportTo | Settings menu actions |
| UI → Lua | getLocale | Returns { locale } from the ox:locale convar |
Integration Examples
Pay a gang for sales on their turf
my-turfs stands for your own turf resource and its exports.
AddEventHandler('oxide-drugselling:drugSold', function(src, saleData)
if saleData.method == 'phone' or saleData.method == 'drop' then return end
local coords = GetEntityCoords(GetPlayerPed(src))
local turfId = exports['my-turfs']:GetTurfAt(coords)
if turfId then
exports['my-turfs']:AddTurfIncome(turfId, math.floor(saleData.finalPrice * 0.1))
end
end)Raise heat when shots are fired
my-shotspotter:shotsFired stands for any server event your server fires with a position.
AddEventHandler('my-shotspotter:shotsFired', function(coords)
exports['oxide-drugselling']:AddHeat('area', coords, 15)
end)Look up a number from your own police tool
local function DescribeNumber(number)
local phone = exports['oxide-drugselling']:GetPhoneByNumber(number)
if not phone then return 'No such number' end
if phone.burned then return 'Phone destroyed' end
local record = exports['oxide-drugselling']:GetPhoneRecord(phone.phoneId)
return ('Battery %d%%, %s'):format(math.floor(record.battery), record.powered and 'on' or 'off')
endAdding your own drug
- Register your item names in the inventory
o-linkuses. - Put metadata on the items that matches your
Config.Drugsentry. - Add progression exports if you want level gating and XP.
- Add the drug to
shared/config/drugs.lua, and restart. See Configuration → Adding your own drug.
Next Steps
Admin Guide
The /drugselling admin commands (settings, corners, drops, kit, heat), the /corner and /trapphone player commands, and how admin permissions work in Oxide Drugselling.
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.