Une clé, des portées
La clé voyage dans l’en-tête X-MDT-API-Key. Chaque clé porte des portées : personnages, véhicules, armes. Une clé qui n’a que la portée véhicules ne peut pas toucher aux dossiers civils, même si elle fuite.
Documentation
Voilà l’intégralité de ce qu’il faut brancher pour que le terminal se remplisse tout seul. Chaque endpoint est documenté avec ses champs, sa réponse et un exemple Lua que vous pouvez copier tel quel. Lisible sans compte, avant même de nous parler.
La clé voyage dans l’en-tête X-MDT-API-Key. Chaque clé porte des portées : personnages, véhicules, armes. Une clé qui n’a que la portée véhicules ne peut pas toucher aux dossiers civils, même si elle fuite.
Un personnage est identifié par son charId, un véhicule par sa plaque, une arme par son numéro de série. Renvoyer deux fois le même appel met à jour au lieu de créer un doublon. Vous pouvez rejouer une synchronisation entière sans rien casser.
Vous appelez ces endpoints depuis votre propre code, avec le framework que vous voulez. Rien à installer côté serveur, rien qui tourne en boucle chez vous, aucune dépendance à maintenir.
01
Le dossier civil est la brique de base : sans lui, ni rapport, ni amende, ni casier. Envoyez le personnage à sa création, puis à chaque changement d’état civil ou de métier.
Idempotent sur charId : le premier appel crée, les suivants mettent à jour. Prénom et nom sont exigés à la création seulement.
Corps de la requête
| charIdrequisnumberVotre identifiant interne, la clé de rapprochement |
| firstnamestringObligatoire à la création |
| lastnamestringObligatoire à la création |
| discordIdstring |
| dateofbirthstringFormat libre, 10 caractères |
| nationalitystring |
| sexstringUn caractère |
| heightnumber |
| weightnumber |
| jobstring |
| jobGradenumber |
Réponse
{ "ok": true, "charId": 1042, "created": true }Exemple
local API = GetConvar('onduty_url', '')
local CLE = GetConvar('onduty_api_key', '')
--- Envoie un personnage au terminal. A appeler a la creation, puis a chaque
--- changement d'etat civil ou de metier.
local function pousserPersonnage(perso)
PerformHttpRequest(API .. '/api/fivem/characters', function(code, corps)
if code ~= 200 then
print(('[onduty] personnage %s refuse (%s) %s'):format(perso.charId, code, corps))
end
end, 'POST', json.encode(perso), {
['Content-Type'] = 'application/json',
['X-MDT-API-Key'] = CLE,
})
end
pousserPersonnage({
charId = 1042,
firstname = 'Rachel',
lastname = 'Vasquez',
dateofbirth = '08/03/1994',
job = 'police',
jobGrade = 4,
})Suit un wipe de personnage côté serveur. Réversible : renvoyez le même appel avec restore à true pour rétablir.
Corps de la requête
| charIdrequisnumber |
| reasonstringConservé dans le journal d’audit |
| restorebooleantrue pour annuler un effacement |
Réponse
{ "ok": true, "charId": 1042 }Exemple
local API = GetConvar('onduty_url', '')
local CLE = GetConvar('onduty_api_key', '')
PerformHttpRequest(API .. '/api/fivem/characters/wipe', function(code)
print('[onduty] wipe ' .. code)
end, 'POST', json.encode({
charId = 1042,
reason = 'Suppression demandee par le joueur',
}), {
['Content-Type'] = 'application/json',
['X-MDT-API-Key'] = CLE,
})Bloque l’accès au terminal pour tous les personnages liés à ce Discord. Durée en minutes, ou 0 pour un bannissement définitif.
Corps de la requête
| discordIdrequisstring |
| reasonstring |
| durationMinutesnumber0 ou absent pour définitif |
Réponse
{ "ok": true, "blocked": true }Exemple
local API = GetConvar('onduty_url', '')
local CLE = GetConvar('onduty_api_key', '')
PerformHttpRequest(API .. '/api/fivem/bans', function(code)
print('[onduty] ban ' .. code)
end, 'POST', json.encode({
discordId = '198374658374658374',
reason = 'Sanction staff',
durationMinutes = 10080,
}), {
['Content-Type'] = 'application/json',
['X-MDT-API-Key'] = CLE,
})Rend l’accès au terminal à un Discord précédemment bloqué.
Corps de la requête
| discordIdrequisstring |
Réponse
{ "ok": true, "blocked": false }Exemple
local API = GetConvar('onduty_url', '')
local CLE = GetConvar('onduty_api_key', '')
PerformHttpRequest(API .. '/api/fivem/bans/revoke', function(code)
print('[onduty] unban ' .. code)
end, 'POST', json.encode({ discordId = '198374658374658374' }), {
['Content-Type'] = 'application/json',
['X-MDT-API-Key'] = CLE,
})02
Ce qui rend un contrôle routier crédible. Le fichier des immatriculations vit chez vous, le terminal le reflète.
Idempotent sur la plaque. Rattachez-le à son propriétaire avec ownerCharId, ou à un métier avec ownerJob pour les véhicules de service.
Corps de la requête
| platerequisstring15 caractères maximum |
| makestring |
| modelstring |
| colorstring |
| ownerCharIdnumber |
| ownerJobstringPour un véhicule de service |
| ownerJobLabelstring |
| categoryenumVOITURE, MOTO, POIDS_LOURD, BATEAU, AVION, AUTRE |
Réponse
{ "ok": true, "id": 8821, "plate": "42XKQ71" }Exemple
local API = GetConvar('onduty_url', '')
local CLE = GetConvar('onduty_api_key', '')
PerformHttpRequest(API .. '/api/fivem/vehicles', function(code, corps)
if code ~= 200 then print('[onduty] vehicule refuse ' .. corps) end
end, 'POST', json.encode({
plate = '42XKQ71',
make = 'Bravado',
model = 'Buffalo STX',
color = 'Noir',
ownerCharId = 1042,
category = 'VOITURE',
}), {
['Content-Type'] = 'application/json',
['X-MDT-API-Key'] = CLE,
})Pour un véhicule de location ou de mission qui ne doit pas rester au fichier. Il disparaît tout seul au bout du délai.
Corps de la requête
| platerequisstring |
| minutesrequisnumberDe 1 à 1440 |
| makestring |
| modelstring |
| colorstring |
| ownerCharIdnumber |
| ownerJobstring |
| categoryenum |
Réponse
{ "ok": true, "id": 8822, "plate": "LOC00412" }Exemple
local API = GetConvar('onduty_url', '')
local CLE = GetConvar('onduty_api_key', '')
PerformHttpRequest(API .. '/api/fivem/vehicles/temporary', function(code)
print('[onduty] vehicule temporaire ' .. code)
end, 'POST', json.encode({
plate = 'LOC00412',
minutes = 120,
make = 'Ubermacht',
model = 'Sentinel',
ownerCharId = 1042,
}), {
['Content-Type'] = 'application/json',
['X-MDT-API-Key'] = CLE,
})Retire une plaque du fichier pendant un temps donné, sans la supprimer. Utile pour une opération sous couverture.
Corps de la requête
| platerequisstring |
| minutesrequisnumberDe 0 à 1440, 0 pour réafficher |
Réponse
{ "ok": true, "plate": "42XKQ71" }Exemple
local API = GetConvar('onduty_url', '')
local CLE = GetConvar('onduty_api_key', '')
PerformHttpRequest(API .. '/api/fivem/vehicles/hide', function(code)
print('[onduty] masquage ' .. code)
end, 'POST', json.encode({ plate = '42XKQ71', minutes = 60 }), {
['Content-Type'] = 'application/json',
['X-MDT-API-Key'] = CLE,
})03
Une arme retrouvée sur une scène ne sert à rien si elle ne remonte à personne. Le numéro de série fait le lien.
Idempotent sur le numéro de série. Rattachez-la à son détenteur pour qu’elle apparaisse sur sa fiche.
Corps de la requête
| serialrequisstringLe numéro de série, votre clé |
| itemNamerequisstringLe nom technique de l’arme |
| ownerCharIdnumber |
Réponse
{ "ok": true, "id": 331, "serial": "AB-4471-KQ" }Exemple
local API = GetConvar('onduty_url', '')
local CLE = GetConvar('onduty_api_key', '')
PerformHttpRequest(API .. '/api/fivem/weapons', function(code, corps)
if code ~= 200 then print('[onduty] arme refusee ' .. corps) end
end, 'POST', json.encode({
serial = 'AB-4471-KQ',
itemName = 'weapon_pistol',
ownerCharId = 1042,
}), {
['Content-Type'] = 'application/json',
['X-MDT-API-Key'] = CLE,
})04
Les deux seuls endpoints qui n’utilisent pas la clé d’API. Ils touchent à des choses plus sensibles, ils sont donc protégés autrement.
Le canal générique : appel d’urgence, prise de service, incident. Signé, pas authentifié par clé, parce qu’il déclenche des actions dans le terminal.
Corps de la requête
| typerequisstringLe type d’événement, 64 caractères maximum |
| payloadobjectLe contenu, libre |
| noncestringÉvite qu’un événement rejoué soit traité deux fois |
Réponse
{ "ok": true, "accepted": "dispatch.call" }Exemple
-- La signature se calcule sur le corps exact envoye.
-- Horodatage en secondes, tolerance de 5 minutes.
local SECRET = GetConvar('onduty_webhook_secret', '')
local corps = json.encode({
type = 'dispatch.call',
payload = { code = '10-13', secteur = 'Vinewood', plainte = 'Coups de feu' },
})
local horodatage = tostring(os.time())
local signature = 'sha256=' .. Cipher.hmac('sha256', SECRET, horodatage .. '.' .. corps)
PerformHttpRequest(API .. '/api/fivem/events', function(code)
print('[onduty] evenement ' .. code)
end, 'POST', corps, {
['Content-Type'] = 'application/json',
['X-Oren-Signature'] = signature,
['X-Oren-Timestamp'] = horodatage,
})Échange un jeton émis en jeu contre une session sur le terminal. C’est ce qui permet d’ouvrir le MDT depuis le véhicule sans retaper quoi que ce soit.
Corps de la requête
| tokenrequisstringLe jeton signé émis par votre serveur, 20 caractères minimum |
Réponse
{ "ok": true }Exemple
-- Cote client, ouvre le terminal deja connecte pour ce joueur.
RegisterCommand('mdt', function()
TriggerServerEvent('onduty:demanderJeton')
end, false)
RegisterNetEvent('onduty:jetonPret', function(jeton)
SetNuiFocus(true, true)
SendNUIMessage({ action = 'ouvrir', jeton = jeton })
end)Erreurs
Toutes les réponses portent un champ ok. En cas d’échec, error donne la cause en un mot, et le corps détaille quand c’est utile.
| 400 | invalid_body | Le corps ne respecte pas le schéma. La réponse détaille le champ fautif. |
| 400 | identity_required | Création d’un personnage sans prénom ni nom. |
| 401 | missing_api_key | L’en-tête X-MDT-API-Key est absent. |
| 401 | invalid_api_key | Clé inconnue ou révoquée. |
| 403 | insufficient_scope | La clé existe mais n’a pas la portée demandée. |
ESX, QBCore ou entièrement fait maison, on regarde votre cas avec vous. Ouvrez un ticket et décrivez votre structure, on vous dit quoi envoyer.