Place stations in game, set the plans, and let the bunker rise, open its hatch and push a scooter out on a fork. Everything that costs money is decided on the server.
A server-authoritative scooter rental. The player picks a duration and a payment account at a station, the underground bunker rises, opens and physically dispenses the configured scooter. It can be returned at any SCOOT station.
Scoot Rental is currently not on sale. Rockstar removed the scooters it was built to
rent, so the product has been withdrawn until it is reworked, and it will go back on sale
after that. This manual stays online for existing owners.
What is in the resource
Animated underground rental bunkers, and optional independently placed world-space DUI tablets.
An in-game station creator, with placed stations and active rentals persisted to disk.
Qbox, QBCore, ESX, standalone and custom framework support, plus optional vehicle keys.
Built-in prompt, ox_lib, ox_target, qb-target and custom interaction modes.
Configurable plans, accounts, colours, logos, blips, sounds, UI placement and language (English, Bosnian, German).
Server and client exports for phone applications, and lifecycle events for start, return and expiry.
The server decides everything that costs money. A client or phone application
cannot choose the price, bypass the station distance, create a second rental or grant
itself ownership by calling an export.
Requirements
One framework choice, one streamed scooter model, OneSync. Everything else is optional.
ServerFiveM/FXServer with OneSync enabled.
Scooter modelThe resource does not include or stream a scooter. ScootConfig.scooterModel must name a model your server streams or a vanilla vehicle. The default, labscoot, comes from the separate 9labs_scooter resource.
FrameworkQbox (qbx_core), QBCore (qb-core), ESX (es_extended), standalone rental credits, or a custom server bridge.
Vehicle keysOptional: qbx_vehiclekeys, qb-vehiclekeys, or a custom bridge.
Optional resources
ox_target, qb-target and ox_lib are only needed when
the configuration selects them.
ox_lib is not a dependency. The built-in 9labs_ui prompt and
notifications work without it, and the shipped notify = 'ox_lib' falls back to
the built-in notification when ox_lib is not running. Do not remove ox_lib from your server merely because
SCOOT does not need it — other resources or your framework may still depend on it.
Installation
Start it after the framework, target and key resources, then place a station.
Extract the delivered 9labs_scootbunker folder into your server's resources directory.
Make sure the configured scooter model is streamed, and that rental.vehicleType matches the type declared in its vehicles.meta. For the default labscoot, install and start the separate 9labs_scooter resource.
Start the framework, target, key and scooter resources before SCOOT.
Give administrators access to the station creator. Anyone who already has the command ACE can use it without extra setup; to grant it to others, add the scootbunker.place ACE below.
Configure config.lua, start the resource, and reconnect once after a model or YTYP update so the streamed assets refresh.
Run /scootplace in game to place stations.
Example for Qbox with ox_target, Qbox vehicle keys and the default labscoot scooter:
Merge new configuration keys into your config and preserve locations.json and
rentals.json. Stations saved by an older version keep working — they do not
need converting to the newer tablet station format.
Asset Escrow and editable files
The release is built for Cfx Asset Escrow. These files are explicitly excluded from encryption by fxmanifest.lua:
Frameworks, money, keys, targets, prompts, notifications, translations, logos and storage
are all configurable without touching a protected implementation file.
Bunker and tablet stations
Every station has a bunker plus a separate static rounded frame. The frame does not animate with the machine underneath it.
Bunker interaction
The player interacts at the bunker service point. Depending on the configuration, the
resource opens its built-in rental menu, or fires the external UI event for your own
interface.
DUI tablet
A tablet station adds a physical world-space tablet that is positioned and rotated
independently of the bunker. It has to stay outside the bunker and the scooter exit path,
and within tablet.maxDistance of its bunker.
The prompt or target moves to the tablet, but the scooter is still released by the bunker.
Tablet stations always use their physical DUI.rental.externalUi
replaces the menu for bunker-only stations, never for a tablet.
In-game creator
Aim at the ground, lock the position, adjust it and save. The creator shows its current keyboard controls on screen.
/scootplace -- open the creator to place a new station/scootremove -- open the nearest placed station within 20 m to edit or delete it/scootglow <texture> -- paint one kiosk texture to find the glow strip-- only registered when debug = true/scootdiag -- animation and bone diagnostics for the nearest bunker/scootsound ... -- sound diagnostics
The creator places bunker-only and DUI-tablet stations. Placed stations are stored in
locations.json and can be edited or deleted in game. To change an existing one,
stand near it and run /scootremove: it opens the nearest placed station within
creator.maxDistance, or reports that no station is nearby. Stations defined in
config.lua are fixed and cannot be edited through the creator.
With groundAlign enabled, the bunker and its frame follow mild ground slopes;
anything steeper than maxSlope is clamped. The gap between
spawnDistance and despawnDistance is deliberate — it stops a
kiosk flickering while a player stands on the edge of the radius. With
propCollision enabled the kiosk is solid and players cannot walk through it.
Logo, glow and blips
One PNG brands every bunker, and each station can carry its own strip colour and map icon.
Your logo
Place a transparent 1024×1024 PNG at logo/logo.png and restart the resource.
It is composited at runtime over the supplied door and roof base textures, so give it a real
alpha channel for clean edges. Without an image there, the artwork baked into the model
stays.
logo = { enabled = true, file = 'logo/logo.png', dictionary = 'scoot_bunker', surfaces = { door = { texture = 'scoot_door', base = 'logo/base_door.png', x = 0.50, y = 0.50, size = 0.72, turn = 0 }, roof = { texture = 'scoot_roofpanel', base = 'logo/base_roof.png', x = 0.50, y = 0.40, size = 0.78, turn = 0 }, },}
Each surface names the texture it replaces and its base image. x,
y and size are fractions of the plate, so 0.5 is its
middle; turn rotates the logo. logo.enabled = false keeps the
model's own artwork.
Glow
Colours accept #rrggbb, rrggbb, 0xRRGGBB,
#rgb or aarrggbb. With glow.perStation = true a
placed station stores its own colour. The emissive strip uses one shared runtime texture,
so the nearest station's colour is the one displayed as the player moves between stations.
color is the default strip colour and presets are the colours
offered in the creator; any other hex can still be typed in. textures should
stay scoot_trim, the only texture the emissive strip uses; adding one of the
other kiosk textures repaints that whole surface. /scootglow <texture>
paints a single texture so you can see which is which.
Blips
Every station gets a map blip unless it is disabled globally or individually. Placed
stations may override sprite, colour, scale, short-range mode, flashing and label from the
creator.
label is used when a station has no name. flashInterval is in
milliseconds and only applies while flash is on. sprites and
colors are the buttons offered in the creator; any other id can still be typed
in, and the blip updates on the map while you edit.
Framework and keys
Normal setup happens in config.lua through the global ScootConfig table.
Framework
Value
Behaviour
auto
First running resource, in this order: Qbox, QBCore, ESX; otherwise standalone.
qbx
Uses qbx_core.
qb
Uses qb-core.
esx
Uses es_extended.
standalone
Persistent SCOOT rental credits stored per FiveM license.
custom
The functions at the top of server/bridge.lua.
Pin it on a production server. With auto, a change in resource start order can change which adapter is chosen.
Vehicle keys
Value
Behaviour
auto
qbx_vehiclekeys, then qb-vehiclekeys, when one is running.
qbx
Qbox vehicle keys.
qb
QBCore vehicle keys.
none
No keys are issued or removed.
custom
Custom.giveKeys and Custom.removeKeys in server/bridge.lua.
Built-in E prompt. No external target or UI library required.
ox_lib
ox_lib TextUI prompt, falling back to the built-in prompt when ox_lib is unavailable.
ox_target
ox_target eye interaction only, no E prompt.
qb-target
qb-target eye interaction only, no E prompt.
custom
The prompt and target functions in client/bridge.lua.
The Tebex package ships with interaction = '9labs_ui' and
notify = 'ox_lib'; change them to whatever your server uses.
notify accepts 9labs_ui, ox_lib, native
and custom. With ox_lib, notifications fall back to the built-in
9labs_ui style when ox_lib is not running.
targetIcon is the icon shown on ox_target and qb-target options.
prompt sets the title and size of the built-in 9labs_ui prompt;
scale is clamped between 0.75 and 1.5.
interactDistance is measured in metres from the bunker service point, or from
the tablet on a tablet station.
Plans and payment
A plan is an id, a duration and a price. The server picks the plan and its authoritative price by id.
This resource does not include or stream a scooter; it streams only the bunker, frame,
tablet and their animation. scooterModel must be a model your server streams.
The default, labscoot, expects the separate 9labs_scooter model
resource to be installed and started. You can set any other streamed model name, or a
vanilla vehicle, instead. Either way, vehicleType must match that model's
vehicles.meta type, such as bike or automobile.
With warpIntoScooter = true the renter is seated on the scooter as soon as it
is dispensed; the default leaves the player to walk up and get on.
A type mismatch is the usual cause of a scooter that spawns and then behaves strangely, or does not appear at all.
Standalone credits
Standalone mode never touches framework money. It keeps a separate cash and bank rental wallet per FiveM license in resource KVP storage.
playSounds = false silences the bunker. soundVolume is the master
volume for every cue, soundDistance is the range in metres beyond which nothing
is heard, soundRefDistance is the distance of full loudness, and a higher
soundRolloff drops the volume faster with distance. Each cue in
sounds plays a file from web/sounds/ at an animation frame of the
302-frame, 30 fps cycle.
Rental lifecycle
v4.1.4 has a direct flow: a successful rent call is the handover, not a booking.
select station + plan + account | vserver validates player, distance, ownership, station, balance and storage | vpayment succeeds -> bunker begins dispensing -> rental is active | +--------------------+ | | v v returned at a station timer expires
The server creates and charges the rental only while the player is alive, on foot, in routing bucket 0 and within interactDistance of the selected station service point.
A successful rent immediately begins the bunker release cycle.
The duration begins at the release point, and the calculated expiry includes the release-animation offset.
The server enforces one rental per character, per-player and per-station locks, station busy state, a request cooldown, the account balance, persistent storage and model spawning.
A rental may be returned at any valid station once its scooter is within rental.returnDistance of it.
Unused time is not refunded, and the timer continues while the player is offline.
What the phone API cannot do
There is no remote reserved state in v4.1.4. The phone API lists
stations and plans from anywhere, shows the active rental and routes the player to a
station — but it cannot buy a rental remotely and hold it for later. Call
rent after the player reaches the station; that distance check is
intentional and is repeated by the protected server code.
There are also no public reserve, cancel or proximity
auto-release exports and events, so do not build a phone UI that promises those states.
Persistence and recovery
Two plain JSON files, both Asset Escrow-exempt, both loaded when the resource starts.
locations.jsonStations placed in the creator.
rentals.jsonActive rentals, including ones whose owner is offline.
A rental that outlived its scooter
An active timer continues across disconnects and restarts. If the rental is still valid but
its scooter entity is gone, getRental returns recoverable = true,
and the built-in station menu lets the player collect a replacement scooter for the
remaining time. No new payment is taken.
Payment safety
The server writes a provisional rental before charging. If it stops inside that narrow
window, startup removes the stale provisional entry and prints the owner and the amount in
the console so an administrator can decide whether a manual refund is needed. If storage is
invalid or cannot be written, rental requests fail rather than continuing on unsafe
in-memory payment state.
Server exports
Call these from server-side code only, and always derive the player from the event's own source.
local SCOOT = exports['9labs_scootbunker']SCOOT:getStations() -- every static and placed station, keyed by idSCOOT:getPlans() -- plans, accounts and the display currencySCOOT:getRental(source)SCOOT:rent(source, stationId, planId, account)SCOOT:returnRental(source, stationId)
Return shapes
{ ok = true }{ ok = true, rental = Rental }{ ok = false, error = 'Translated message for the player' }
There is no machine-readable error code in v4.1.4.error is a
localized display message — show it to the player and refresh state, never branch your
phone logic on the English text.
getStations
Returns every static and creator-placed station, indexed by station id:
{ id = 'placed:...', coords = { x = -1037.5, y = -2737.2, z = 19.10 }, heading = 150.0, name = 'Airport SCOOT', color = '#00e5b4', blip = { enabled = true, sprite = 226, color = 3, scale = 0.75, }, fixed = true, -- config.lua stations only; nil for placed stations version = 'bunker', -- or 'tablet'; may be nil for older placed stations terminal = nil, -- tablet transform when present busy = false,}
busy is a live snapshot of the release animation; let the server revalidate
availability at rent time rather than treating a fetched busy = false as a
lock. fixed is true for stations from config.lua, and
name, color, blip and terminal may be
absent when defaults are used.
getPlans and getRental
getPlans returns the configured plans, the allowed accounts and the display
currency. getRental returns the public rental form, or nil when
there is none:
local result = exports['9labs_scootbunker']:rent(source, 'static:1', 'short', 'bank')if result.ok then print(('Scooter %s expires at %s'):format( result.rental.plate, result.rental.expiresAt ))else print(result.error)end
The player must be alive, on foot, in routing bucket 0 and within
interactDistance of the station. Plan, account, price, balance, duplicate
rental, cooldown, storage and station state are all checked again on the server.
returnRental
Ends the rental when the rented scooter is within rental.returnDistance of the
selected station. The export validates the station id, ownership of an active rental and,
when the scooter still exists, its distance. If the scooter entity is already gone, the
rental is ended without a distance check.
Unlike the built-in station callback, the export does not require the player's ped to be
next to the station, does not apply requestCooldownMs, does not wait for the
original station to finish dispensing and does not check the scooter's routing bucket. A
phone wrapper that wants the physical behaviour should apply its own proximity policy and
rate limit first. On success the rental is removed from storage, keys are removed
where supported, the scooter is deleted and scootbunker:rentalEnded fires with
reason returned.
Client exports
These work on the local player's streamed station state only.
local SCOOT = exports['9labs_scootbunker']SCOOT:getLocations() -- stations last synchronized to this clientSCOOT:getNearestStation() -- stationId, distance (nil, nil when nothing is loaded)SCOOT:openStation(stationId)SCOOT:previewStation(stationId, delay, options)SCOOT:clearPreview(stationId)
getLocations returns the station table last synchronized to this client, which
may be empty for the first few seconds after startup. getNearestStation
measures from the player to the main bunker coordinates and returns nil, nil
when no station data is loaded. openStation runs the same open flow as walking
up to it — the server still validates distance and station state — and it does not return
the server mutation result.
previewStation
local started = exports['9labs_scootbunker']:previewStation( stationId, 1.5, { scooter = true, keep = false, cleanup = 45, })
A local demonstration cycle: no server call, no money, no rental, and the scooter is a local
entity. scooter = false runs the cycle with an empty fork, keep = true
leaves the preview scooter where it lands, and cleanup overrides the 45-second
default. It returns false when the station is not streamed, is busy, or its
bunker entity does not exist. clearPreview removes the local scooter when it is
safe to delete.
Events and external UI
Client: stationOpened
Local client event, emitted only for bunker-only stations when rental.externalUi = true:
AddEventHandler('scootbunker:stationOpened', function(stationId, data) -- data.ok == true -- data.station station data -- data.plans configured rental plans -- data.accounts allowed payment accounts -- data.balance { cash = number, bank = number } -- data.rental active rental or nil -- data.busy current station busy snapshot OpenMyPhoneScootScreen(stationId, data)end)
This is a local integration event, not a purchase endpoint. The rental itself must
still go through your phone's server-side wrapper and the rent export.
Server: rentalStarted
Fires after the rental is stored and charged and the handover cycle has started — use it for phone refreshes, analytics or logs.
Offline rentals are expired and cleaned up too, but an offline player has no current server
id, so do not expect a rentalEnded callback with src for an
expiry processed while the owner is disconnected.
Phone workflow
List from anywhere, rent at the station. Keep every mutation on the server side of your phone resource.
Request getStations, getPlans and getRental from the phone's server code.
Show stations, durations, prices and payment methods.
Let the player pick a station and set a waypoint to its main coords.
When the player reaches the station and is on foot, call the phone's server rent callback.
Display the returned remaining, expiresAt and plate.
Refresh on rentalStarted and rentalEnded.
Return the scooter only once it is inside the configured return radius of a station.
Server side
local SCOOT = exports['9labs_scootbunker']RegisterNetEvent('myphone:scoot:requestCatalog', function() local src = source TriggerClientEvent('myphone:scoot:catalog', src, { stations = SCOOT:getStations(), rental = SCOOT:getRental(src), catalog = SCOOT:getPlans(), })end)RegisterNetEvent('myphone:scoot:rent', function(stationId, planId, account) local src = source -- never take this value from the client payload local result = SCOOT:rent(src, stationId, planId, account) TriggerClientEvent('myphone:scoot:rentResult', src, result)end)RegisterNetEvent('myphone:scoot:returnRental', function(stationId) local src = source local result = SCOOT:returnRental(src, stationId) TriggerClientEvent('myphone:scoot:returnResult', src, result)end)
Add an application-level cooldown to catalog requests if the phone can refresh them
frequently. rent applies its own validation and request cooldown;
returnRental validates ownership and distance but has no cooldown, so
rate-limit your return endpoint as well.
Client GPS routing
RegisterNetEvent('myphone:scoot:setRoute', function(coords) if type(coords) ~= 'table' then return end local x, y = tonumber(coords.x), tonumber(coords.y) if not x or not y then return end SetNewWaypoint(x + 0.0, y + 0.0)end)
Security rules
What the protected server code validates, and what your integration must not do.
Validated on the server
Player and character identity.
Routing bucket, player health and the on-foot requirement for a purchase.
Real player coordinates for a purchase.
Station existence and current busy state.
Plan and payment-account allowlists.
The server-configured price and the current balance.
One active rental per character, per-owner and per-station locks, and request cooldowns.
Entity model spawn success and network ownership.
Persistent storage before and after payment.
Rental ownership and, while the scooter exists, its distance for a return.
Integration rules
Call mutation exports only from server-side code.
Derive the player id from the network event or callback source.
Never accept a price, balance, expiry, plate, network id or owner identity from the client.
Treat catalog data as display data; the server revalidates it at action time.
Do not create a second client-side payment path around the rent export.
Rate-limit public phone catalog endpoints in your phone framework.
Display export errors, then refresh authoritative state.
Custom bridges
Both bridge files are Asset Escrow-exempt, so no protected file needs editing.
Framework, money and keys
Set framework = 'custom' and/or keys = 'custom', then implement the CUSTOM functions:
-- server/bridge.lua, CUSTOM blockCustom.identity(src)Custom.getMoney(src, account)Custom.removeMoney(src, account, amount, reason)Custom.addMoney(src, account, amount, reason)Custom.isAdmin(src)-- used when keys = 'custom'Custom.giveKeys(src, vehicle, plate)Custom.removeKeys(src, vehicle, plate)
Custom.identity must return a stable, non-empty string for the current
character — rentals are indexed by it, so a temporary server id will not do. Money functions
must return accurate values and booleans: the resource checks the balance before charging,
and calls addMoney only when transaction recovery requires a refund.
vehicle in the key functions is a server entity handle.
Prompt, target and notifications
Set interaction = 'custom' and/or notify = 'custom', then implement:
A custom target registration must return its removal handle, or nil when no
target is created. The resource still handles the station action itself and the server still
validates it.
Troubleshooting
The station appears but no scooter is dispensed
Confirm scooterModel is streamed and spelled exactly as its model name. For the default labscoot, the separate 9labs_scooter resource must be installed and started before SCOOT.
Confirm rental.vehicleType matches its vehicles.meta type.
Confirm OneSync is enabled.
Reconnect after a streamed model or YTYP change.
Set debug = true and read the client F8 console. This also registers /scootdiag and /scootsound.
The phone rent call says the player is too far away
Expected. v4.1.4 lists stations remotely but starts rentals only at a physical station:
the player must be within interactDistance, alive, on foot and in routing
bucket 0.
A target does not appear
Verify the configured interaction name matches ox_target or qb-target exactly.
Start the target resource before 9labs_scootbunker.
For custom, verify Custom.addTarget returns a handle.
Tablet stations target the tablet position, not the bunker service point.
The menu does not use the external phone UI
Set rental.externalUi = true.
Listen for the local client event scootbunker:stationOpened.
Test with a bunker-only station — a physical tablet station deliberately keeps its DUI.
The player cannot return the scooter
Use a valid station id.
Bring the scooter within rental.returnDistance of that station.
With the built-in station UI, bring the player within interactDistance as well, and wait until the original station has finished its dispensing cycle.
Implement the matching functions in server/bridge.lua.
Make sure the identity is stable per character.
Make sure the money functions return true only after the transaction succeeded.
A rental survived a restart but has no scooter
Expected recovery behaviour. Open a station while the rental still has time left and use
Collect remaining rental; no new payment is taken.
When you ask for support
Run this in the server console first. It prints the version, the detected framework, the
station count, active and pending rentals, tracked players and the expiry timer state, and
warns about any foreign scripts added to the resource's fxmanifest.lua:
scootstatus
Include its output, the resource version, the selected framework, keys, interaction and notification
modes, the console and F8 errors, whether the station is static or placed and bunker-only or
tablet, the configured scooter model and vehicleType, whether it also happens
through the built-in UI, and the steps to reproduce it. Never post license keys, Tebex
credentials, full server configs or framework credentials publicly.