> ## Documentation Index
> Fetch the complete documentation index at: https://docs.aps-store.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Installation

## 1. Download Resource

Download the purchased resource from [**CFX Portal**](https://portal.cfx.re/assets/granted-assets), the official FiveM site for purchased resources.

Put the `aps_cityhall` folder into your `resources` directory. **Keep the folder name**: other scripts call its exports as `exports['aps_cityhall']`.

***

## 2. Install Required Dependencies

| Resource | Required | Download Link |
| - | - | - |
| **es\_extended** / **qb-core** / **qbx\_core** | One of them | ESX Legacy 1.9+, QBCore or Qbox (detected automatically) |
| **oxmysql** | Yes | [https://github.com/overextended/oxmysql](https://github.com/overextended/oxmysql) |
| **MugShotBase64** | Optional (document photos) | [https://github.com/BaziForYou/MugShotBase64](https://github.com/BaziForYou/MugShotBase64) |
| **ox\_target** / **qb-target** | Optional | Used automatically when started, otherwise a 3D text + E prompt is used |
| **ox\_lib** | Optional | Notifications (default on Qbox) |
| **ox\_inventory** / **qs-inventory** / **origen\_inventory** / **tgiann-inventory** / **core\_inventory** | Optional | Document and payment items. Without them the framework's item functions are used |

All other integrations (society accounts, phones, license systems, VMS scripts) are listed on the [Compability](https://docs.aps-store.com/assets/aps_cityhall/compability.md) page.

***

## 3. Database

> [!TIP]
>
> **Nothing to import.** Every table is created automatically on the first start and upgraded on later versions. The database user needs the `CREATE` and `ALTER` rights.

If you prefer to create the tables yourself, run `sql/aps_cityhall.sql` (it works for ESX, QBCore and Qbox).

> [!WARNING]
>
> ## **Not sure how to do it?**
>
> Open your database in HeidiSQL, phpMyAdmin or the txAdmin database tab, open a new query window, paste the SQL below and run it.

> [accordion] Database (optional, ESX / QBCore / Qbox)
>
> ```sql theme={null}
> -- APS City Hall — Database Schema
> -- Optional: every table is also created automatically on start.
> -- Run it if you prefer to create the tables yourself.
> -- NOTE: If upgrading from nt_licensecenter, run:
> --   RENAME TABLE nt_licenses TO aps_licenses;
> --   RENAME TABLE nt_license_cooldowns TO aps_license_cooldowns;
>
> CREATE TABLE IF NOT EXISTS `aps_licenses` (
>     `id`         INT          NOT NULL AUTO_INCREMENT,
>     `identifier` VARCHAR(60)  NOT NULL,
>     `license_id` VARCHAR(50)  NOT NULL,
>     `issued_at`  TIMESTAMP    NOT NULL DEFAULT CURRENT_TIMESTAMP,
>     PRIMARY KEY (`id`),
>     UNIQUE KEY  `uk_license` (`identifier`, `license_id`),
>     KEY         `idx_identifier` (`identifier`)
> ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
>
> CREATE TABLE IF NOT EXISTS `aps_license_cooldowns` (
>     `identifier`   VARCHAR(60)  NOT NULL,
>     `license_id`   VARCHAR(50)  NOT NULL,
>     `last_attempt` TIMESTAMP    NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
>     PRIMARY KEY (`identifier`, `license_id`)
> ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
>
> CREATE TABLE IF NOT EXISTS `aps_documents` (
>     `id`          INT          NOT NULL AUTO_INCREMENT,
>     `identifier`  VARCHAR(60)  NOT NULL,
>     `doc_type`    VARCHAR(50)  NOT NULL,
>     `serial`      VARCHAR(30)  NULL DEFAULT NULL,
>     `issued_at`   TIMESTAMP    NOT NULL DEFAULT CURRENT_TIMESTAMP,
>     `expires_at`  TIMESTAMP    NOT NULL,
>     `price_paid`  INT          NOT NULL DEFAULT 0,
>     `revoked`     TINYINT      NOT NULL DEFAULT 0,
>     `data`        LONGTEXT     NULL,
>     PRIMARY KEY (`id`),
>     UNIQUE KEY `uk_serial` (`serial`),
>     KEY `idx_ident` (`identifier`)
> ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
>
> CREATE TABLE IF NOT EXISTS `aps_vehicle_insurance` (
>     `id`          INT          NOT NULL AUTO_INCREMENT,
>     `identifier`  VARCHAR(60)  NOT NULL,
>     `plate`       VARCHAR(20)  NOT NULL,
>     `model`       VARCHAR(50)  NOT NULL,
>     `expires_at`  TIMESTAMP    NOT NULL,
>     `price_paid`  INT          NOT NULL DEFAULT 0,
>     `issued_at`   TIMESTAMP    NOT NULL DEFAULT CURRENT_TIMESTAMP,
>     PRIMARY KEY (`id`),
>     KEY `idx_ident`  (`identifier`),
>     KEY `idx_plate`  (`plate`)
> ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
>
> CREATE TABLE IF NOT EXISTS `aps_bills` (
>     `id`         INT          NOT NULL AUTO_INCREMENT,
>     `identifier` VARCHAR(60)  NOT NULL,
>     `type`       VARCHAR(30)  NOT NULL,
>     `status`     VARCHAR(20)  NOT NULL DEFAULT 'unpaid',
>     `issuer`     VARCHAR(100) NOT NULL,
>     `issuer_job` VARCHAR(50)  NOT NULL DEFAULT '',
>     `amount`     INT          NOT NULL,
>     `due_at`     TIMESTAMP    NULL DEFAULT NULL,
>     `paid_at`    TIMESTAMP    NULL DEFAULT NULL,
>     `issued_at`  TIMESTAMP    NOT NULL DEFAULT CURRENT_TIMESTAMP,
>     `data`       LONGTEXT     NOT NULL,
>     PRIMARY KEY (`id`),
>     KEY `idx_ident`  (`identifier`),
>     KEY `idx_status` (`status`)
> ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
>
> -- Mini-games: best score per player and game
> -- (the resource also creates / upgrades this table on start)
> CREATE TABLE IF NOT EXISTS `aps_game_scores` (
>     `identifier` VARCHAR(60)  NOT NULL,
>     `game`       VARCHAR(20)  NOT NULL DEFAULT 'tower',
>     `name`       VARCHAR(100) NOT NULL,
>     `best_score` INT          NOT NULL DEFAULT 0,
>     `updated_at` TIMESTAMP    NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
>     PRIMARY KEY (`identifier`, `game`),
>     KEY `idx_game_score` (`game`, `best_score`)
> ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
>
> -- Business Register (also created automatically on start)
> CREATE TABLE IF NOT EXISTS `aps_businesses` (
>     `id`          INT          NOT NULL AUTO_INCREMENT,
>     `owner`       VARCHAR(60)  NOT NULL,
>     `owner_name`  VARCHAR(100) NOT NULL,
>     `name`        VARCHAR(60)  NOT NULL,
>     `category`    VARCHAR(30)  NOT NULL,
>     `description` VARCHAR(400) NOT NULL DEFAULT '',
>     `phone`       VARCHAR(20)  NOT NULL DEFAULT '',
>     `status`      VARCHAR(10)  NOT NULL DEFAULT 'active',
>     `fee_paid`    INT          NOT NULL DEFAULT 0,
>     `review_note` VARCHAR(300) NOT NULL DEFAULT '',
>     `reviewed_by` VARCHAR(100) NOT NULL DEFAULT '',
>     `reviewed_at` TIMESTAMP    NULL DEFAULT NULL,
>     `job`         VARCHAR(50)  NOT NULL DEFAULT '',
>     `created_at`  TIMESTAMP    NOT NULL DEFAULT CURRENT_TIMESTAMP,
>     PRIMARY KEY (`id`),
>     KEY `idx_owner` (`owner`),
>     KEY `idx_status` (`status`)
> ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
>
> -- Business permits, vehicles, employees, inspections (also created automatically on start)
> CREATE TABLE IF NOT EXISTS `aps_business_permits` (
>     `id`          INT          NOT NULL AUTO_INCREMENT,
>     `business_id` INT          NOT NULL,
>     `permit`      VARCHAR(40)  NOT NULL,
>     `status`      VARCHAR(10)  NOT NULL DEFAULT 'pending',
>     `days`        INT          NOT NULL DEFAULT 0,
>     `price_paid`  INT          NOT NULL DEFAULT 0,
>     `expires_at`  TIMESTAMP    NULL DEFAULT NULL,
>     `review_note` VARCHAR(300) NOT NULL DEFAULT '',
>     `reviewed_by` VARCHAR(100) NOT NULL DEFAULT '',
>     `reviewed_at` TIMESTAMP    NULL DEFAULT NULL,
>     `created_at`  TIMESTAMP    NOT NULL DEFAULT CURRENT_TIMESTAMP,
>     PRIMARY KEY (`id`),
>     KEY `idx_business` (`business_id`, `permit`, `status`)
> ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
>
> CREATE TABLE IF NOT EXISTS `aps_business_vehicles` (
>     `plate`       VARCHAR(20)  NOT NULL,
>     `business_id` INT          NOT NULL,
>     `created_at`  TIMESTAMP    NOT NULL DEFAULT CURRENT_TIMESTAMP,
>     PRIMARY KEY (`plate`),
>     KEY `idx_business` (`business_id`)
> ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
>
> CREATE TABLE IF NOT EXISTS `aps_business_employees` (
>     `business_id` INT          NOT NULL,
>     `identifier`  VARCHAR(60)  NOT NULL,
>     `name`        VARCHAR(100) NOT NULL,
>     `created_at`  TIMESTAMP    NOT NULL DEFAULT CURRENT_TIMESTAMP,
>     PRIMARY KEY (`business_id`, `identifier`),
>     KEY `idx_identifier` (`identifier`)
> ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
>
> CREATE TABLE IF NOT EXISTS `aps_business_inspections` (
>     `id`          INT          NOT NULL AUTO_INCREMENT,
>     `business_id` INT          NOT NULL,
>     `inspector`   VARCHAR(100) NOT NULL,
>     `result`      VARCHAR(50)  NOT NULL,
>     `notes`       VARCHAR(500) NOT NULL DEFAULT '',
>     `fine`        INT          NOT NULL DEFAULT 0,
>     `bill_id`     INT          NULL DEFAULT NULL,
>     `created_at`  TIMESTAMP    NOT NULL DEFAULT CURRENT_TIMESTAMP,
>     PRIMARY KEY (`id`),
>     KEY `idx_business` (`business_id`)
> ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
>
> -- Government Portal activity log (also created automatically on start)
> CREATE TABLE IF NOT EXISTS `aps_gov_log` (
>     `id`         INT          NOT NULL AUTO_INCREMENT,
>     `actor`      VARCHAR(60)  NOT NULL,
>     `actor_name` VARCHAR(100) NOT NULL,
>     `action`     VARCHAR(40)  NOT NULL,
>     `target`     VARCHAR(100) NOT NULL DEFAULT '',
>     `details`    VARCHAR(300) NOT NULL DEFAULT '',
>     `created_at` TIMESTAMP    NOT NULL DEFAULT CURRENT_TIMESTAMP,
>     PRIMARY KEY (`id`),
>     KEY `idx_created` (`created_at`)
> ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
>
> -- License card photos (also created automatically on start)
> CREATE TABLE IF NOT EXISTS `aps_mugshots` (
>     `identifier` VARCHAR(60) NOT NULL,
>     `image`      MEDIUMTEXT  NOT NULL,
>     `updated_at` TIMESTAMP   NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
>     PRIMARY KEY (`identifier`)
> ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
>
> -- ═════════════════════════════════════════════
> --  3.7.0 — taxes, treasury, license suspension, penalty points,
> --  paychecks, Job Center (all also created automatically on start)
> -- ═════════════════════════════════════════════
>
> -- Scheduler: last period each job ran
> CREATE TABLE IF NOT EXISTS `aps_scheduler` (
>     `name`     VARCHAR(60) NOT NULL,
>     `last_key` VARCHAR(20) NOT NULL,
>     PRIMARY KEY (`name`)
> ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
>
> -- Personal taxes
> CREATE TABLE IF NOT EXISTS `aps_taxes` (
>     `id`          INT          NOT NULL AUTO_INCREMENT,
>     `identifier`  VARCHAR(60)  NOT NULL,
>     `title`       VARCHAR(100) NOT NULL,
>     `category`    VARCHAR(30)  NOT NULL DEFAULT 'other',
>     `base_amount` INT          NOT NULL DEFAULT 0,
>     `percent`     DECIMAL(6,2) NOT NULL DEFAULT 0,
>     `amount`      INT          NOT NULL,
>     `status`      VARCHAR(20)  NOT NULL DEFAULT 'unpaid',
>     `ref`         VARCHAR(80)  NULL DEFAULT NULL,
>     `issued_at`   TIMESTAMP    NOT NULL DEFAULT CURRENT_TIMESTAMP,
>     `due_at`      TIMESTAMP    NULL DEFAULT NULL,
>     `paid_at`     TIMESTAMP    NULL DEFAULT NULL,
>     `paid_amount` INT          NOT NULL DEFAULT 0,
>     `auto`        TINYINT(1)   NOT NULL DEFAULT 0,
>     PRIMARY KEY (`id`),
>     UNIQUE KEY `uniq_ref` (`identifier`, `ref`),
>     KEY `idx_identifier` (`identifier`, `status`)
> ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
>
> -- Business taxes per account and month
> CREATE TABLE IF NOT EXISTS `aps_business_taxes` (
>     `id`          INT          NOT NULL AUTO_INCREMENT,
>     `account`     VARCHAR(60)  NOT NULL,
>     `period`      CHAR(7)      NOT NULL,
>     `earned`      BIGINT       NOT NULL DEFAULT 0,
>     `tax_amount`  INT          NOT NULL DEFAULT 0,
>     `status`      VARCHAR(20)  NOT NULL DEFAULT 'open',
>     `paid_amount` INT          NOT NULL DEFAULT 0,
>     `paid_by`     VARCHAR(100) NULL DEFAULT NULL,
>     `paid_at`     TIMESTAMP    NULL DEFAULT NULL,
>     PRIMARY KEY (`id`),
>     UNIQUE KEY `uniq_period` (`account`, `period`)
> ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
>
> -- City treasury (one row)
> CREATE TABLE IF NOT EXISTS `aps_treasury` (
>     `id`           TINYINT NOT NULL,
>     `balance`      BIGINT  NOT NULL DEFAULT 0,
>     `total_earned` BIGINT  NOT NULL DEFAULT 0,
>     PRIMARY KEY (`id`)
> ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
>
> -- Treasury ledger
> CREATE TABLE IF NOT EXISTS `aps_treasury_log` (
>     `id`         INT          NOT NULL AUTO_INCREMENT,
>     `amount`     BIGINT       NOT NULL,
>     `reason`     VARCHAR(120) NOT NULL,
>     `created_at` TIMESTAMP    NOT NULL DEFAULT CURRENT_TIMESTAMP,
>     PRIMARY KEY (`id`),
>     KEY `idx_created` (`created_at`)
> ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
>
> -- License suspensions
> CREATE TABLE IF NOT EXISTS `aps_license_suspensions` (
>     `id`          INT          NOT NULL AUTO_INCREMENT,
>     `identifier`  VARCHAR(60)  NOT NULL,
>     `license_id`  VARCHAR(50)  NOT NULL,
>     `until`       TIMESTAMP    NULL DEFAULT NULL,
>     `reason`      VARCHAR(200) NOT NULL DEFAULT '',
>     `by_name`     VARCHAR(100) NOT NULL DEFAULT '',
>     `created_at`  TIMESTAMP    NOT NULL DEFAULT CURRENT_TIMESTAMP,
>     `lifted_at`   TIMESTAMP    NULL DEFAULT NULL,
>     PRIMARY KEY (`id`),
>     KEY `idx_active` (`identifier`, `license_id`, `lifted_at`)
> ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
>
> -- Penalty points
> CREATE TABLE IF NOT EXISTS `aps_penalty_points` (
>     `id`          INT          NOT NULL AUTO_INCREMENT,
>     `identifier`  VARCHAR(60)  NOT NULL,
>     `points`      INT          NOT NULL,
>     `reason`      VARCHAR(200) NOT NULL DEFAULT '',
>     `bill_id`     INT          NULL DEFAULT NULL,
>     `issued_at`   TIMESTAMP    NOT NULL DEFAULT CURRENT_TIMESTAMP,
>     `expires_at`  TIMESTAMP    NULL DEFAULT NULL,
>     `removed`     TINYINT(1)   NOT NULL DEFAULT 0,
>     PRIMARY KEY (`id`),
>     KEY `idx_identifier` (`identifier`, `removed`)
> ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
>
> -- Paychecks waiting at City Hall
> CREATE TABLE IF NOT EXISTS `aps_paychecks` (
>     `identifier` VARCHAR(60) NOT NULL,
>     `balance`    BIGINT      NOT NULL DEFAULT 0,
>     `total`      BIGINT      NOT NULL DEFAULT 0,
>     `updated_at` TIMESTAMP   NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
>     PRIMARY KEY (`identifier`)
> ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
>
> -- Paycheck history
> CREATE TABLE IF NOT EXISTS `aps_paycheck_log` (
>     `id`         INT          NOT NULL AUTO_INCREMENT,
>     `identifier` VARCHAR(60)  NOT NULL,
>     `amount`     INT          NOT NULL,
>     `label`      VARCHAR(100) NOT NULL DEFAULT '',
>     `created_at` TIMESTAMP    NOT NULL DEFAULT CURRENT_TIMESTAMP,
>     PRIMARY KEY (`id`),
>     KEY `idx_identifier` (`identifier`, `created_at`)
> ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
>
> -- Job applications
> CREATE TABLE IF NOT EXISTS `aps_resumes` (
>     `id`          INT          NOT NULL AUTO_INCREMENT,
>     `identifier`  VARCHAR(60)  NOT NULL,
>     `name`        VARCHAR(100) NOT NULL,
>     `job`         VARCHAR(60)  NOT NULL,
>     `answers`     TEXT         NOT NULL,
>     `status`      VARCHAR(20)  NOT NULL DEFAULT 'pending',
>     `reviewed_by` VARCHAR(100) NULL DEFAULT NULL,
>     `reviewed_at` TIMESTAMP    NULL DEFAULT NULL,
>     `note`        VARCHAR(300) NOT NULL DEFAULT '',
>     `created_at`  TIMESTAMP    NOT NULL DEFAULT CURRENT_TIMESTAMP,
>     PRIMARY KEY (`id`),
>     KEY `idx_job` (`job`, `status`),
>     KEY `idx_identifier` (`identifier`)
> ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
>
> -- Job Center: applications open / closed per job
> CREATE TABLE IF NOT EXISTS `aps_resume_settings` (
>     `job`     VARCHAR(60) NOT NULL,
>     `allowed` TINYINT(1)  NOT NULL DEFAULT 1,
>     PRIMARY KEY (`job`)
> ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
>
> INSERT IGNORE INTO `aps_treasury` (id, balance, total_earned) VALUES (1, 0, 0);
> ```

On start City Hall also adds the `vin` and `vin_hidden` columns to your owned vehicles table (`Config.VIN.autoMigrate`). See [Database](https://docs.aps-store.com/assets/aps_cityhall/developer-api/database.md) for the full table list.

***

## 4. Add Required Items

Documents are real inventory items with serial number, photo and validity. Icons for every item ship in `aps_cityhall/web/items/` (SVG).

> [accordion] Items for ESX inventory
>
> ```sql theme={null}
> INSERT IGNORE INTO `items` (`name`, `label`, `weight`, `rare`, `can_remove`) VALUES
>     ('license_car',          'Driving License',      1, 0, 1),
>     ('license_motorcycle',   'Motorcycle License',   1, 0, 1),
>     ('license_truck',        'Truck License',        1, 0, 1),
>     ('license_boat',         'Boat License',         1, 0, 1),
>     ('license_pilot',        'Pilot License',        1, 0, 1),
>     ('license_weapon',       'Firearms License',     1, 0, 1),
>     ('id_card',              'ID Card',              1, 0, 1),
>     ('driving_license',      'Driving License (Document)', 1, 0, 1),
>     ('weapons_license',      'CCW Permit',           1, 0, 1),
>     ('police_badge',         'Police Badge',         1, 0, 1),
>     ('law_license',          'Law License',          1, 0, 1),
>     ('health_card',          'Health Insurance Card', 1, 0, 1),
>     ('business_certificate', 'Business Certificate', 1, 0, 1),
>     ('hacking_usb',          'Hacking USB',          1, 0, 1);
> ```

> [accordion] Items for ox\_inventory
>
> > [!NOTE]
> >
> > Every document needs `server = { export = 'aps_cityhall.useDocument' }` (makes it usable) and `consume = 0` (keeps the item).
>
> ```lua theme={null}
> -- ox_inventory/data/items.lua
> ['id_card'] = {
>     label = 'ID Card', weight = 10, stack = false, close = true, consume = 0,
>     server = { export = 'aps_cityhall.useDocument' },
>     client = { image = 'nui://aps_cityhall/web/items/id_card.svg' },
> },
> ['driving_license'] = {
>     label = 'Driving License', weight = 10, stack = false, close = true, consume = 0,
>     server = { export = 'aps_cityhall.useDocument' },
>     client = { image = 'nui://aps_cityhall/web/items/driving_license.svg' },
> },
> ['license_motorcycle'] = {
>     label = 'Motorcycle License', weight = 10, stack = false, close = true, consume = 0,
>     server = { export = 'aps_cityhall.useDocument' },
>     client = { image = 'nui://aps_cityhall/web/items/license_motorcycle.svg' },
> },
> ['license_truck'] = {
>     label = 'Truck License', weight = 10, stack = false, close = true, consume = 0,
>     server = { export = 'aps_cityhall.useDocument' },
>     client = { image = 'nui://aps_cityhall/web/items/license_truck.svg' },
> },
> ['license_boat'] = {
>     label = 'Boat License', weight = 10, stack = false, close = true, consume = 0,
>     server = { export = 'aps_cityhall.useDocument' },
>     client = { image = 'nui://aps_cityhall/web/items/license_boat.svg' },
> },
> ['license_pilot'] = {
>     label = 'Pilot License', weight = 10, stack = false, close = true, consume = 0,
>     server = { export = 'aps_cityhall.useDocument' },
>     client = { image = 'nui://aps_cityhall/web/items/license_pilot.svg' },
> },
> ['weapons_license'] = {
>     label = 'CCW Permit', weight = 10, stack = false, close = true, consume = 0,
>     server = { export = 'aps_cityhall.useDocument' },
>     client = { image = 'nui://aps_cityhall/web/items/weapons_license.svg' },
> },
> ['police_badge'] = {
>     label = 'Police Badge', weight = 10, stack = false, close = true, consume = 0,
>     server = { export = 'aps_cityhall.useDocument' },
>     client = { image = 'nui://aps_cityhall/web/items/police_badge.svg' },
> },
> ['law_license'] = {
>     label = 'Law License', weight = 10, stack = false, close = true, consume = 0,
>     server = { export = 'aps_cityhall.useDocument' },
>     client = { image = 'nui://aps_cityhall/web/items/law_license.svg' },
> },
> ['health_card'] = {
>     label = 'Health Insurance Card', weight = 10, stack = false, close = true, consume = 0,
>     server = { export = 'aps_cityhall.useDocument' },
>     client = { image = 'nui://aps_cityhall/web/items/health_card.svg' },
> },
> ['business_certificate'] = {
>     label = 'Business Certificate', weight = 10, stack = false,
>     client = { image = 'nui://aps_cityhall/web/items/business_certificate.svg' },
> },
> ['hacking_usb'] = {
>     label = 'Hacking USB', weight = 50, stack = false,
>     client = { image = 'nui://aps_cityhall/web/items/hacking_usb.svg' },
> },
> ```

> [accordion] Items for qb-inventory / ps-inventory / qs-inventory
>
> > [!NOTE]
> >
> > These inventories read PNG files from their own image folder. Convert the SVG icons from `aps_cityhall/web/items/` to PNG and copy them there.
>
> ```lua theme={null}
> -- qb-core/shared/items.lua
> id_card = { name = 'id_card', label = 'ID Card', weight = 10, type = 'item', image = 'id_card.png', unique = true, useable = true, shouldClose = true, description = 'ID Card' },
> driving_license = { name = 'driving_license', label = 'Driving License', weight = 10, type = 'item', image = 'driving_license.png', unique = true, useable = true, shouldClose = true, description = 'Driving License' },
> license_motorcycle = { name = 'license_motorcycle', label = 'Motorcycle License', weight = 10, type = 'item', image = 'license_motorcycle.png', unique = true, useable = true, shouldClose = true, description = 'Motorcycle License' },
> license_truck = { name = 'license_truck', label = 'Truck License', weight = 10, type = 'item', image = 'license_truck.png', unique = true, useable = true, shouldClose = true, description = 'Truck License' },
> license_boat = { name = 'license_boat', label = 'Boat License', weight = 10, type = 'item', image = 'license_boat.png', unique = true, useable = true, shouldClose = true, description = 'Boat License' },
> license_pilot = { name = 'license_pilot', label = 'Pilot License', weight = 10, type = 'item', image = 'license_pilot.png', unique = true, useable = true, shouldClose = true, description = 'Pilot License' },
> weapons_license = { name = 'weapons_license', label = 'CCW Permit', weight = 10, type = 'item', image = 'weapons_license.png', unique = true, useable = true, shouldClose = true, description = 'CCW Permit' },
> police_badge = { name = 'police_badge', label = 'Police Badge', weight = 10, type = 'item', image = 'police_badge.png', unique = true, useable = true, shouldClose = true, description = 'Police Badge' },
> law_license = { name = 'law_license', label = 'Law License', weight = 10, type = 'item', image = 'law_license.png', unique = true, useable = true, shouldClose = true, description = 'Law License' },
> health_card = { name = 'health_card', label = 'Health Insurance Card', weight = 10, type = 'item', image = 'health_card.png', unique = true, useable = true, shouldClose = true, description = 'Health Insurance Card' },
> business_certificate = { name = 'business_certificate', label = 'Business Certificate', weight = 10, type = 'item', image = 'business_certificate.png', unique = true, useable = false, shouldClose = true, description = 'Business Certificate' },
> hacking_usb = { name = 'hacking_usb', label = 'Hacking USB', weight = 50, type = 'item', image = 'hacking_usb.png', unique = true, useable = false, shouldClose = true, description = 'Hacking USB' },
> ```

***

## 5. Register Jobs

The **Government Portal** can be opened by job, by login or both (`Config.Government.access`). If you want job access, add a `government` job. The grades match the default roles in [goverment.lua](https://docs.aps-store.com/assets/aps_cityhall/configuration-files/goverment.lua.md): grade 0 = Clerk, grade 2 = Manager, grade 4 = Mayor. Salaries are examples.

> [accordion] Jobs for ESX
>
> ```sql theme={null}
> INSERT IGNORE INTO `jobs` (`name`, `label`) VALUES ('government', 'Government');
>
> INSERT IGNORE INTO `job_grades` (`job_name`, `grade`, `name`, `label`, `salary`, `skin_male`, `skin_female`) VALUES
>     ('government', 0, 'clerk',        'Clerk',        500,  '{}', '{}'),
>     ('government', 1, 'senior_clerk', 'Senior Clerk', 750,  '{}', '{}'),
>     ('government', 2, 'manager',      'Manager',      1000, '{}', '{}'),
>     ('government', 3, 'deputy_mayor', 'Deputy Mayor', 1500, '{}', '{}'),
>     ('government', 4, 'boss',         'Mayor',        2000, '{}', '{}');
> ```

> [accordion] Jobs for QB-Core
>
> ```lua theme={null}
> -- qb-core/shared/jobs.lua
> ['government'] = {
>     label = 'Government',
>     defaultDuty = true,
>     offDutyPay = false,
>     grades = {
>         ['0'] = { name = 'Clerk', payment = 50 },
>         ['1'] = { name = 'Senior Clerk', payment = 75 },
>         ['2'] = { name = 'Manager', payment = 100 },
>         ['3'] = { name = 'Deputy Mayor', payment = 125 },
>         ['4'] = { name = 'Mayor', isboss = true, payment = 150 },
>     },
> },
> ```

> [accordion] Jobs for QBX-Core
>
> ```lua theme={null}
> -- qbx_core/shared/jobs.lua
> ['government'] = {
>     label = 'Government',
>     defaultDuty = true,
>     offDutyPay = false,
>     grades = {
>         [0] = { name = 'Clerk', payment = 50 },
>         [1] = { name = 'Senior Clerk', payment = 75 },
>         [2] = { name = 'Manager', payment = 100 },
>         [3] = { name = 'Deputy Mayor', payment = 125 },
>         [4] = { name = 'Mayor', isboss = true, bankAuth = true, payment = 150 },
>     },
> },
> ```

> [!WARNING]
>
> Login accounts are set in `config/config.server.lua` → `Config.GovernmentAccounts`. Accounts with the password `change-me` are disabled. **Change the example passwords before going live.**

***

## 6. Additional Integrations

**1. VIN for existing vehicles**

Run `_generatevin` once in the server console. Read the [VIN](https://docs.aps-store.com/assets/aps_cityhall/guides/vin.md) guide.

**2. Paychecks collected at City Hall (optional)**

Needs a small edit in your framework's paycheck loop. Read the [Paychecks](https://docs.aps-store.com/assets/aps_cityhall/guides/paychecks.md) guide.

**3. Phone app, notifications and mails**

Detected automatically. Read the [Phone integration](https://docs.aps-store.com/assets/aps_cityhall/guides/phone-integration.md) guide.

**4. Coming from vms\\\_cityhall?**

Set `Config.Compat.vmsCityhall = true` so scripts calling `exports['vms_cityhall']` keep working. Read [Compability](https://docs.aps-store.com/assets/aps_cityhall/compability.md).

***

## 7. Start Resource

Start `aps_cityhall` in your `server.cfg` **after** your framework, target, inventory and banking resources so they are detected:

```bash theme={null}
ensure oxmysql
ensure es_extended    # or qb-core / qbx_core

# Here you run your other resources like ox_target, ox_lib, ox_inventory, phone etc.

# APS Resources
ensure MugShotBase64  # optional, document photos
ensure aps_cityhall
```

Then restart the server, go to the City Hall blip on the map and talk to the clerk.

> [!TIP]
>
> On first start the console prints nothing unless something is wrong. Table upgrades are logged as `[aps_cityhall] Upgraded …`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.