legacy-cityhall
Integration
Exports and events for integrating other resources with legacy-cityhall, with working code samples.
Integration
legacy-cityhall exposes both direct Lua exports (call from any server script) and ox_lib callbacks (used by its own NUI, but callable from other client scripts too via lib.callback.await). It also fires local server events you can hook with AddEventHandler, and one net event for pushing notifications to clients.
⚠️ Several direct exports intentionally skip checks that their callback equivalents enforce (pricing, licences, background checks, permissions, serial uniqueness, auto-hire). Read each description carefully before choosing which surface to use.
Registering a job
-- server/*.lua (any resource)
local jobId = exports['legacy-cityhall']:RegisterJob({
job = 'mechanic',
label = 'Mechanic',
type = 'public', -- 'public' or 'whitelisted'
maxSlots = 10,
requireInterview = false,
autoCloseDays = 14,
cooldownDays = 30,
maxActiveApplications = 50,
allowedDocuments = {},
})
-- Returns nil if a job with this job_name already exists (even if soft-deleted).
-- Use the legacy-cityhall:registerJob callback instead if you need reactivation
-- of a soft-deleted job.
Other job management exports: UnregisterJob(jobName), GetRegisteredJobs(), UpdateJobConfig(jobName, partialConfig), UpdateJobListing(jobName, listingData), SetJobOpenSlots(jobName, count), GetJobListing(jobName), CanManageJob(playerId, jobName) (playerId is actually a server source id), CanViewApplications(playerId, jobName), GetManageableJobs(playerId).
Applications
-- server/*.lua (any resource)
local appId = exports['legacy-cityhall']:CreateApplication(playerIdentifier, 'mechanic', {
reason = 'I like fixing cars',
})
-- No framework job/cooldown/max-application checks are performed here —
-- those only exist in the legacy-cityhall:submitApplication callback.
Other application exports: GetApplications(jobName, status), GetApplicationById(appId), UpdateApplicationStatus(appId, status, reviewerId, note) (bypasses the transition state machine and does not auto-hire on accept), AddApplicationNote(appId, reviewerId, note), GetApplicationNotes(appId), GetPlayerApplications(playerId), WithdrawApplication(appId, playerId).
Documents
-- server/*.lua (any resource)
local docId = exports['legacy-cityhall']:IssueDocument(source, 'id_card', { note = 'manual issue' })
-- issued_by is recorded as 'system'. Serial uniqueness is NOT verified against
-- existing rows (unlike the orderDocument callback). Pricing, provider hook,
-- licence checks and background-check status are all bypassed.
Other document exports: RevokeDocument(documentId, reason) (no permission check, no Provider.revoke call, no player notification — caller must gate access), VerifyDocument(serialNumber) (returns the full raw row, unfiltered — unlike the public-safe verifyDocument callback), GetPlayerDocuments(playerId), IsDocumentValid(documentId) (true only when status is exactly 'active').
Adding a custom document provider
-- server/providers/<yourprovider>.lua
local Provider = {}
Provider.issue = function(source, docType, data)
-- data = { playerName, serial }
-- return truthy on success, falsy on failure
return true
end
Provider.revoke = function(documentId)
return true
end
return Provider
-- Then set Config.DocumentProvider = 'yourprovider' in config.lua
Hooking lifecycle events
-- server/*.lua (any resource)
AddEventHandler('legacy-cityhall:playerHired', function(identifier, jobName, appId)
print(('%s was hired into %s (app #%s)'):format(identifier, jobName, tostring(appId)))
end)
Available local server events: legacy-cityhall:jobRegistered, legacy-cityhall:applicationSubmitted, legacy-cityhall:applicationWithdrawn, legacy-cityhall:applicationStatusChanged, legacy-cityhall:applicationNoteAdded, legacy-cityhall:playerHired, legacy-cityhall:playerQuit, legacy-cityhall:documentIssued, legacy-cityhall:documentRevoked, legacy-cityhall:documentReported.
Client-side notifications
-- client/*.lua (any resource)
RegisterNetEvent('legacy-cityhall:notify', function(data)
-- data.message: string, data.type: 'success'|'error'|'info'|'warning'
end)
This event is triggered server-side (TriggerClientEvent) to push a toast to a specific player, rendered via lib.notify with title 'City Hall'. If the City Hall NUI happens to be open on that client, the payload is also forwarded into the NUI as { action = 'notification', data = data }.
Opening the UI from your own code
OpenUI/CloseUI are not exported — they're local functions in client/client.lua. The only supported ways to open the UI are through the spawned NPC target interaction, or by replicating the sequence yourself:
-- client/*.lua (any resource)
local initData = lib.callback.await('legacy-cityhall:getInitData', false)
if initData then
SetNuiFocus(true, true)
SendNUIMessage({ action = 'open', data = initData })
end
Admin-only callbacks (via lib.callback.await)
These require the caller to be in Config.AdminGroups (or have appropriate job rank, where noted): legacy-cityhall:registerJob, legacy-cityhall:unregisterJob, legacy-cityhall:updateJobConfig, legacy-cityhall:createDocType, legacy-cityhall:updateDocType, legacy-cityhall:getAuditLog, legacy-cityhall:getFrameworkJobs, legacy-cityhall:getAllDocTypes, legacy-cityhall:revokeDocument.
Player-facing callbacks: legacy-cityhall:submitApplication, legacy-cityhall:withdrawApplication, legacy-cityhall:getApplications, legacy-cityhall:updateApplicationStatus, legacy-cityhall:getAppNotes, legacy-cityhall:addAppNote, legacy-cityhall:orderDocument, legacy-cityhall:verifyDocument (public, no permission check), legacy-cityhall:reportDocument, legacy-cityhall:getStolenReports, legacy-cityhall:getInitData, legacy-cityhall:getJobDetail, legacy-cityhall:publicHire, legacy-cityhall:quitJob, legacy-cityhall:getListing, legacy-cityhall:updateListing, legacy-cityhall:updateFormFields, legacy-cityhall:markNotificationsRead.