Everything you can drive from your own scripts: spawn and delete cars, seat players, control engine and lights, set paint and plates, apply upgrades, react to events. Lua and C# both work.
What each physics field means lives in Handling. Preparing a new car's assets lives in Model standard. Installation and permissions setup are in Getting started.
Ground rules
Cars are composite RDR objects with custom physics. The usual SetVehicle* and GetVehicle* natives do not touch them. Use these operations instead.
vehicleId is an opaque string, shaped session:7:43. It is not an entity handle and not a network ID, both of which the engine reuses. Restarting the core invalidates every previous ID, so never store one as a long-term key. For a garage, keep your own record and reapply state to a freshly spawned car.
seatId starts at 1, where 1 is the driver. playerId is a server player ID.
Exports take and return JSON strings, not Lua tables. Operation names are case-sensitive. Physics keys keep their capitals (EngineTorque); everything else is lower camelCase.
{"version":1,"id":"garage-1","op":"setEngine","vehicleId":"session:7:43","value":true}
Replies are {"id":"...","ok":true,"data":...} or {"id":"...","ok":false,"error":"..."}. Requests cap at 32 KiB. Duplicate keys, malformed JSON, unknown fields, numeric strings and non-finite numbers are all rejected. Player requests are rate-limited, so never call a setter every frame.
Calling from a client script
Request(json) answers get and list immediately from the local cache. Everything else queues a server request and returns {"ok":true,"pending":true,"id":"c123"}.
Pending is not success. Match the returned id against the local wvOnResponse event:
local waiting = {}
AddEventHandler('wvOnResponse', function(raw)
local reply = json.decode(raw)
local done = waiting[reply.id]
if done then waiting[reply.id] = nil; done(reply) end
end)
local function request(fields, done)
fields.version = 1
local queued = json.decode(exports.world_vehicles:Request(json.encode(fields)))
if not queued.ok or not queued.pending then done(queued) return end
waiting[queued.id] = done
end
request({op = 'delete', vehicleId = selected}, function(reply)
if not reply.ok then print(reply.error) end
end)
Clear pending callbacks when your resource stops. Local getters are cached and scoped to that client, so they are not the whole server fleet and can be stale.
Calling from a server script
wvRequest(json) runs from a started server resource. Cfx authenticates the calling resource itself, so trusted management works without a player nearby and without the usual six-metre limit.
local function wv(fields)
fields.version = 1
return json.decode(exports.world_vehicles:wvRequest(json.encode(fields)))
end
local plate = wv({op = 'setPlate', vehicleId = id, plateText = 'APPI 50', plateState = 'lemoyne'})
if not plate.ok then print(plate.error) end
local speed = wv({op = 'getSpeed', vehicleId = id})
if speed.ok and speed.data.available then
print(('%.1f mph'):format(speed.data.speedMph))
end
Never forward untrusted network JSON straight into this export. A bridge like that hands a client server authority. Check your own garage, job and purchase rules first. Spawning, boarding and driver actions still require a real player.
The server export is registered once the core has finished starting, so a resource state of started is not proof that it is callable yet. Wrap the first call in pcall and retry later rather than spinning in a loop.
Who is allowed to do what
| Scope | Reach |
|---|---|
| Normal | Creator or confirmed driver, same bucket, within 6 m. Deleting normally requires the creator. |
| Freeroam player | Only while the mode is on, and only their own cars: spawn, paint, plate, delete. Same bucket, no 6 m rule for those actions, no admin anything. |
| Driver | Confirmed seat 1, same bucket, within 5 m. Gear and horn. |
| Admin | Developer ACE. Remote management of any car, across distance and buckets. |
| Trusted server | A started resource calling the server export. No player needed; your code authorises the user. |
| Profile | Shared baseline for one model and its future spawns. |
| Instance | One car. Does not touch another and does not survive deletion or restart. |
A trusted, developer or playerId field inside JSON grants nothing: trust comes from where the call originates. Identity, lifecycle, value validation and safe-editing checks still apply to admins. Seat tokens are private leases, not values to guess.
Reading state
| Operation | Input | Returns |
|---|---|---|
getApiInfo |
— | Protocol, capabilities, configured ACE, limits |
listProfiles |
— | Available profile IDs; unavailable modules excluded |
list |
— | Scoped snapshots; the server export lists every registered car |
get |
id | One snapshot. Normal reads are same-bucket and ≤120 m |
getSpeed |
id | speedMps, speedKmh, speedMph, available, source, measuredAt. Absolute 3D body speed including vertical movement, not wheel RPM. Use it only when available is true |
getHandling |
id | Full effective physics. A snapshot's handling holds overrides only |
getHandlingSchema |
— | Every field with type, bounds, step, description, read-only flag |
getTuning |
id | Preset metadata and validity flags |
getPaint |
id | Supported flags, current preset IDs, revision, authored palettes |
getPlate |
id | Text, state, revision, available states |
getPlateStates |
— | The five state styles with labels and colours |
getAudioState |
id | Engine and horn plus fresh telemetry when available |
getDamage |
id | Health, flooding, light failures, bumper states |
getDamageSettings |
id | Effective damage settings, not current HP |
getSettings |
— | Session, revision, global settings, profile overrides |
menuSnapshot |
— | The caller's own menu. access.role comes back as admin or freeroam; a player role gets their own cars, public model metadata and quota, never admin schemas or settings |
adminSnapshot |
— | Whole fleet with location, bucket, speed, schemas. Admin only, not a per-frame poll |
Use adminSnapshot to build an editor: it carries the schemas and read-only flags you need, and those schemas are your allowlist.
Cars and people
| Operation | Input | Effect |
|---|---|---|
spawn |
profile?, position?, heading? |
Issues a ticket, not a ready car. Wait for wvOnSpawn, then for the assembly flags to clear. Normal spawns are within 10 m of the player, and an explicit Z is used as given |
delete |
id | Removes one car and releases its occupants |
flipVehicle |
id | Admin. Assembled and stopped. Returns a pending token; completion arrives as wvOnFlip. Not a repair or a teleport |
enter |
vehicleId?, seatId? |
Reserves a free seat within 5 m. No ID means the nearest car, no seat means the driver |
seatReady |
id, token |
Completes an entering lease. Do not fake this |
exit |
id, token |
Leaves the caller's seat. Moving, airborne and underwater exits are allowed |
leave |
id, token |
Releases the lease outright, including emergency cleanup |
setEngine |
id, value |
A damaged or flooded car will not start until repaired |
setLights |
id, value |
A broken bulb group stays dark |
setPanel |
id, panel, value |
Opens or closes an authored panel such as door_l or hood. It cannot create a hinge the model lacks |
setHorn |
id, value |
Driver. Refresh roughly every 500 ms; it stops 1 s after the last refresh |
setGear |
id, gear |
Driver. -1 for reverse, or 1 to the profile's gear count. No neutral or clutch |
setTransmissionMode |
id, mode |
Admin, one car. Use global settings for fleet policy |
A shared default horn recording ships with the core, so a model with no cue of its own still sounds; a pack or model cue overrides it. The sound fires once when the value goes from false to true rather than on every refresh, so release the horn before sounding it again.
Appearance
| Operation | Input | Effect |
|---|---|---|
setPaint |
id, paintId?, secondaryPaintId? |
At least one palette ID. Validated atomically |
setPlate |
id, plateText?, plateState? |
At least one. Text is trimmed and uppercased: 1–8 characters, A–Z, 0–9, spaces and hyphens, with at least one letter or digit |
Plate states are lemoyne warm white, new_hanover black, ambarino blue, west_elizabeth red and new_austin yellow. A model needs the matching assets for any of it to appear; authoring them is covered in Model standard.
A fresh car already wears a plate: five random digits in WV 48271 form and a random state, unless the profile sets plate.randomState to false or authors its own defaultText. That choice is made once and is not rerolled by a repair, a roster change or a new owner. Generated text avoids other registered cars, while text you set through the API does not have to be unique.
Selecting the value a car already has emits no event. Choices are instance state, so a garage stores them and reapplies them after a later spawn.
Damage
| Operation | Input | Effect |
|---|---|---|
repairVehicle |
id | Admin. Body and engine to 1000, clears flooding. Does not start the engine |
setHealth |
id, bodyHealth?, engineHealth? |
Admin. Absolute health 0–1000, not damage. Does not clear flooding |
applyDamage |
id, bodyDamage?, engineDamage? |
Trusted server only. Subtracts health, never heals |
setBumperDamage |
id, panel, stage |
Admin. bumper_f or bumper_r, stage 0–3: clean, dented, crushed, detached |
setDamageSettings |
id, patch |
Admin. Per-instance settings, no HP change, not persisted |
Damage settings accept enabled, collisionEnabled and waterEnabled as booleans; collisionMinDeltaV 0.1–100 and collisionMaxDeltaV 0.2–200 m/s with max above min; bodyDamagePerDeltaV 0–1000; engineDamageRatio 0–1; impactCooldownMs and waterConfirmMs 100–60000; submergedThreshold 0.1–1; and visualStage1Health, visualStage2Health, frontLightsFailureHealth, tailLightsFailureHealth 0–1000, with stage 2 at or below stage 1.
Tuning and editing
| Operation | Input | Scope |
|---|---|---|
setHandling |
id, patch |
Admin. Up to 64 raw overrides on one car |
setSeat |
id, seatId, offset |
Admin. One car, each coordinate ±10 m |
setEngineLevel |
id, level 0–4 |
Admin. One car |
setTopSpeedMph |
id, mph 20–200 |
Admin. One car |
setSuspensionFirmness |
id, value 0.5–2 |
Admin. One car |
setSuspensionHeight |
id, meters −0.2–0.3 |
Admin. One car |
setGlobalSettings |
patch, persist? |
Admin. Whole server |
setProfileSettings |
profile, patch, persist? |
Admin. Every car of one model |
exportProfileSettings |
profile, patch? |
Admin. Validates and writes a file without applying |
exportHandling / exportSeats / exportDamageSettings / exportDiagnostics |
id | Admin. Writes a file for review |
Every tuning call needs the car assembled and stopped at ≤0.6 m/s, and the resulting complete physics must validate. Raw edits to a preset's fields invalidate that preset's metadata until you reapply it. What the values mean is in Handling.
Saving settings
persist:false, the default, applies a change for the session. persist:true applies it and saves the patch you just sent, merged with previously saved overrides. It does not sweep up unrelated live previews, so if you previewed something earlier and want it kept, send it again with persist:true.
The server writes two alternating journal files under the core resource's data/ directory, with checksums and readback verification, and can fall back to the last valid record. Clients never choose a path. A failed validation is not a save.
Exports only create files for you to review and install by hand. They never overwrite config.json or a module's vehicle.json.
Global settings cover freeroam, transmissionMode, speedometer, debug, controls, camera, audio and surfaceEffects. Profile settings cover physics, seats, seatExitOffsets, lighting, damage, engineInitiallyOn and audio. Authored strings such as audio.pack stay read-only.
debug holds five strict booleans, enabled, serverLogging, physicsDetails, printEverySecond and observerPoseEnabled, all false in a clean install. SetDebug moves only collection enabled; everything else goes through a setGlobalSettings patch, and none of it is a player privilege.
surfaceEffects holds enabled and dustPlumes, both true, and tireTracks, false, plus dustScale 0.25 to 3 with a default of 1.6, trackLifetimeSeconds 5 to 60 seconds with a default of 25, and trackOpacity 0.1 to 1 with a default of 0.55. Marks are cosmetic: they do not change traction, they do not deform terrain and they do not survive a restart.
Replies distinguish the live revision from the stored savedRevision. Saving an unchanged value can advance storage without emitting a change event.
A few things persistence deliberately is not: it is not a garage, it does not save cars, it does not eject current occupants, and engineInitiallyOn affects future spawns rather than the engine running right now.
Freeroam
Freeroam is a server-wide policy, off by default, that lets ordinary players open /wvmenu for their own cars. Switch it with setGlobalSettings and patch:{"freeroam":{"enabled":true}}, adding persist:true to keep it after a restart. It is a policy, not a role: it never grants the developer ACE and never opens an administrator action.
A player in that mode can spawn an available model, see what they have out, repaint it, renumber it and delete it. Handling, upgrades, repair, flip, global settings and diagnostics stay out of their menu, and the server validates every request anyway. A hidden button is not the security boundary.
Slots cap at three per player, or lower where limits.perPlayer already is. An active car, a pending ticket and an unfinished deletion each hold a slot, and quota reports active, pending, deleting, used, limit and canSpawn. The server-wide cap, the spawn cooldown and model availability still apply.
Ownership means the recorded creator, not the driver and not whoever happens to simulate the car. A passenger or a migrated network owner cannot repaint, renumber or delete someone else's car.
Turning the mode off cancels unfinished Freeroam spawn requests and closes the player menu on its next authoritative update. Cars that already exist stay, and an administrator's tools are untouched.
Named exports
The generic Request and wvRequest always work. Named wrappers just fill in op for you.
Client, answered locally: GetVehicle, ListVehicles, GetCurrentVehicle, GetApiInfo, GetAudioStatus, GetSurfaceEffectsStatus, GetSpeed, GetSpeedMph, GetSpeedKmh, wvGetTuning, wvGetPlateStates.
Client, queued: DeleteVehicle, FlipVehicle, wvGetPlate, wvSetPlate and the positional tuning setters. GetSpeedMph returning null means unavailable, not stopped.
Server: wvSpawnVehicle, wvDeleteVehicle, wvEnterVehicle, wvExitVehicle, wvGetVehicles, wvGetVehicle, wvListProfiles, wvGetHandling, wvGetHandlingSchema, wvGetAudioState, wvGetApiInfo, wvSetEngine, wvSetLights, wvSetPanel, wvPreviewHandling, wvPreviewSeat, wvExportHandling, wvExportSeats, wvExportDiagnostics, wvGetDamage, wvRepairVehicle, wvApplyDamage, wvSetHealth, wvSetBumperDamage, wvGetDamageSettings, wvPreviewDamageSettings, wvExportDamageSettings, wvGetPaint, wvSetPaint, wvGetPlate, wvSetPlate, wvGetSpeed, wvFlipVehicle, wvGetAdminSnapshot, wvGetSettings, wvSetGlobalSettings, wvSetProfileSettings, wvExportProfileSettings, wvGetPlateStates.
Both sides also expose positional tuning exports, which take arguments rather than JSON: wvGetTuning(id), wvSetEngineLevel(id, level), wvSetTopSpeedMph(id, mph), wvSetSuspensionFirmness(id, value), wvSetSuspensionHeight(id, meters).
C# SDK
Reference WorldVehicles.Api with the bundled Newtonsoft. CitizenFX pins a portable 12.0.0.0 assembly; do not swap in a modern .NET-only build.
var api = new WorldVehicles.Api.WorldVehiclesApiClient(
json => Exports["world_vehicles"].wvRequest(json)); // server
// client transport: json => Exports["world_vehicles"].Request(json)
api.SetPlate(vehicleId, "APPI 50", "lemoyne");
api.SetEngineLevel(vehicleId, 2);
api.SetGlobalTransmissionMode("manual", persist: true);
api.SetProfileHandling("pioneer_sedan", new JObject { ["RestLength"] = 0.70 }, true);
var speed = api.GetSpeed(vehicleId);
The SDK is a transport adapter, not an entity owner and not a permission bypass. Client helpers still queue rather than answering synchronously. VehicleSpeedState is nullable, so check Available first.
Methods mirror the operations above, plus convenience wrappers: SetGlobalTransmissionMode, SetSpeedometer, SetDebug, SetProfileHandling, and Request(op, vehicleId, fields) for anything else.
Snapshots
A snapshot carries id, net, generation, profile, model, creator, seats, preparing, spawnFrozen, engine and horn and light state, panels, handling and its revision, seat offsets, transmission mode, requested gear, tuning, telemetry, paint and plate state with revisions, and damage with settings.
Local snapshots add entity, effectiveHandling, telemetryAvailable and the viewer's own localSeatId, localSeatToken and localSeatPhase. An entity of 0 means the body is not resolved on that client this frame.
Change things through the API, not by editing a returned snapshot. A reserved seat may still be entering: wvOnEnter is what confirms it is ready.
Events
Subscribe with AddEventHandler in Lua or EventHandlers in C#. These are local notifications, not commands to send back.
| Event | Arguments |
|---|---|
wvOnSpawn |
vehicleId |
wvOnEnter |
vehicleId, seatId, playerId |
wvOnExit |
vehicleId, seatId, playerId |
wvOnDelete |
vehicleId |
wvEngine |
vehicleId, enabled, actorId |
wvOnLights |
vehicleId, enabled, actorId |
wvOnPanel |
vehicleId, panelId, open, actorId |
wvOnHorn |
vehicleId, enabled, actorId |
wvOnPaint |
vehicleId, paintStateJson, actorId |
wvOnPlate |
vehicleId, plateStateJson, actorId |
wvOnTuningChanged |
vehicleId, tuningJson, actorId |
wvOnDamage |
vehicleId, damageJson, reason |
wvOnHealthSet |
vehicleId, damageJson |
wvOnRepair |
vehicleId, damageJson |
wvOnBumperImpact |
vehicleId, panelId, deltaV, bumperStateJson |
wvOnFlip |
vehicleId, success, actorId |
wvOnSettingsChanged |
settingsJson, actorId |
wvOnProfileSettingsChanged |
profileId, profileJson, actorId |
wvOnResponse |
replyJson |
Four practical notes. Every ...Json argument is a string, so decode it. wvOnDamage carries a reason of collision, flood or manual. An actorId of 0 means a trusted server or system action. wvOnExit also fires for a cancelled entry, so handle it idempotently.
Vehicle events are server-local and relayed to clients in that car's bucket. A remote admin should query fleet state rather than assume every cross-bucket event arrives. The settings events are server-local too, so do not treat them as a broadcast channel to clients.
Internal wv:* streams carry tickets, seat leases and owner epochs. They are not an alternative public API.
Not available
No arbitrary vertex deformation, no bullet tyre punctures, no persistent tyre tracks, no full door damage, no collision-mesh editing and no arbitrary RGB paint setter. Authored bumper stages and health-driven light failures do not imply otherwise. The radio is a separate add-on, and the only menu an ordinary player can open is the reduced Freeroam one.
Building a garage
The usual integration, and the shape that avoids every trap above:
- Add
dependency 'world_vehicles'to your resource. - Keep your own stable record per owned car: model, plate, colours, upgrades.
- When a player retrieves it,
spawna fresh car and wait forwvOnSpawnplus the assembly flags. - Reapply plate, paint and upgrades from your record.
- On storage, read what you need and
delete.
Never persist a vehicleId between sessions, and remember that configuration persistence is not this database.