API Reference

Server exports, server events, the client gang statebag, and integration examples for developers building on Oxide Gangs.

This page is for developers integrating another resource with oxide-gangs. Every export listed here is server-side and verified against the resource manifest.

If you only run the resource, you don't need anything here — this is for scripting.

Which API to use

oxide-gangs registers itself as the gang provider inside o-link, so the bridge is the API you should be calling:

olink = exports['o-link']:olink()

local gang = olink.gang.Get(source)

Going through o-link means your resource keeps working on servers that do not run oxide-gangs — the bridge falls back to whatever gang system the framework provides, or to a safe stub. Calling exports['oxide-gangs'] directly makes your resource hard-depend on it.

Every export below is exposed through o-link under the same name in the gang namespace.

Detecting the full backend

Territories, wars, and influence reporting only exist when oxide-gangs is the provider. The generic gang stubs are callable, so olink.supports('gang.GetTerritories') cannot tell you whether a real backend is behind them. Check which resource owns the namespace instead:

local function GangsAvailable()
    -- Resource-state check first, so gang-less servers never touch the bridge.
    if GetResourceState('oxide-gangs') == 'missing' then return false end
    local getName = olink.gang and olink.gang.GetResourceName
    if getName == nil then return false end
    -- Snapshotted functions cross the resource boundary as funcref tables,
    -- so a type() == 'function' check always fails. pcall instead.
    local ok, name = pcall(getName)
    return ok and name == 'oxide-gangs'
end

For the simpler calls (Get, GetAll, HasPermission, treasury) you do not need this — they work against any gang backend o-link supports.

Server Exports

Membership

Get

The gang a connected player belongs to.

local gang = exports['oxide-gangs']:Get(source)
-- or, preferred:
local gang = olink.gang.Get(source)

Parameters:

NameTypeDescription
srcnumberThe player's server ID

Returns: table|nilnil if the player is in no gang, otherwise:

FieldTypeDescription
namestringInternal gang name, e.g. ballas
labelstringDisplay name, e.g. The Ballas
gradestringInternal rank name, e.g. enforcer
gradeLabelstringDisplay rank name, e.g. Enforcer
ranknumberNumeric rank

GetMemberByCharId

The same lookup keyed by character ID instead of server ID. This is the one that works for offline characters.

local member = olink.gang.GetMemberByCharId(charId)

Parameters:

NameTypeDescription
charIdstringThe character identifier, as returned by olink.character.GetIdentifier

Returns: table|nilnil if that character is in no gang, otherwise everything Get returns plus:

FieldTypeDescription
isBossbooleanWhether their rank is a boss rank
charIdstringThe character identifier
memberNamestringTheir cached display name in the roster
onlinebooleanWhether they are connected right now
sourcenumber|nilTheir server ID if online

IsGangMember

local inAnyGang   = olink.gang.IsGangMember(source)
local inTheBallas = olink.gang.IsGangMember(source, 'ballas')

Parameters:

NameTypeDescription
srcnumberThe player's server ID
gangNamestring|nilInternal gang name. Pass nil to ask "are they in any gang at all?"

Returns: boolean


HasPermission

Whether a player's rank holds a given gang permission. Boss ranks return true for everything.

if olink.gang.HasPermission(source, 'stash') then
    -- ...
end

Parameters:

NameTypeDescription
srcnumberThe player's server ID
permstringOne of invite, kick, promote, demote, manage_ranks, withdraw, deposit, stash, garage, war, announce, edit_gang

Returns: boolean


Set

Puts a player in a gang, moves them between gangs, changes their rank, or removes them.

olink.gang.Set(source, 'ballas', nil, nil, nil, 2)   -- join/move to ballas at rank 2
olink.gang.Set(source, nil)                          -- remove from their gang

Parameters:

NameTypeDescription
srcnumberThe player's server ID
gangNamestring|nilInternal gang name. nil, 'none', or '' removes them from their gang
gangLabelstring|nilIgnored — the registry owns labels
gradeNamestring|nilIgnored — the registry owns rank names
gradeLabelstring|nilIgnored
gradeRanknumber|nilRank to seat them at. Defaults to 0

Returns: booleanfalse if the gang does not exist, that rank does not exist in it, the gang is full, or the player has no character loaded.

The gang must already exist in the registry. This never creates one. Labels and rank names are ignored on purpose: the registry is the single source of truth for those.


Gangs and Ranks

GetAll

Every active gang.

for _, gang in ipairs(olink.gang.GetAll()) do
    print(gang.name, gang.label, gang.memberCount)
end

Returns: table[], sorted by internal name:

FieldTypeDescription
idnumberThe gang's numeric ID
namestringInternal name
labelstringDisplay name
colorstringHex colour, e.g. #e11d48
mottostring|nilThe gang's motto
memberCountnumberHow many members it has

GetMembers

A gang's full roster, online and offline.

local roster = olink.gang.GetMembers('ballas')

Parameters:

NameTypeDescription
gangNamestringInternal gang name

Returns: table[], sorted by rank descending then by name. Empty if the gang does not exist.

FieldTypeDescription
charIdstringCharacter identifier
namestring|nilCached display name
ranknumberNumeric rank
gradeLabelstringDisplay rank name
isBossbooleanWhether their rank is a boss rank
joinedAtstringWhen they joined
invitedBystring|nilCharacter ID of whoever brought them in
onlinebooleanWhether they are connected right now

GetGrades

A gang's rank ladder.

local grades = olink.gang.GetGrades('ballas')

Parameters:

NameTypeDescription
gangNamestringInternal gang name

Returns: table[], sorted by rank ascending. Empty if the gang does not exist.

FieldTypeDescription
ranknumberNumeric rank
namestringInternal rank name
labelstringDisplay rank name
isBossbooleanWhether this is a boss rank
permissionstableMap of permission name to true

Treasury

GetTreasury

local balance = olink.gang.GetTreasury('ballas')

Parameters:

NameTypeDescription
gangNamestringInternal gang name

Returns: number — the balance, or 0 if the gang does not exist.


AddToTreasury

olink.gang.AddToTreasury('ballas', 5000, 'protection racket payout')

Parameters:

NameTypeDescription
gangNamestringInternal gang name
amountnumberA positive whole number. Zero and negatives are rejected
descriptionstring|nilShows in the gang's transaction history. Defaults to 'external'

Returns: boolean


RemoveFromTreasury

olink.gang.RemoveFromTreasury('ballas', 2500, 'territory upkeep')

Parameters:

NameTypeDescription
gangNamestringInternal gang name
amountnumberA positive whole number — the amount to remove. Zero and negatives are rejected
descriptionstring|nilShows in the gang's transaction history. Defaults to 'external'

Returns: booleanfalse if the gang does not exist or the treasury does not have that much. This is a safe debit gate: a false return means no money moved, so you can branch on it.


Territory

Only available when oxide-gangs is the gang provider — see Detecting the full backend.

GetTerritories

Every enabled territory.

for _, t in ipairs(olink.gang.GetTerritories()) do
    print(t.label, t.owner and t.owner.label or 'unclaimed')
end

Returns: table[], sorted by ID:

FieldTypeDescription
idnumberTerritory ID
namestringInternal name, e.g. textile_city
labelstringDisplay name, e.g. Textile City
colorstringHex colour
centertable{ x, y, z } — the centre of the zone
ownertable|nil{ name, label, color } of the owning gang, or nil if unclaimed
contestedbooleanWhether a contest window is open
contestEndsAtnumber|nilEpoch seconds when that contest settles
modifierstable|nil{ yield, growth, price, buyerDensity }
standingstable[]Top five gangs by influence: { gangId, name, label, color, influence }

Zone polygons are deliberately not exposed. Use GetTerritoryAt to test a point.


GetTerritoryAt

The territory containing a world position, or nil.

local coords = GetEntityCoords(GetPlayerPed(source))
local territory = olink.gang.GetTerritoryAt(coords)

Parameters: accepts either a coordinate value or three numbers:

NameTypeDescription
xtable|vector3|vector4|numberA coordinate value, or the X component
ynumber|nilY, if you passed numbers
znumber|nilZ, if you passed numbers. Optional — height is only checked on zones that recorded height bounds

Returns: table|nil — the same shape as one entry from GetTerritories.


GetTerritoryOwner

local ownerName = olink.gang.GetTerritoryOwner(3)

Parameters:

NameTypeDescription
territoryIdnumberTerritory ID

Returns: string|nil — the owning gang's internal name, or nil if unclaimed or the territory does not exist.


Wars

GetWars

Active wars, plus the fifteen most recently ended.

for _, war in ipairs(olink.gang.GetWars()) do
    print(war.attacker.label, 'vs', war.defender.label, war.status)
end

Returns: table[] — active wars first, newest started first, then history:

FieldTypeDescription
idnumberWar ID
statusstring'active' or 'ended'
attackertable{ id, label, color }
defendertable{ id, label, color }
stakesnumberStake per side
declaredAtnumber|nilEpoch seconds
startedAtnumber|nilEpoch seconds
endsAtnumber|nilEpoch seconds
endedAtnumber|nilEpoch seconds
endedReasonstring|nil'clock', 'ceasefire', 'disband', or 'admin'
winnerIdnumber|nilGang ID of the winner. nil on a draw
winnerstring|nilWinner's display name
scorestable{ attacker, defender } team totals

Member-level data is never included — no character IDs, no per-player scores.


GetActiveWar

local war = olink.gang.GetActiveWar('ballas')

Parameters:

NameTypeDescription
gangNamestringInternal gang name

Returns: table|nil — that gang's active war in the same shape as a GetWars entry, or nil if it is not at war.


Reporting Influence

ReportDrugSale

Feeds a completed drug sale into territory influence for the seller's gang, in the zone where the sale physically happened.

local applied = olink.gang.ReportDrugSale(source, saleCoords, totalPrice)

Parameters:

NameTypeDescription
srcnumberThe seller's server ID
coordstable|vector3|vector4Where the sale happened
valuenumberThe sale value in dollars

Returns: number — the influence actually applied. 0 when the seller is in no gang, the sale was outside every territory, or the gang has hit its hourly cap.

Influence is value × Config.Territory.saleGainPerDollar, then run through the hourly caps. You do not need to check whether the seller is in a gang or standing in a territory first — this handles all of it.


ReportActivity

The general-purpose version, for anything that is not a drug sale: robberies, rackets, graffiti, protection runs, whatever your resource does.

local applied = olink.gang.ReportActivity(source, 'robbery', coords, 8.0)

Parameters:

NameTypeDescription
srcnumberThe acting player's server ID
activityTypestringA label for your logs. Up to 32 characters, letters, numbers, _ and - only
coordstable|vector3|vector4Where the activity happened
amountnumberInfluence to report. Must be positive and finite

Returns: number — the influence actually applied. 0 when the player is in no gang, the coordinates are outside every territory, the arguments are invalid, or the cap is reached.

Every external report shares one cap — Config.Territory.hourlyCap.external. The activityType only labels the entry in the server owner's logs; it does not get its own budget. This is deliberate, so a server owner has a single knob controlling total third-party influence inflow no matter how many scripts are feeding it.

Server Events

Listen with AddEventHandler. These are local server events, not net events — they are fired on the server only.

oxide:gangs:server:initialized

Fired once when oxide-gangs has finished loading everything. Wait for this if your resource needs the registry to be ready at boot.

AddEventHandler('oxide:gangs:server:initialized', function()
    -- gangs, members, bases, territories and wars are all loaded
end)

oxide:gangs:server:memberChanged

Fired whenever a character's gang membership changes — join, leave, kick, rank change, disband. Fires for offline characters too, which makes it the right hook for invalidating a character-keyed cache.

AddEventHandler('oxide:gangs:server:memberChanged', function(charId, gangName, rank, reason, src)
    -- charId   : string  - the character identifier
    -- gangName : string|nil - the gang they are now in, or nil if they left
    -- rank     : number|nil - their new rank, or nil if they left
    -- reason   : string  - 'join', 'grade', 'kick', 'leave', 'disband' or 'admin'
    -- src      : number|nil - their server ID if they are online
end)

oxide:gangs:server:gangsChanged

Fired when the gang registry itself changes.

AddEventHandler('oxide:gangs:server:gangsChanged', function(reason, payload)
    -- reason  : 'created' | 'updated' | 'disbanded' | 'gradeChanged' | 'gradeRemoved'
    -- payload : table - { gangId, name } on created/disbanded,
    --                   { gangId } on updated,
    --                   { gangId, rank } on grade changes
end)

oxide:gangs:server:territoryContested

AddEventHandler('oxide:gangs:server:territoryContested', function(territoryId, challengerGangId)
    -- a rival overtook the owner; a contest window just opened
end)

oxide:gangs:server:territoryFlipped

AddEventHandler('oxide:gangs:server:territoryFlipped', function(territoryId, newOwnerGangId)
    -- a contest settled and the territory changed hands
end)

oxide:gangs:server:warDeclared

AddEventHandler('oxide:gangs:server:warDeclared', function(war)
    -- war: the same shape as a GetWars entry, status 'pending'
end)

oxide:gangs:server:warStarted

AddEventHandler('oxide:gangs:server:warStarted', function(war)
    -- war: the same shape as a GetWars entry, status 'active'
end)

oxide:gangs:server:warEnded

AddEventHandler('oxide:gangs:server:warEnded', function(war)
    -- war.endedReason : 'clock' | 'ceasefire' | 'disband' | 'admin'
    -- war.winnerId    : gang ID, or nil on a draw
end)

oxide:gangs:server:baseActivity

Fired when a member uses their gang base — opening the stash, or taking out / returning a vehicle. oxide-gangs listens to this itself to award base-activity influence, and your resource can trigger it too if you add your own base interaction.

-- listening
AddEventHandler('oxide:gangs:server:baseActivity', function(gangId, charId, coords) end)

-- triggering, from your own base feature
TriggerEvent('oxide:gangs:server:baseActivity', gangId, charId, coords)

olink:server:gangChanged

Fired by oxide-gangs through the bridge whenever a connected player's gang changes. Prefer this over memberChanged when you only care about online players.

It also fires with the player's current gang (or nil if they have none) when their character loads and when the resource restarts — not only on an actual change. Do not treat receiving it as proof something changed; compare against your own stored state if that matters.

AddEventHandler('olink:server:gangChanged', function(src, gangName)
    -- gangName is nil when they left their gang (or have none at login)
end)

Client State

The gang statebag

A connected player's gang rides on their player statebag under the key oxide:gang. The server is the only writer; clients read it.

local gang = LocalPlayer.state['oxide:gang']
if gang then
    print(gang.label, gang.gradeLabel, gang.isBoss)
end

The value is nil when the player is in no gang, and can also be nil briefly while the resource restarts. Treat nil as "no gang".

FieldTypeDescription
idnumberGang ID
namestringInternal gang name
labelstringDisplay name
colorstringHex colour
gradestringInternal rank name
gradeLabelstringDisplay rank name
ranknumberNumeric rank
isBossbooleanWhether their rank is a boss rank
permissionstableMap of permission name to true

Never trust the statebag for a server-side permission decision

It is a display convenience. Ask the server with HasPermission.

The territory list

Enabled territories are published to every client on GlobalState, without polygons.

local territories = GlobalState['oxide-gangs:territories']
-- table[] of { id, label, color, center, blip, owner, contested }

Client Events

olink:client:gangChanged

Fired on the local client when this player's gang changes, including at login.

AddEventHandler('olink:client:gangChanged', function(gang)
    -- gang: the statebag payload above, or nil if they left / have no gang
end)

Integration Examples

Give a gang a cut of a heist

olink = exports['o-link']:olink()

local function PayGangCut(source, amount)
    local gang = olink.gang.Get(source)
    if not gang then return false end
    return olink.gang.AddToTreasury(gang.name, amount, 'heist cut')
end

Charge a gang for something, safely

RemoveFromTreasury returns false without moving money if the gang cannot afford it, so it works as the payment gate itself:

local function BuySupplies(gangName, cost)
    if not olink.gang.RemoveFromTreasury(gangName, cost, 'supply drop') then
        return false, 'insufficient_treasury'
    end
    -- money is gone; deliver the goods
    return true
end

Gate a door on gang membership and rank

local function CanOpenSafehouse(source)
    if not olink.gang.IsGangMember(source, 'ballas') then return false end
    local gang = olink.gang.Get(source)
    return gang and gang.rank >= 2
end

Apply a territory's economy modifier

Every zone carries yield, growth, price, and buyerDensity. Default to 1.0 so your resource behaves identically on servers with no gang system:

local function TerritoryMultiplier(coords, key)
    local t = olink.gang.GetTerritoryAt(coords)
    local m = t and t.modifiers
    local v = m and tonumber(m[key])
    return (v and v > 0) and v or 1.0
end

local payout = basePayout * TerritoryMultiplier(saleCoords, 'price')

Feed your own activity into territory influence

-- After a successful store robbery
local applied = olink.gang.ReportActivity(source, 'store_robbery', GetEntityCoords(GetPlayerPed(source)), 10.0)
if applied > 0 then
    olink.notify.Send(source, ('Your crew gained ground here (+%.1f)'):format(applied), 'success')
end

Only show a feature when the full backend is present

local function GangsAvailable()
    if GetResourceState('oxide-gangs') == 'missing' then return false end
    local getName = olink.gang and olink.gang.GetResourceName
    if getName == nil then return false end
    local ok, name = pcall(getName)
    return ok and name == 'oxide-gangs'
end

if GangsAvailable() then
    -- register the territory tab, the turf bonus, etc.
end

React to turf changing hands

AddEventHandler('oxide:gangs:server:territoryFlipped', function(territoryId, gangId)
    local owner = olink.gang.GetTerritoryOwner(territoryId)
    print(('Territory %d is now held by %s'):format(territoryId, owner or 'nobody'))
end)

Notes

  • Every export listed here is server-side. oxide-gangs exposes no client exports.
  • The resource also registers callbacks named oxide-gangs:server:dashboard:* and oxide-gangs:server:admin:*. Those are internal to its own user interface, re-check permissions on every call, and are not a supported integration surface. Do not call them from another resource.
  • Coordinates cross the export boundary as vector3/vector4 values, not plain tables. Anything on this page that takes coordinates accepts a table, a vector3, or a vector4, so you can pass GetEntityCoords results straight in.

Next Steps