How Integrations Work#
Nothing inside Licenses Creator knows which framework or inventory you run. Every call to the outside world goes through a small, unencrypted bridge file under client/custom/ or server/custom/, and the right one is picked by looking at what is running.
Layer | Supported | Without it |
|---|---|---|
Framework | es_extended · qb-core · qbx_core | Standalone: the player is their license identifier, money is a |
Inventory | ox_inventory · qs-inventory · qb-inventory · origen_inventory · tgiann-inventory | Required — licenses are items. The console warns on every boot. |
Target | ph-target · ox_target · qb-target | An |
Photo capture | screenshot-basic | The interface captures the frame itself, which on many builds comes out black. |
Photo host | Discord · Imgur · Cloudinary · Fivemanage · your own | Nothing that needs a new photo can be issued. |
Notifications | qs-interface | ox_lib notifications. |
3D prompt | qs-textui | A built-in drawn label. |
Character creation | qb-clothing · illenium-appearance · esx_skin | Starter licenses still fire on the framework's player-loaded event and |
Driving categories | ESX | The categories line on the driver license is left empty. |
Every optional integration degrades silently. A missing resource makes its feature stand down — the rest of the script carries on.
Inventory Items#
Two config keys decide how many items your inventory needs:
Key | Default | Meaning |
|---|---|---|
| true | One item per license, named after its template id in lowercase — |
| 'license' | The single item every license uses when the switch above is false, told apart by its label — Driver License - John Doe. |
| 'authorization_document' | One item for the whole server, shared by every license that needs approval. Added once. |
The creator's Inventory item setup button prints the snippet for the license you opened, already matching these settings. For reference, here is the ID card in each format — swap in the name the creator shows you, and add the authorization document only once. Restart the inventory afterwards.
ox_inventory/data/items.lua, images in ox_inventory/web/images/. The server.export line is what makes using the item show the card.
['id_card'] = {
label = 'Identification Card',
weight = 10,
stack = false,
close = true,
description = 'Official document issued by the government',
client = { image = 'id_card.png' },
server = { export = 'qs-licensescreator.useLicenseItem' },
},
-- once, for the whole server
['authorization_document'] = {
label = 'Authorization Document',
weight = 10,
stack = false,
close = true,
description = 'Approved authorization to present at the license office',
client = { image = 'authorization_document.png' },
},qs-inventory/shared/items.lua, images in qs-inventory/html/images/. On QBCore, qs-inventory reads qb-core/shared/items.lua instead — use the qb-inventory snippet there.
['id_card'] = {
['name'] = 'id_card',
['label'] = 'Identification Card',
['weight'] = 10,
['type'] = 'item',
['image'] = 'id_card.png',
['unique'] = true,
['useable'] = true,
['shouldClose'] = true,
['combinable'] = nil,
['description'] = 'Official document issued by the government'
},
-- once, for the whole server
['authorization_document'] = {
['name'] = 'authorization_document',
['label'] = 'Authorization Document',
['weight'] = 10,
['type'] = 'item',
['image'] = 'authorization_document.png',
['unique'] = true,
['useable'] = false,
['shouldClose'] = true,
['combinable'] = nil,
['description'] = 'Approved authorization to present at the license office'
},Items live in qb-core: qb-core/shared/items.lua, images in qb-inventory/html/images/.
id_card = { name = 'id_card', label = 'Identification Card', weight = 10, type = 'item', image = 'id_card.png', unique = true, useable = true, shouldClose = true, combinable = nil, description = 'Official document issued by the government' },
-- once, for the whole server
authorization_document = { name = 'authorization_document', label = 'Authorization Document', weight = 10, type = 'item', image = 'authorization_document.png', unique = true, useable = false, shouldClose = true, combinable = nil, description = 'Approved authorization to present at the license office' },Plain ESX keeps its items in the items database table. The picture still comes from your inventory's image folder. Restart es_extended afterwards.
INSERT INTO `items` (`name`, `label`, `weight`, `rare`, `can_remove`) VALUES
('id_card', 'Identification Card', 1, 0, 1)
ON DUPLICATE KEY UPDATE `label` = VALUES(`label`);
-- once, for the whole server
INSERT INTO `items` (`name`, `label`, `weight`, `rare`, `can_remove`) VALUES
('authorization_document', 'Authorization Document', 1, 0, 1)
ON DUPLICATE KEY UPDATE `label` = VALUES(`label`);Neither owns an item list on QBCore — they read the framework's. Use the snippet for the file yours reads:
Inventory | On QBCore | On ESX |
|---|---|---|
origen_inventory |
| The |
tgiann-inventory |
|
|
Images go in origen_inventory/html/images/ and tgiann-inventory/html/images/.
Every issued license writes its data into the item's metadata — the twenty card fields plus license_id, issued_id, generated_id, photo_url, label, description, image and imageurl — so another script can read a license straight from the inventory.
Item images#
The template's Inventory image is written into each item, so every license shows its own picture even on a single shared item. A plain file name is resolved against your inventory's image folder from Config.InventoryConfig; a full URL is used as is. To force one license's picture regardless of the template:
-- shared/advanced.lua
Config.ItemImageByLicenseId = {
['driver_license'] = 'my_custom_driver.png',
}Photo Upload#
Every license carries the holder's photo, taken once and reused on every license after that. Where it is stored is picked in Settings → Photo upload:
Provider | Needs |
|---|---|
Discord | A webhook URL. The photo is posted to that channel and its link is stored — use a private channel. |
Imgur | A client id. |
Cloudinary | The cloud name, plus either an unsigned upload preset or an API key and secret. |
Fivemanage | An API key. When possible the photo goes straight from the player to Fivemanage, which keeps large images off your server. |
Custom | Your own handler, below. |
For your own storage, define the handler in a server file that loads with the resource — server/custom/fields.lua is a good place — and pick Custom in Settings:
_G.PhotoUpload_CustomHandler = function(source, base64Data, cfg, callback)
-- upload base64Data wherever you like, then:
callback('https://your-cdn/photo.jpg', nil)
-- or on failure:
-- callback(nil, 'Upload failed')
endThe framing of the shot lives in shared/advanced.lua. If your host rejects uploads, lower quality and the maximum size first — it is almost always a file-size limit.
Config.PhotoCapture = {
encoding = 'jpg',
quality = 0.92, -- 0.0 to 1.0
maxWidth = 480,
maxHeight = 360,
camFov = 28.0, -- lower zooms in on the face
camDistance = 1.3, -- metres from the player
camOffsetSide = 0.0,
camOffsetUp = 0.05,
}Discord Logs#
Settings → Logs takes a webhook URL and decides what is posted:
Event | Carries |
|---|---|
License purchased | Player, citizen id, license, document number, price, first issue or duplicate. |
License reported | Citizen id, license, document number, stolen or lost. |
License cancelled | Citizen id, license, document number, and who cancelled it: admin, terminal, export or points. |
License about to expire | Citizen id, license, document number, days left — once a day per document. |
With the webhook type set to Discord each event is a coloured embed; with Fivemanage it is one line of text. Titles, colours and layout are in server/webhook.lua, which is open for you to change.
Custom Card Fields#
server/custom/fields.lua overrides any of the twenty fields a card can bind. What ships fills Dynamic content: a fixed line per license, and the driving categories the holder actually owns on the driver license.
local CONTENT_BY_TEMPLATE = {
['weapon_license'] = 'CLASS B - HANDGUN',
['pilot_license'] = 'SEP / MEP',
['fishing_license'] = 'ALL SEASONS',
['business_license'] = 'GENERAL TRADE',
}
local DRIVING_CATEGORIES = {
['driver_license'] = { 'drive_bike', 'drive', 'drive_truck', 'driver', 'motorcycle', 'truck' },
}
local CATEGORY_LABELS = {
drive = 'B', driver = 'B',
drive_bike = 'A', motorcycle = 'A',
drive_truck = 'C', truck = 'C',
}Change a line and the next card issued changes with it. To override any other field, add a getter for its key. It receives the holder's context and returns the text:
-- ctx.source, ctx.template, ctx.issued, ctx.firstName, ctx.lastName,
-- ctx.birthdate, ctx.gender, ctx.nationality, ctx.citizenid, ctx.jobLabel ...
LicenseFieldGetters.nationality = function(ctx)
return ctx.nationality ~= '' and ctx.nationality or 'Los Santos'
endThe keys are fullName, firstName, lastName, firstAndLastName, firstInitialLastName, lastNameFirstName, firstInitialDotLastName, firstNameDotLastName, dateOfBirth, nationality, citizenId, generatedId, codeGenerated, gender, job, expirationDate, issueDate, issuedBy, title and content. A getter that errors prints an empty field rather than breaking the card.
Overriding Detection#
Each layer is detected by looking for started resources in priority order — the first one found wins. Move a line up in shared/config.lua to give it priority, or pin a value outright:
-- Auto, the default: the first started resource in each list wins.
Config.Framework = detect(frameworks) or 'standalone'
Config.Inventory = detect(inventories)
-- Pinned:
Config.Framework = 'qb' -- 'esx', 'qb' or 'standalone'
Config.Inventory = 'qs' -- 'ox', 'qs', 'qb', 'origen' or 'tgiann'This is worth doing when two candidates run at once — an old inventory left started next to the new one — or when a resource has been renamed. Qbox is driven by the qb bridge.
Adding a Backend#
Supporting an inventory that is not on the list is deliberately small:
Copy the closest bridge
Duplicate
server/custom/inventory/ox.luaorqb.lua, change the guard at the top to your id, and point each function at your inventory's own exports. Every bridge implements the same eight:lua GetItems, AddItem, RemoveItem, RemoveItemBySlot, CanCarryItem, GetLicenseItems, GetLicenseItemBySlot, SetItemMetaDataTwo more are optional:
InventoryHasItemDefinitionpowers the missing-item warning on boot, andRegisterInventoryUsableItemhooks item use when the framework's own hook does not fire. Without them the framework's versions are used.Register it
Add your resource and id to the
inventorieslist inshared/config.lua, and its image folder toConfig.InventoryConfiginshared/advanced.lua.lua Config.InventoryConfig['mine'] = { resource = 'my-inventory', baseUrl = 'nui://my-inventory/html/images/' }Hook item use if needed
If using a license item does nothing, have your inventory call the export directly when it is used:
lua exports['qs-licensescreator']:useLicense(source, item, slot)
Frameworks extend the same way through server/custom/framework/ and client/custom/framework/. The server bridge is where player data comes from, so if your server stores something differently, replace the body of one function:
Function | Used for |
|---|---|
| Who holds a license — the citizen id stored on every record. |
| First name, last name, date of birth, gender, nationality. |
| Job and rank fields on the card, and ped access. |
| Creator access by admin group. |
| The job and grade lists in the Peds tab. |
| The driving categories printed on the driver license. |
| Paying for a license, and the refund if it cannot be handed over. |
| Showing the card when the item is used. |
On the client, SendTextMessage (notifications) and DrawText3D (the prompt) are the two to replace for another notification or text UI resource.
Languages#
41 translations ship in locales/: ar, bg, bn, ca, cs, da, de, el, en, es, et, fa, fi, fr, he, hi, hr, hu, id, it, ja, ko, lt, lv, ms, nl, no, pl, pt, ro, ru, sk, sl, sv, sw, th, tr, uk, ur, vi, zh. The language is picked in Settings → Validity → Panel language and switches for every player at once, notifications included.
Config.Languages is the list offered in that dropdown — trim it to what your staff uses. For a language of your own, copy locales/en.json to locales/<code>.json, translate it and add the code to the list. Translate Config.GenderLabels, Config.Nationality and the blip labels in shared/config.lua too — they are printed as written.



