The player picks a car, a duration and an account on a physical tablet — or reserves one from a phone app. The capsule rises out of the ground, opens its doors and hands the car over on the platform.
A server-authoritative vehicle rental system with physical underground delivery capsules, a world-space tablet, persistent rentals and, since v1.2.0, a public integration API. This manual covers v1.3.1.
What v1.2.0 added
Client and server exports intended for phone applications and other resources. An integration
can list stations and prices, reserve a vehicle remotely, route the player to the pickup,
monitor the reservation, cancel it, start a return and receive every lifecycle event — without
editing a single protected file.
Server-side by design
Everything that costs money or hands out a vehicle is decided on the server: identity, distance,
price, account, cooldowns, one rental per character, station locks and bay occupancy. The phone
never calculates or submits an authoritative price.
RequirementsFXServer with OneSync, Lua 5.4 (enabled by the manifest). No ox_lib and no required phone resource.
FrameworksQbox, QBCore, ESX, standalone rental credits, or a custom bridge. Keys via qbx_vehiclekeys, qb-vehiclekeys or custom.
Two station shapesA rental booth with a clerk and a screen, or a compact freestanding tablet, each placed separately from the capsule.
Escrow-friendlyConfig, both bridges, locales and the JSON state files are in escrow_ignore. No require anywhere.
Installation
Start it after your framework and key resource, give yourself the creator, place a station, reconnect once.
Download 9labs_capsulerent from Keymaster and extract the folder into the server's resources directory.
Start it after the framework and the key resource you use.
Give an administrator access to the station creator. Anyone with the command ACE
(creator.adminAce) already has it, which usually includes group.admin;
otherwise grant capsulerent.place (placeAce). With
framework = 'custom', Custom.isAdmin is an additional way in.
Restart the resource, reconnect once so the streamed models load, then run /capsuleplace and create at least one station.
# Only needed when the admin does not already have the command ACEadd_ace group.admin capsulerent.place allow
Keep your state files. Placed stations live in locations.json, reservations
and rentals in rentals.json. Preserve both across upgrades, and merge new keys into
your config.lua rather than overwriting it.
Placing a station
A station is two placements: the capsule that holds the car, and the terminal the player talks to.
/capsuleplace opens the creator. Aim at the ground within 35 m, Enter locks the
capsule, arrows nudge it, Q and E rotate, Page Up and Page Down change height, Shift makes every
step fine, WASD moves you around, the right mouse button looks around and Esc cancels. Choose
which side of the terminal the capsule sits on.
Choose Rental booth (booth with a clerk and a screen) or
Freestanding tablet (compact pedestal) and place it separately — at most 20 m
from the capsule and clear of the bay.
Name the station, pick the strip colour (a preset or any hex colour) and the map blip — on or
off, label, sprite, colour, scale, short range and flashing — then save.
The save refuses ground that is not flat: all four corners of each footprint must stand within
23 cm of each other. You must be within creator.maxDistance (35 m) of both the
capsule and the terminal when saving. /capsuleremove edits the nearest placed
station within the same 35 m; stations from config.lua are skipped, and a station
with a reservation or rental tied to it cannot be edited or deleted until that ends.
creator.minimumSpacing (12 m) keeps two stations apart and
creator.maxLocations (100) caps the total. Both command names can be changed with
creator.command and creator.removeCommand.
Ground offsets
creator.capsuleGroundOffset is 0 — the capsule sits at terrain level — while
creator.boothGroundOffset lifts the rental booth 3 cm so it does not z-fight with the
pavement. The freestanding tablet stays on the ground. Cursor snapping and saving use each
component's own offset.
Fixed stations
Stations written into the locations table in config.lua are fixed and
cannot be edited in game — handy for a map you ship with the server. Existing stations saved
before the booth/tablet split keep their original combined layout.
config.lua
Every operator-facing option lives here. Restart the resource after a change; a model or YTYP update also needs a client reconnect.
Key
What it does
locale
Notification and creator language: en, bs, de.
framework
auto, qbx, qb, esx, standalone or custom. auto takes the first of qbx_core, qb-core or es_extended that is running, otherwise standalone.
keys
auto (qbx_vehiclekeys or qb-vehiclekeys when running), qbx, qb, none or custom. qb uses the server exports of qb-vehiclekeys 1.6 and newer; older versions get the old client event.
interaction
Exactly one is used: 9labs_ui (built-in E prompt, the default), ox_lib TextUI, ox_target or qb-target (eye only, no E prompt), or custom.
interactKey, interactKeyLabel, interactDistance
Control id (38 = E), the label the prompt shows, and the 3 m reach to the tablet.
targetIcon
Icon for the ox_target and qb-target options (fa-solid fa-car).
prompt
title and scale (0.75–1.5) of the built-in prompt. Used only with interaction = '9labs_ui'.
notify
9labs_ui, ox_lib (default, falls back to 9labs_ui when ox_lib is not running), native or custom.
Title, subtitle, ticker and accent on the world tablet and the sign. animation = false keeps the textures static.
tablet
DUI texture resolution and the inspect camera: cameraDistance 1.20 m and cameraFov 40 fill about 91% of screen height.
npc
The clerk in the booth: enabled, model, seat offsets, seatHeight, heading and the seated animation. The chair is part of the model, not a spawned prop.
glow
Default strip color and the presets offered in the creator; any hex colour is also accepted. DUI replacement is shared, so the nearest station's colour wins.
spawnDistance / despawnDistance
85 m and 100 m. Stations are created around the player rather than all at once.
defaultSide
Creator default (right), left or right; each station can differ.
placeAce
ACE that opens the creator (capsulerent.place).
creator
command and removeCommand (capsuleplace, capsuleremove); adminAce (command), a second ACE that also opens the creator; maxDistance (35 m), the placement reach and the server-side proximity for creator saves, tablet menus and vehicle recovery; minimumSpacing and maxLocations; moveStep, rotateStep and precisionMultiplier (the Shift factor); and the ground offsets.
locations
Fixed stations written into the config, see Placing a station. Leave it empty when you place stations in game.
blips
Map icon defaults (enabled, sprite, color, scale, shortRange, label) plus the sprites and colors lists offered in the creator. Each station can override them, including label and flashing.
Volume 0–1 (0.4), audible distance (30 m), full-volume distance (3 m) and rolloff (1.4).
sounds
The rise/door/sink cues on a 30 fps, 0–600 frame timeline, each with its own volume.
ui
Creator and rental HUD theme: colours, currency, number format and screen anchors.
debug
Step-by-step output in F8.
Do not convert this resource to require. Every runtime script is loaded
directly by fxmanifest.lua, which is what keeps the escrowed build working.
Fleet and pricing
A plan sets the duration and the base price; each vehicle carries a multiplier. The tablet and the integration API use the same fleet, plans, accounts and stations.
The server always calculates selected plan price × selected vehicle multiplier.
A medium plan at 250 with a sultan multiplier of 1.5 costs 375. Phone
apps read the ready-made prices table on each vehicle and never submit a price.
Timings
Key
Meaning
reservationTimeout
Seconds a paid reservation holds its pickup station (600).
autoReleaseDistance
Reserved vehicles are handed over automatically inside this radius (10 m).
returnDistance
How close the rental must be to open a return capsule (18 m).
returnTimeout
Seconds to drive into an opened return bay before it may close (180).
warningSeconds
How long before the end the renter is warned (60).
requestCooldownMs
Rate limit on repeated requests from one player.
The car has to fit
The capsule takes a model up to 2.3 m wide, 5.6 m long and 2.0 m high. Anything larger will not
close. Images are relative to web/; put the PNGs in web/images/.
Business stations
Every station is either run by the city or sold to a player who runs it: buys the fleet, sets the prices, hires staff and keeps the rental money.
Public or for sale
In the creator pick Public or For sale and, for a for-sale
station, its purchase price. A public station works exactly as before: it rents every vehicle
from rental.vehicles at plan price × multiplier and the money goes nowhere. A
for-sale station rents nothing until a player buys it and puts vehicles into its fleet.
business = { enabled = true, defaultPrice = 250000, -- purchase price suggested in the creator ownerShare = 1.0, -- part of each rental paid into the station balance sellBackFraction = 0.5, -- selling to the city / removing a fleet unit returns this part maxEmployees = 10, maxStock = 20, -- units per vehicle per station maxRentalPrice = 100000, -- highest price an owner may set for one rental hireDistance = 8.0, -- hire or transfer only to players standing this close},-- in rental.vehicles: what an owner pays for one unit of the fleet{ id = 'sultan', model = 'sultan', label = 'Sultan', multiplier = 1.5, purchasePrice = 28000 },
Buying and running a station
Buying: the tablet of an unowned station shows the price and a Buy this station button (cash or bank). The buyer becomes the owner.
Managing: owner and staff get a Manage button on the tablet. It opens a panel with Overview, Fleet, Staff and Owner tabs.
Fleet: units are bought with the station balance at each vehicle's purchasePrice. A customer can rent a vehicle only while a unit is parked at the station; it counts again when the rental ends. Sell 1 returns sellBackFraction of the purchase price to the balance.
Prices: one price per vehicle and plan, from 1 to maxRentalPrice. The tablet and the phone exports use the station's own prices.
Income: ownerShare of each rental goes into the balance when the car is handed over. A cancelled or expired reservation pays nothing.
Selling: selling to the city pays sellBackFraction of the purchase price plus the balance and clears staff and fleet. It is refused while a vehicle of the station is out on rent.
Roles
Role
Can do
Staff
View the dashboard, switch vehicles on and off.
Manager
Also set prices, buy and sell fleet units, deposit money, hire and fire staff.
Owner
Also withdraw, hire and fire managers, transfer the station to another player and sell it.
Removing a for-sale station in the creator, or switching it back to Public, pays the
owner the full purchase price plus the balance. An owner who is offline is paid the next
time they use any rental tablet. Every action is checked on the server (role, distance,
balance) and saved to businesses.json; a failed save is rolled back and the
money returned.
Standalone credits
With framework = 'standalone' — or auto with no framework running — rentals are paid with credits kept per FiveM license in resource KVP storage.
These are not framework money and never touch a player's bank account.
standalone.startingCash (1000) and standalone.startingBank (0) are
granted once, the first time a license is seen. Refill from the console, or as a creator admin
in game:
capsulecredits <playerId> <amount> [cash|bank]
Free rentals: set every plan price to 0. The flow, cooldowns and the one-rental-per-character rule stay exactly the same.
Rental lifecycle
Since v1.2.0 a rental has states, and the timer does not start until the car is actually in the player's hands.
reserved -> handover -> active -> returning -> record removed (rentalReturned) | +-> cancelled / reservation expired (refunded, record removed)active -> rental expired (record removed, no refund)
Status
What is true
reserved
Payment succeeded and the pickup station is held for this character. No vehicle exists and the rental timer has not started.
handover
The player reached the server-checked pickup radius and the capsule is opening. The vehicle exists but is frozen and cannot be driven.
active
The capsule is fully open, the vehicle is unfrozen and keys are issued. Only now do handedOverAt and expiresAt exist.
returning
The rented vehicle passed the final return checks and the capsule is closing.
These four are the only statuses an integration sees. When the return animation completes, the
record is deleted and the vehicle removed: GetRental returns nil, and
rentalReturned fires with the last snapshot, whose status is still
returning and whose reason is returned.
Automatic handover
The client runs a low-frequency proximity watcher. When a reserved player enters
autoReleaseDistance, it requests handover on its own. The server independently
validates character identity, routing bucket, player health, actual coordinates, station
ownership, the station lock, capsule occupancy and duplicate requests before it spawns anything.
Do not add a second proximity loop in a phone app unless the UI needs one. The built-in watcher already handles it.
Renting and returning
The world tablet drives both halves for a walk-up rental. Returning is deliberately physical: the car has to be on the platform.
Renting at the station
Walk up to the tablet — E prompt or target — and choose a car, a duration and an account.
Confirm. The capsule rises, opens and releases the car with keys and a full tank.
Returning
Bring the car within returnDistance of any station.
Open the return capsule from the tablet, or from the bay option when you use a target.
Drive onto the raised platform within returnTimeout, step out, and confirm beside the bay.
The car must be stopped and completely inside the platform bounds, with no ped or unrelated
vehicle in the closing area. A rental survives reconnects and restarts; if the vehicle is lost,
the record is marked recoverable and the tablet offers to recover it for the
remaining time. Time keeps running while the player is offline, the renter is warned
warningSeconds before the end, and an expired rental removes the car without
refunding unused time.
Storage and refunds
Three documents, kept as JSON files in the resource folder or in one database table, all written before money moves.
locations.jsonStations placed in the creator.
rentals.jsonReservations and rentals, including ones whose owner is offline.
businesses.jsonStation owners, staff, fleets, balances, history and payouts waiting for offline owners.
auto (default): JSON files in the resource folder while the server can write them. Some hosts (ZAP-Hosting and other shared hosts) do not let the server write its own resource folder. Then it switches to the database by itself through oxmysql: it creates the table capsulerent_storage, copies the three files into it once and uses the table from then on. Nothing to import by hand.
files: JSON files only.
database: the oxmysql table only, created automatically.
For the database, start oxmysql before the resource:
ensure oxmysqlensure 9labs_capsulerent
The console names the storage on every start: Storage: JSON files in the resource folder or
Storage: database table capsulerent_storage (oxmysql). Once the table holds data it stays the
source on every restart, so the data never goes back to old files. Database writes are queued and only the
newest copy of each document is written, so players never wait on MySQL; a failed write is retried until it
goes through.
The rules the server enforces
One character can hold only one reservation or rental record.
A pickup station can hold only one reservation and one active capsule cycle.
A reservation is written before payment is requested, then finalized after payment succeeds.
A handover that fails on model load or fit cancels and refunds the reservation automatically.
Cancellation and online reservation expiry refund the original account.
An active rental keeps counting down while disconnected, and expiry refunds nothing.
Offline expiry is not auto-refunded. If a paid reservation expires while its owner is
offline, the record moves to refund_pending and the console prints the identity,
amount and account for manual reconciliation — which avoids issuing the same refund twice on a
framework without an idempotent offline-money API.
Never delete or edit rentals.json (or the capsulerent_storage rows) while the resource is running. Back it up first.
Integration API: overview and data
Any phone can integrate by calling the documented Lua exports from its own client or server code. No protected file is edited, and no phone resource is required.
Return shape
Mutation and network-backed client exports return one of these two shapes. Use code for application logic and error for display — it is already translated for the player.
{ ok = true, ... }{ ok = false, code = 'stable_code', error = 'Translated message for the player' }
Catalog
GetCatalog returns ready, the stations map,
vehicles, plans, accounts, reservationTimeout
and autoReleaseDistance. On the server export, check ready before
showing any purchase action. The client export returns the same catalog in the
catalog field of an ok result, and returns rate_limit
instead while the resource is still loading or when it is called again within 500 ms, so a
client never sees ready = false.
Station
{ id = 'placed:...', name = 'Legion Square', coords = { x = 0.0, y = 0.0, z = 0.0 }, heading = 90.0, side = 'left', version = 'booth', -- 'booth' or 'tablet'; nil for the original combined layout color = '#00e5b4', -- strip colour, nil when the station uses glow.color available = true, revision = 1, fixed = false, terminal = { coords = { x = 0.0, y = 0.0, z = 0.0 }, heading = 180.0, }, blip = { sprite = 225, color = 3, label = 'Legion Square' }, -- only when overridden}
terminal is optional, and version is nil for stations
without a separate terminal; use the main coords for pickup GPS routing.
color and blip carry the station's own strip colour and map blip
override when it has one.
available is false while the station holds a reservation or an active capsule cycle,
and the server repeats every availability and occupancy check when an action is actually requested.
Vehicle
{ id = 'sultan', model = 'sultan', label = 'Sultan', category = 'Sports sedan', image = 'images/sultan.png', multiplier = 1.5, prices = { short = 225, medium = 375, long = 600, },}
prices is added to catalog and export copies, already calculated from server config.
After handover, status becomes active, netId holds the
networked vehicle ID, handedOverAt and expiresAt are populated, and
remaining counts down. recoverable becomes true only when an active
persisted rental no longer has a live vehicle, such as after a resource restart.
Server exports
For the authoritative side of a phone resource. Every player-specific export takes the real FiveM server ID first.
local Capsule = exports['9labs_capsulerent']Capsule:GetCatalog() -- whole catalog, check catalog.readyCapsule:GetStations() -- station map keyed by station idCapsule:GetAvailableVehicles(stationId) -- priced fleet, {} while the station is busy or unknownCapsule:GetRental(source) -- public reservation/rental state, or nilCapsule:ReserveVehicle(source, { id = stationId, vehicle = 'sultan', plan = 'medium', account = 'bank', route = true,})Capsule:CancelReservation(source)Capsule:RequestHandover(source, stationId)Capsule:EndRental(source, stationId)Capsule:ConfirmReturn(source, stationId)
Never accept a client-supplied source ID. Use the source of the server
event or callback the request arrived on.
ReserveVehicle
Performs the authoritative identity, duplicate ownership, station availability, account, balance,
price, durable storage and payment checks. It does not spawn the vehicle or start the
rental timer. On success it returns ok, the rental and the
station. GPS routing defaults to enabled; pass route = false when the
app routes the player itself. A positional form is also accepted:
local result = exports['9labs_capsulerent']:ReserveVehicle( source, stationId, 'sultan', 'medium', 'bank', true)
The rest
CancelReservation — only reserved can be cancelled. It refunds through the same account and bridge and unlocks the pickup station. A handover already in progress cannot be cancelled.
RequestHandover — normally the built-in watcher calls this around 10 m. It is public for a manual retry or status action; server distance and ownership checks always apply.
EndRental — starts the physical return at a nearby station. The rented vehicle must exist and be inside returnDistance. It opens the capsule; it does not bypass parking or safety checks.
ConfirmReturn — call after the vehicle is stopped on the raised platform and the renter has exited. The server verifies the entity, ownership, routing bucket, exact platform bounds, speed and a clear closing area.
Client exports
Network exports yield while waiting for the protected server callback and return a result table, so call them from normal event, thread or callback context.
local Capsule = exports['9labs_capsulerent']Capsule:GetCatalog() -- network call, returns { ok, catalog }Capsule:GetStations() -- local synchronized map, no networkCapsule:GetAvailableVehicles(stationId) -- network call, returns { ok, vehicles }Capsule:GetRental() -- immediate local stateCapsule:RefreshRental() -- authoritative refresh, returns { ok, loaded, rental }Capsule:ReserveVehicle(stationId, 'sultan', 'medium', 'bank', true)Capsule:SetPickupRoute(stationId, coords) -- coords optional; true when the waypoint was setCapsule:CancelReservation()Capsule:RequestHandover(stationId) -- defaults to the reservation's stationCapsule:EndRental(stationId) -- defaults to the nearest stationCapsule:ConfirmReturn(stationId)
Local versus authoritative
GetStations and GetRental answer immediately from local state kept
current by server pushes — no network request. RefreshRental is the authoritative
read; use it when opening a phone screen, where one request is worth the certainty. Its result
also carries loaded, false while the player's character is not loaded yet.
Throttling and timeouts
RefreshRental is limited to one call per 2 seconds per player, and the resource itself polls the same status in the background. A call inside that window, or before the resource is ready, returns unavailable; fall back to GetRental rather than treating it as an error.
GetCatalog and GetAvailableVehicles are limited to one call per 500 ms each and return rate_limit inside that window or while the resource is loading.
Every network export returns connection_error when the request cannot be sent or no answer arrives within 15 seconds.
Defaults
Routing defaults to enabled, and on success the waypoint is set to the capsule pickup
coordinates; SetPickupRoute(stationId, coords) does the same on its own and routes
to the station's main coords. When the station is not in the local map it uses the
optional coords table instead. It returns true when a waypoint was set.
The station argument is optional on
RequestHandover (defaults to the reservation's pickup station),
EndRental and ConfirmReturn (nearest synchronized station). Every final
distance, vehicle and ownership check stays server-side.
Lifecycle events
Every lifecycle change fires a local server event and, while the player is online, a targeted network client event.
Event suffix
When it fires
reservationCreated
Payment and persistent reservation succeeded.
reservationCancelled
Reservation was cancelled and refunded.
reservationExpired
The pickup timeout elapsed.
vehicleHandedOver
Capsule opened, timer started, vehicle released and keys issued.
rentalReturnStarted
Final platform checks passed and the capsule started closing.
rentalReturned
Return animation completed, state cleared and vehicle removed.
rentalExpired
Active rental timer elapsed and the vehicle and state were removed.
Arguments are (src, data), where data.owner is the stable bridge
identity. src is 0 when an expiry happens while the player is offline.
On both sides data is the rental snapshot plus event; the ending events
(cancelled, reservation expired, returned, expired) also carry reason and hold the
last state before the record was removed.
Client listener
RegisterNetEvent('9labs_capsulerent:client:vehicleHandedOver', function(data) if source ~= 65535 then return end print(('Rental active until %s'):format(data.expiresAt))end)
Keep the source == 65535 check so another client resource cannot spoof a server lifecycle notification.
The older local server events capsulerent:rentalStarted and capsulerent:rentalEnded still fire as well; see Legacy exports.
Phone workflow
The recommended order, and two examples that already follow it.
When the rental app opens, call GetCatalog and RefreshRental.
With no current rental, render only stations whose available is true.
Render catalog.vehicles, catalog.plans and each vehicle's exact prices[plan.id].
Submit station, vehicle, plan and account IDs to ReserveVehicle.
On success show reserved and route the player to result.station.coords.
On vehicleHandedOver, switch the UI to the active countdown using expiresAt.
To return, call EndRental near a station, guide the player onto the platform, then call ConfirmReturn after they exit — the built-in world interaction can also do the final confirmation.
Client-only integration
local Capsule = exports['9labs_capsulerent']local function openRentalApp() local catalogResult = Capsule:GetCatalog() if not catalogResult.ok then return { ok = false, code = catalogResult.code, error = catalogResult.error } end local rental local rentalResult = Capsule:RefreshRental() if rentalResult.ok then rental = rentalResult.rental elseif rentalResult.code == 'unavailable' then rental = Capsule:GetRental() -- refreshed moments ago; local state is current else return rentalResult end return { ok = true, catalog = catalogResult.catalog, rental = rental, }endlocal function reserveFromPhone(data) return Capsule:ReserveVehicle({ id = data.station, vehicle = data.vehicle, plan = data.plan, account = data.account, route = true, })end
Server-backed callback
local Capsule = exports['9labs_capsulerent']RegisterNetEvent('my_phone:server:reserveCapsule', function(data) local src = source if type(data) ~= 'table' then return end local result = Capsule:ReserveVehicle(src, { id = data.station, vehicle = data.vehicle, plan = data.plan, account = data.account, route = true, }) TriggerClientEvent('my_phone:client:capsuleResult', src, result)end)
The example deliberately uses the real server event source. Capsule Rental still
validates every ID, the money, ownership, locks and distance itself, but the phone resource
should also apply its normal callback authentication and request throttling.
Error codes
Stable codes for your logic, translated messages for the player.
Code
Meaning
invalid
Invalid IDs, state or request data.
unavailable
Resource storage, framework or player state is not ready. RefreshRental also returns it when called again within 2 seconds.
rate_limit
The player repeated the request too quickly. Client GetCatalog and GetAvailableVehicles also return it while the resource is still loading.
connection_error
A client network export could not reach the server or got no answer within 15 seconds.
busy
Station, owner or capsule is already processing another action.
already_rented
This character already owns a reservation or rental.
insufficient
The selected server-side account cannot cover the calculated price.
too_far
Server coordinates are outside the required pickup or return radius, or the player is too far from the tablet when opening its menu or recovering a vehicle.
model_failed
The configured vehicle cannot load, fit or spawn safely.
storage_failed
Persistent JSON state could not be saved.
refund_failed
The payment bridge could not return reservation funds. The record stays in refund_pending in rentals.json for manual reconciliation.
bay_blocked
A ped or vehicle blocks the capsule bay.
bring_scooter
The rented vehicle is not close enough to the return station.
vehicle_moving
The returned vehicle has not stopped.
park_straight
The vehicle origin is outside the physical platform bounds.
clear_platform
A ped or unrelated vehicle remains inside the closing area.
biz_sold_out
The business station has no free unit of that vehicle (none bought, all rented out, or switched off).
Station creator
The creator also returns rate_limit, storage_failed, busy, invalid and too_far (terminal more than 20 m from the capsule, or the admin further than creator.maxDistance), plus these:
Code
Meaning
no_permission
The player has neither ACE and, with framework = 'custom', Custom.isAdmin did not allow it.
terminal_clear
The booth or tablet stands on the capsule platform.
changed
The station was edited elsewhere, or it is a fixed station. Reopen the creator.
in_use
A reservation or rental is tied to the station.
limit
creator.maxLocations has been reached.
spacing
Another station, or its terminal, is closer than creator.minimumSpacing or overlaps.
Framework, money and keys
Both bridge files are open and escrow-exempt, and each starts with a CUSTOM block. Every built-in adapter lives below that block and needs no edits.
Server bridge
-- server/bridge.lua, CUSTOM block (used when framework = 'custom')-- Must return the same value every time this character logs in; nil while not loaded.function Custom.identity(src) return GetPlayerIdentifierByType(src, 'license') -- or your character idend-- account is 'cash' or 'bank'function Custom.getMoney(src, account) return 0 -- return the account balanceend-- amount is a whole number, already checked against getMoney; reason is 'capsule-rental'function Custom.removeMoney(src, account, amount, reason) -- take the money here return true -- true only when it was takenend-- reason is 'capsule-refund' for refundsfunction Custom.addMoney(src, account, amount, reason) -- pay the money back here return true -- true only when it was paidend-- Extra creator check on top of the ACE permissions in config.luafunction Custom.isAdmin(src) return falseend-- used when keys = 'custom'; vehicle is a server entity handlefunction Custom.giveKeys(src, vehicle, plate)endfunction Custom.removeKeys(src, vehicle, plate)end
Custom.identity, getMoney, removeMoney, addMoney
and isAdmin are used when framework = 'custom';
giveKeys and removeKeys when keys = 'custom'.
identity keys every reservation and rental, so it must be stable across logins — the shipped default is the FiveM license. Return a character id instead when players have several characters.
removeMoney and addMoney must return exactly true on success. Anything else counts as a failure: a failed charge returns insufficient, and a refund that does not return true fails with refund_failed. The shipped placeholders return false, so fill both in before switching to custom.
giveKeys and removeKeys receive the server vehicle entity and its trimmed plate.
Client bridge
-- client/bridge.lua, CUSTOM block-- key is interactKeyLabel ('E'), or nil for plain status text; text is already translatedfunction Custom.showPrompt(text, key)endfunction Custom.hidePrompt()end-- data = { description = 'text', type = 'success' | 'error' | 'inform' }function Custom.notify(data)end-- A sphere zone: coords is a vector3 centre, radius and distance are metres.-- options: list of { label, icon, onSelect = function() end, canInteract = function() end }function Custom.addTarget(id, coords, radius, distance, options) return nil -- return a handle, or nil for no targetend-- handle is what addTarget returnedfunction Custom.removeTarget(handle)end
showPrompt, hidePrompt, addTarget and
removeTarget are used when interaction = 'custom'; notify
when notify = 'custom'. The prompt functions only draw: the resource still listens
for interactKey itself. addTarget registers a sphere zone around the
tablet or the capsule bay, not an entity, and whatever it returns is passed back to
removeTarget.
Nothing in the bridges is a network endpoint. Prices, ownership, distance and cooldowns
stay in the protected code, so an edited bridge cannot be used to hand out free cars.
Legacy exports
Integrations written before v1.2.0 keep working unchanged.
-- serverexports['9labs_capsulerent']:getStations()exports['9labs_capsulerent']:getRental(source)-- server, local eventsAddEventHandler('capsulerent:rentalStarted', function(src, rental) end)AddEventHandler('capsulerent:rentalEnded', function(src, data) end) -- data = { reason, station }-- clientexports['9labs_capsulerent']:getLocations()exports['9labs_capsulerent']:getNearestStation()exports['9labs_capsulerent']:openStation(stationId)exports['9labs_capsulerent']:previewStation(stationId, delay, vehicle, hold)exports['9labs_capsulerent']:previewStation(stationId, delay, { vehicle = 'sultan', hold = 8 })exports['9labs_capsulerent']:clearPreview(stationId) -- no id clears every preview
capsulerent:rentalStarted fires when a handover completes, with the public rental.
capsulerent:rentalEnded fires when a rental is returned or expires, with
reasonreturned or expired and the station id;
src is 0 when the player is offline.
previewStation plays one open, hold and close cycle with a local car and no rental
behind it, for trailers and camera work. stationId may be 'near' (the
station closest to the rendered camera), delay is seconds before it starts,
vehicle a model name or 'none' ('false' works too), and
hold the seconds the capsule stays open. The third argument may instead be an
options table with vehicle and hold fields. The station must be loaded on that client,
so stay within spawnDistance of it. clearPreview without an id clears
every preview. The same from chat, where clear stops all previews:
/capsulepreview near sultan 0 8/capsulepreview clear
Troubleshooting
Start here
/capsulediag prints the state of every loaded station and its vehicle. Set
debug = true for step-by-step output in F8.
Everything says "Saving failed"
The server could not save its data. Look at the console right after the resource starts. If it says the
server cannot write files in the resource folder and oxmysql is not running, start oxmysql before
9labs_capsulerent (with storage = 'auto') and restart. The table is created by itself.
The catalog is empty
Confirm 9labs_capsulerent is actually started.
Confirm at least one station exists in locations.json or config.lua.
Check catalog.ready on the server export (the client export returns rate_limit until it is ready) and the server console for invalid storage errors.
A reservation returns busy
Refresh the catalog and inspect station.available.
Make sure another reservation or capsule cycle does not already own the station.
Clear vehicles and peds physically blocking the capsule footprint.
The reservation works but no capsule opens
Confirm the rental state is reserved.
Confirm the player is inside autoReleaseDistance, in routing bucket 0 and alive.
Call RequestHandover once and display its stable code.
Verify the configured vehicle model exists and fits the capsule dimensions.
Handover worked but the timer already lost time
Inspect handedOverAt and expiresAt. Since v1.2.0, expiresAt
must be absent during reserved and handover, then equal
handedOverAt + durationSeconds once the status becomes active.
The return is rejected
EndRental needs the actual rental vehicle near the selected station.
ConfirmReturn needs the player on foot, the vehicle stopped and completely inside the raised platform.
Show the returned code to tell bring_scooter, vehicle_moving, park_straight and clear_platform apart.
The models are invisible after the first start
Reconnect once. Streamed assets added by a freshly started resource are not loaded for a client already in the session.
The creator refuses to save
The flatness check failed: all four corners of each footprint must stand within 23 cm of each
other. The terminal must also be within 20 m of the capsule and clear of the bay.