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'
endFor 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:
| Name | Type | Description |
|---|---|---|
| src | number | The player's server ID |
Returns: table|nil — nil if the player is in no gang, otherwise:
| Field | Type | Description |
|---|---|---|
name | string | Internal gang name, e.g. ballas |
label | string | Display name, e.g. The Ballas |
grade | string | Internal rank name, e.g. enforcer |
gradeLabel | string | Display rank name, e.g. Enforcer |
rank | number | Numeric 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:
| Name | Type | Description |
|---|---|---|
| charId | string | The character identifier, as returned by olink.character.GetIdentifier |
Returns: table|nil — nil if that character is in no gang, otherwise everything Get returns plus:
| Field | Type | Description |
|---|---|---|
isBoss | boolean | Whether their rank is a boss rank |
charId | string | The character identifier |
memberName | string | Their cached display name in the roster |
online | boolean | Whether they are connected right now |
source | number|nil | Their server ID if online |
IsGangMember
local inAnyGang = olink.gang.IsGangMember(source)
local inTheBallas = olink.gang.IsGangMember(source, 'ballas')Parameters:
| Name | Type | Description |
|---|---|---|
| src | number | The player's server ID |
| gangName | string|nil | Internal 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
-- ...
endParameters:
| Name | Type | Description |
|---|---|---|
| src | number | The player's server ID |
| perm | string | One 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 gangParameters:
| Name | Type | Description |
|---|---|---|
| src | number | The player's server ID |
| gangName | string|nil | Internal gang name. nil, 'none', or '' removes them from their gang |
| gangLabel | string|nil | Ignored — the registry owns labels |
| gradeName | string|nil | Ignored — the registry owns rank names |
| gradeLabel | string|nil | Ignored |
| gradeRank | number|nil | Rank to seat them at. Defaults to 0 |
Returns: boolean — false 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)
endReturns: table[], sorted by internal name:
| Field | Type | Description |
|---|---|---|
id | number | The gang's numeric ID |
name | string | Internal name |
label | string | Display name |
color | string | Hex colour, e.g. #e11d48 |
motto | string|nil | The gang's motto |
memberCount | number | How many members it has |
GetMembers
A gang's full roster, online and offline.
local roster = olink.gang.GetMembers('ballas')Parameters:
| Name | Type | Description |
|---|---|---|
| gangName | string | Internal gang name |
Returns: table[], sorted by rank descending then by name. Empty if the gang does not exist.
| Field | Type | Description |
|---|---|---|
charId | string | Character identifier |
name | string|nil | Cached display name |
rank | number | Numeric rank |
gradeLabel | string | Display rank name |
isBoss | boolean | Whether their rank is a boss rank |
joinedAt | string | When they joined |
invitedBy | string|nil | Character ID of whoever brought them in |
online | boolean | Whether they are connected right now |
GetGrades
A gang's rank ladder.
local grades = olink.gang.GetGrades('ballas')Parameters:
| Name | Type | Description |
|---|---|---|
| gangName | string | Internal gang name |
Returns: table[], sorted by rank ascending. Empty if the gang does not exist.
| Field | Type | Description |
|---|---|---|
rank | number | Numeric rank |
name | string | Internal rank name |
label | string | Display rank name |
isBoss | boolean | Whether this is a boss rank |
permissions | table | Map of permission name to true |
Treasury
GetTreasury
local balance = olink.gang.GetTreasury('ballas')Parameters:
| Name | Type | Description |
|---|---|---|
| gangName | string | Internal gang name |
Returns: number — the balance, or 0 if the gang does not exist.
AddToTreasury
olink.gang.AddToTreasury('ballas', 5000, 'protection racket payout')Parameters:
| Name | Type | Description |
|---|---|---|
| gangName | string | Internal gang name |
| amount | number | A positive whole number. Zero and negatives are rejected |
| description | string|nil | Shows in the gang's transaction history. Defaults to 'external' |
Returns: boolean
RemoveFromTreasury
olink.gang.RemoveFromTreasury('ballas', 2500, 'territory upkeep')Parameters:
| Name | Type | Description |
|---|---|---|
| gangName | string | Internal gang name |
| amount | number | A positive whole number — the amount to remove. Zero and negatives are rejected |
| description | string|nil | Shows in the gang's transaction history. Defaults to 'external' |
Returns: boolean — false 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')
endReturns: table[], sorted by ID:
| Field | Type | Description |
|---|---|---|
id | number | Territory ID |
name | string | Internal name, e.g. textile_city |
label | string | Display name, e.g. Textile City |
color | string | Hex colour |
center | table | { x, y, z } — the centre of the zone |
owner | table|nil | { name, label, color } of the owning gang, or nil if unclaimed |
contested | boolean | Whether a contest window is open |
contestEndsAt | number|nil | Epoch seconds when that contest settles |
modifiers | table|nil | { yield, growth, price, buyerDensity } |
standings | table[] | 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:
| Name | Type | Description |
|---|---|---|
| x | table|vector3|vector4|number | A coordinate value, or the X component |
| y | number|nil | Y, if you passed numbers |
| z | number|nil | Z, 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:
| Name | Type | Description |
|---|---|---|
| territoryId | number | Territory 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)
endReturns: table[] — active wars first, newest started first, then history:
| Field | Type | Description |
|---|---|---|
id | number | War ID |
status | string | 'active' or 'ended' |
attacker | table | { id, label, color } |
defender | table | { id, label, color } |
stakes | number | Stake per side |
declaredAt | number|nil | Epoch seconds |
startedAt | number|nil | Epoch seconds |
endsAt | number|nil | Epoch seconds |
endedAt | number|nil | Epoch seconds |
endedReason | string|nil | 'clock', 'ceasefire', 'disband', or 'admin' |
winnerId | number|nil | Gang ID of the winner. nil on a draw |
winner | string|nil | Winner's display name |
scores | table | { 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:
| Name | Type | Description |
|---|---|---|
| gangName | string | Internal 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:
| Name | Type | Description |
|---|---|---|
| src | number | The seller's server ID |
| coords | table|vector3|vector4 | Where the sale happened |
| value | number | The 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:
| Name | Type | Description |
|---|---|---|
| src | number | The acting player's server ID |
| activityType | string | A label for your logs. Up to 32 characters, letters, numbers, _ and - only |
| coords | table|vector3|vector4 | Where the activity happened |
| amount | number | Influence 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)
endThe 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".
| Field | Type | Description |
|---|---|---|
id | number | Gang ID |
name | string | Internal gang name |
label | string | Display name |
color | string | Hex colour |
grade | string | Internal rank name |
gradeLabel | string | Display rank name |
rank | number | Numeric rank |
isBoss | boolean | Whether their rank is a boss rank |
permissions | table | Map 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')
endCharge 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
endGate 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
endApply 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')
endOnly 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.
endReact 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-gangsexposes no client exports. - The resource also registers callbacks named
oxide-gangs:server:dashboard:*andoxide-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/vector4values, not plain tables. Anything on this page that takes coordinates accepts a table, avector3, or avector4, so you can passGetEntityCoordsresults straight in.
Next Steps
- Features — what the systems behind these exports actually do
- Configuration — the settings that govern them
- Troubleshooting — common issues