Sonoran.lua is a Lua SDK for Sonoran CAD v2 endpoints with shared client code for FiveM and Roblox runtimes.
Install from LuaRocks:
luarocks install sonoran.luaLuaRocks package: sonoran.lua on LuaRocks
Or use the generated FiveM resource release asset and start it before resources that consume it.
ensure Sonoran.luaIn your consuming resource:
fx_version 'cerulean'
game 'gta5'
lua54 'yes'
server_scripts {
'server.lua'
}
dependency 'Sonoran.lua'Install from Wally:
[dependencies]
Sonoran = "sonoransoftwaregit/sonoran-lua@^0.1.0"Then require the package module:
local Sonoran = require(ReplicatedStorage.Packages.Sonoran)FiveM resource usage:
local Sonoran = require("sonoran")
local sonoran = exports["Sonoran.lua"]:createClient({
product = Sonoran.productEnums.CAD,
apiKey = "your-cad-api-key",
communityId = "your-community-id",
apiUrl = "https://api.sonorancad.com",
defaultServerId = 1,
timeoutMs = 30000,
logLevel = Sonoran.logLevels.ERROR
})
sonoran:setLogLevel(Sonoran.logLevels.DEBUG)
sonoran:setRoomId(1)Roblox usage:
local sonoran = Sonoran.createClient({
product = Sonoran.productEnums.CAD,
apiKey = "your-cad-api-key",
communityId = "your-community-id",
apiUrl = "https://api.sonorancad.com",
defaultServerId = 1,
timeoutMs = 30000,
logLevel = Sonoran.logLevels.ERROR
})
sonoran:setLogLevel(Sonoran.logLevels.DEBUG)apiKey: required for authenticated endpoints.product: required; useSonoran.productEnums.CAD,Sonoran.productEnums.CMS, orSonoran.productEnums.RADIO.communityId: optional; used bygetLoginPageV2()when no explicitcommunityIdis supplied.apiUrl: optional; defaults tohttps://api.sonorancad.com.defaultServerId: optional; defaults to1for CAD/CMS-style server-scoped helpers. Radio v2 helpers resolve the community route fromcommunityId.roomId: optional for CAD/CMS and required for Radio v2 room-scoped helpers.headers: optional extra headers merged into every request.timeoutMs: optional timeout for the FiveM adapter; defaults to30000.logLevel: optional;Sonoran.logLevels.ERRORby default. Supported values areOFF,ERROR, andDEBUG.
Use setLogLevel() to toggle HTTP debug output at runtime:
sonoran:setLogLevel(Sonoran.logLevels.DEBUG)Use setRoomId() to update the Radio room used by later room-scoped requests without creating a new client:
sonoran:setRoomId(1)Use ERROR to only print failed requests and rate-limit events:
sonoran:setLogLevel(Sonoran.logLevels.ERROR)When DEBUG is enabled, Sonoran.lua prints every HTTP request and response to the console, including the method, URL, headers, body, response status, and response headers. Sensitive request values such as Authorization and API-key style fields are redacted in both DEBUG and ERROR logs.
All public methods return:
{ success = true, data = ... }or:
{ success = false, reason = ... }Successful JSON responses are decoded automatically. Plain-text error responses are returned as strings. 204 No Content responses return data = nil.
For CAD v2 endpoints, Sonoran.lua automatically retries 429 Too Many Requests responses up to 2 times when it can safely honor the server's wait window. The client checks Retry-After first, including both delta-seconds and HTTP-date formats, then falls back to RateLimit-Reset, X-RateLimit-Reset, and finally a short exponential backoff. If the server asks for a longer wait than the client's automatic retry limit, or the active adapter cannot sleep, the request fails instead of retrying too early.
General endpoint:
local version = sonoran.cad:getVersionV2()
if version.success then
print(version.data)
endCivilian endpoint:
local characters = sonoran.cad:getCharactersV2({
roblox = 1234567890
})
if characters.success then
print(("Found %s character(s)"):format(#characters.data))
endEmergency endpoint:
local created = sonoran.cad:createDispatchCallV2({
serverId = 1,
origin = 1,
status = 1,
priority = 1,
block = "123",
address = "Main St",
postal = "100",
title = "Traffic Stop",
code = "TS",
description = "Blue sedan heading north",
notes = {},
communityUserIds = { "1234567890" }
})
if not created.success then
print(json.encode(created.reason))
endAll CAD v2 helpers are available under client.cad.*. The root-level methods are still present for backward compatibility.
getLoginPageV2(params?)checkApiIdV2(apiId)applyPermissionKeyV2(data)banUserV2(data)getPenalCodesV2()setPenalCodesV2(codes)setApiIdsV2(data)getTemplatesV2(recordTypeId?)createRecordV2(data)updateRecordV2(recordId, data)removeRecordV2(recordId)sendRecordDraftV2(data)lookupV2(data)lookupByValueV2(data)lookupCustomV2(data)getAccountV2(query?)getAccountsV2(query?)createCommunityLinkV2(data)checkCommunityLinkV2(data)setCommunityLinkV2(data)setAccountPermissionsV2(data)heartbeatV2(serverId, playerCount)getVersionV2()getServersV2()setServersV2(servers, deployMap?)verifySecretV2(secret)authorizeStreetSignsV2(serverId?)setPostalsV2(postals)sendPhotoV2(data)uploadBodycamRecordingV2(data)getInfoV2()
getCharactersV2(query?)removeCharacterV2(characterId)setSelectedCharacterV2(data)getCharacterLinksV2(query?)addCharacterLinkV2(syncId, data)removeCharacterLinkV2(syncId, data)
getUnitsV2(query?)getCallsV2(query?)getCurrentCallV2(accountUuid)updateUnitLocationsV2(data)setUnitPanicV2(data)setUnitStatusV2(data)kickUnitV2(data)getIdentifiersV2(accountUuid)getAccountUnitsV2(data)selectIdentifierV2(accountUuid, identId)createIdentifierV2(accountUuid, data)updateIdentifierV2(accountUuid, identId, data)deleteIdentifierV2(accountUuid, identId)addIdentifiersToGroupV2(data)createEmergencyCallV2(data)deleteEmergencyCallV2(callId, serverId?)getDispatchTemplatesV2(templateId?)createDispatchCallV2(data)createCustomDispatchCallV2(data)updateDispatchCallV2(callId, data)attachUnitsToDispatchCallV2(callId, data)detachUnitsFromDispatchCallV2(data)setDispatchPostalV2(callId, postal, serverId?)setDispatchPrimaryV2(callId, identId, trackPrimary?, serverId?)addDispatchNoteV2(callId, data)closeDispatchCallsV2(callIds, serverId?)updateStreetSignsV2(data)setStreetSignConfigV2(signs, serverId?)setAvailableCalloutsV2(callouts, serverId?)getPagerConfigV2(serverId?)setPagerConfigV2(data)setStationsV2(config, serverId?)Sends the provided top-level station payload as-is. Passlocations,tones, andunitColorsdirectly on the request body.getBlipsV2(serverId?)createBlipV2(data)updateBlipV2(blipId, data)deleteBlipsV2(ids, serverId?)
- Account-targeted CAD v2 helpers accept
accountUuid,communityUserId,roblox,discord, and legacyapiIdwhere supported by the backend. updateUnitLocationsV2(data)uses the HTTP v2 endpoint for slower unit location updates, and each update can targetcommunityUserId,roblox, ordiscord.- Unit location updates can target
communityUserId,roblox, ordiscordthrough the v2 HTTP endpoint. - FiveM uses
PerformHttpRequest,promise.new(), andCitizen.Await. - Roblox uses
HttpService:RequestAsync(). - Radio, CMS, and legacy CAD endpoints are intentionally out of scope for this initial port.
Use getPermissionCatalogV2, getAccountPermissionsV2, and replaceAccountPermissionsV2 for new permission integrations. The existing setAccountPermissionsV2 remains a legacy category adapter.
local catalog = sonoran.cad:getPermissionCatalogV2()
local account = sonoran.cad:getAccountPermissionsV2(accountUuid)
local response = sonoran.cad:replaceAccountPermissionsV2(accountUuid, { "global.police" })
-- Clear all grants explicitly:
local cleared = sonoran.cad:replaceAccountPermissionsV2(accountUuid, {})Use the account UUID, not a community user ID, in these calls. Fetch the community catalog for exact, case-sensitive grant IDs and template IDs; legacyGrants maps uppercase legacy flags to current grants. Replacement overwrites the full grant list (version 2), and an empty list clears it. Never treat a failed read as an empty list. Only pending or active non-owner accounts can be edited. Nonempty grants activate pending accounts subject to the member limit; empty grants make active accounts pending. A granular save ends legacy category inheritance for future record templates.
Discover support from getPermissionCatalogV2(): use record.<templateId>.edit.selected only when that exact grant is returned. It requires the updated CAD backend and may not yet be available during rollout. No SDK method or permission-document version change is required.
edit.own: full editing of records owned by the account.edit.any: full editing of anyone's records, including the account's own records; field opt-in does not limit this grant on the updated backend.edit.selected: editing only fields marked Allow limited editing (editableByOthers: true) on another account's records. It does not includeedit.ownoredit.any.supervise: an additional requirement for supervisor-only fields; it does not grant editing by itself or bypass limited-field opt-in. Read-only fields remain locked for account editing.
Existing grants and field settings are preserved, and the new grant is not automatically assigned. For limited access, remove that template's edit.any grant from every source and assign edit.selected instead; optionally retain edit.own. Permissions from keys or role mappings may combine, and any remaining edit.any grants full editing. Fetch the account first and preserve unrelated grants when replacing its complete permission set. Never treat a failed read as an empty grant list. Community API-key record operations retain their existing service authority; these grants govern community accounts.