Documentation de l’API Qentra System

Tout pour connecter votre logiciel en quelques minutes : authentification, chaque route avec ses paramètres, des exemples prêts à copier dans quatre langages, les webhooks et la liste complète des erreurs.

Démarrage rapide

  1. Connectez-vous à votre espace client, puis ouvrez Mon compte › Clés API.
  2. Créez une clé de test : elle commence par qsk_test_ et donne accès à trois véhicules simulés.
  3. Appelez /me pour vérifier la clé, puis /positions.
  4. Quand votre intégration fonctionne, créez une clé de production (qsk_live_) : les mêmes appels renvoient vos vrais véhicules.

Adresse de base de toutes les routes :

https://qentrasystem.com/api/public/v1

Vous codez avec un assistant IA ? Donnez-lui le fichier qentra-api.md ou utilisez nos prompts pour Claude, ChatGPT et Gemini.

Premier appel :

curl "https://qentrasystem.com/api/public/v1/me" \
  -H "Authorization: Bearer $QENTRA_API_KEY"
<?php
$ch = curl_init('https://qentrasystem.com/api/public/v1/me');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('QENTRA_API_KEY'),
        'Content-Type: application/json',
    ],
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

if (! $response['success']) {
    exit($response['error']['code'] . ' : ' . $response['error']['message']);
}
print_r($response['data']);
const response = await fetch('https://qentrasystem.com/api/public/v1/me', {
  headers: {
    Authorization: `Bearer ${process.env.QENTRA_API_KEY}`,
    'Content-Type': 'application/json',
  },
});
const json = await response.json();

if (!json.success) {
  throw new Error(`${json.error.code} : ${json.error.message}`);
}
console.log(json.data);
import os
import requests

response = requests.request(
    "GET",
    "https://qentrasystem.com/api/public/v1/me",
    headers={"Authorization": f"Bearer {os.environ['QENTRA_API_KEY']}"},
    timeout=15,
)
data = response.json()

if not data["success"]:
    raise Exception(f"{data['error']['code']} : {data['error']['message']}")
print(data["data"])

Authentification

Chaque requête envoie la clé dans l’en-tête Authorization, en HTTPS uniquement :

Authorization: Bearer qsk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  • La clé n’est affichée qu’une fois, à sa création. Nous n’en gardons qu’une empreinte chiffrée : nous ne pouvons pas vous la renvoyer.
  • Gardez-la côté serveur (variable d’environnement QENTRA_API_KEY), jamais dans une application mobile ou une page web.
  • Une clé peut être limitée à certaines adresses IP, expirer à une date choisie et être révoquée à tout moment.

Test et production

Clé de test qsk_test_

Trois véhicules simulés (ids 9001, 9002 et 9003) qui roulent dans Abidjan, des alertes et une zone d’exemple. Les commandes sont simulées. Quota de 1 000 appels par jour.

Clé de production qsk_live_

Vos vrais véhicules, leurs positions et leurs alertes. Quota mensuel de 20 000 appels par véhicule (au moins 20 000).

Les réponses ont exactement le même format dans les deux modes.

Format des réponses

Toutes les réponses sont en JSON (UTF-8). Les dates sont au format ISO 8601 en UTC, les vitesses en km/h et les distances en kilomètres.

Succès

{
    "success": true,
    "data": [],
    "meta": {
        "count": 1
    }
}

Erreur

{
    "success": false,
    "error": {
        "code": "vehicle_not_found",
        "message": "Véhicule 12 introuvable sur ce compte.",
        "request_id": "9b1f4c2e-6a0d-4f7e-8c1b-2d3e4f5a6b7c"
    }
}

Le champ request_id (aussi renvoyé dans l’en-tête X-Request-Id) identifie chaque appel : communiquez-le au support pour une recherche immédiate. Les listes paginées renvoient meta.page, meta.per_page, meta.total et meta.last_page.

Limites et quotas

En-têteSignification
X-RateLimit-LimitRequêtes autorisées par minute et par clé (60).
X-RateLimit-RemainingRequêtes restantes dans la minute en cours.
X-Quota-LimitQuota du mois (ou du jour en test).
X-Quota-RemainingAppels restants avant la remise à zéro.
Retry-AfterSecondes à attendre après une erreur rate_limited.
X-Request-IdIdentifiant unique de la requête.

L’historique est limité à 7 jours par appel et à 5 000 points : découpez les périodes plus longues.

Droits des clés

Donnez à chaque clé uniquement les droits dont le logiciel a besoin.

vehicles:readLire les véhicules et leurs positions
history:readLire l'historique et les rapports
alerts:readLire les alertes
zones:readLire les zones
shares:writeCréer des liens de partage de position
webhooks:manageGérer les webhooks
commands:engineCouper et remettre le moteur : exige le mot de passe du compte à la création de la clé, refusé si le véhicule roule.

Référence des routes

Toutes les adresses ci-dessous sont relatives à https://qentrasystem.com/api/public/v1. Les exemples utilisent les véhicules de test : remplacez 9001 par l’identifiant renvoyé par /vehicles.

GET /me
GET /vehicles
GET /vehicles/9001
GET /positions
GET /vehicles/9001/history
GET /vehicles/9001/trips
GET /vehicles/9001/stops
GET /vehicles/9001/summary
GET /alerts
GET /zones
POST /vehicles/9001/shares
POST /vehicles/9001/engine/stop
POST /vehicles/9001/engine/resume
GET /webhooks
POST /webhooks
POST /webhooks/3/test
GET /webhooks/3/deliveries
DELETE /webhooks/3

Compte

Vérifier la clé

GET /me

Renvoie le compte, le mode (test ou production), les droits de la clé et la consommation du quota. Idéal pour tester votre configuration.

Requête

curl "https://qentrasystem.com/api/public/v1/me" \
  -H "Authorization: Bearer $QENTRA_API_KEY"
<?php
$ch = curl_init('https://qentrasystem.com/api/public/v1/me');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('QENTRA_API_KEY'),
        'Content-Type: application/json',
    ],
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

if (! $response['success']) {
    exit($response['error']['code'] . ' : ' . $response['error']['message']);
}
print_r($response['data']);
const response = await fetch('https://qentrasystem.com/api/public/v1/me', {
  headers: {
    Authorization: `Bearer ${process.env.QENTRA_API_KEY}`,
    'Content-Type': 'application/json',
  },
});
const json = await response.json();

if (!json.success) {
  throw new Error(`${json.error.code} : ${json.error.message}`);
}
console.log(json.data);
import os
import requests

response = requests.request(
    "GET",
    "https://qentrasystem.com/api/public/v1/me",
    headers={"Authorization": f"Bearer {os.environ['QENTRA_API_KEY']}"},
    timeout=15,
)
data = response.json()

if not data["success"]:
    raise Exception(f"{data['error']['code']} : {data['error']['message']}")
print(data["data"])

Réponse

{
    "success": true,
    "data": {
        "account": {
            "id": 42,
            "name": "Transports Kouassi",
            "company": "TK SARL"
        },
        "key": {
            "name": "ERP livraison",
            "prefix": "qsk_live_8fK2pQ",
            "mode": "live",
            "scopes": [
                "vehicles:read",
                "history:read"
            ],
            "expires_at": null
        },
        "quota": {
            "period": "month",
            "limit": 200000,
            "used": 1840,
            "resets_at": "2026-11-01T00:00:00+00:00"
        },
        "rate_limit_per_minute": 60
    }
}

Véhicules et positions

Lister les véhicules

GET /vehicles Droit : vehicles:read

Tous les véhicules du compte, avec leur état de connexion. Utilisez /positions pour obtenir aussi la dernière position.

Requête

curl "https://qentrasystem.com/api/public/v1/vehicles" \
  -H "Authorization: Bearer $QENTRA_API_KEY"
<?php
$ch = curl_init('https://qentrasystem.com/api/public/v1/vehicles');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('QENTRA_API_KEY'),
        'Content-Type: application/json',
    ],
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

if (! $response['success']) {
    exit($response['error']['code'] . ' : ' . $response['error']['message']);
}
print_r($response['data']);
const response = await fetch('https://qentrasystem.com/api/public/v1/vehicles', {
  headers: {
    Authorization: `Bearer ${process.env.QENTRA_API_KEY}`,
    'Content-Type': 'application/json',
  },
});
const json = await response.json();

if (!json.success) {
  throw new Error(`${json.error.code} : ${json.error.message}`);
}
console.log(json.data);
import os
import requests

response = requests.request(
    "GET",
    "https://qentrasystem.com/api/public/v1/vehicles",
    headers={"Authorization": f"Bearer {os.environ['QENTRA_API_KEY']}"},
    timeout=15,
)
data = response.json()

if not data["success"]:
    raise Exception(f"{data['error']['code']} : {data['error']['message']}")
print(data["data"])

Réponse

{
    "success": true,
    "data": [
        {
            "id": 9001,
            "name": "Véhicule de test 1",
            "plate": "AB-1234-CI",
            "imei": "860000000009001",
            "model": "Simulateur",
            "vehicle_type": "car",
            "status": "online",
            "last_update": "2026-10-01T14:20:05Z"
        }
    ],
    "meta": {
        "count": 1
    }
}

Détail d’un véhicule

GET /vehicles/9001 Droit : vehicles:read

Un véhicule et sa dernière position connue.

ParamètreTypeObligatoireDescription
id entier Oui Identifiant du véhicule (dans l’adresse).

Requête

curl "https://qentrasystem.com/api/public/v1/vehicles/9001" \
  -H "Authorization: Bearer $QENTRA_API_KEY"
<?php
$ch = curl_init('https://qentrasystem.com/api/public/v1/vehicles/9001');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('QENTRA_API_KEY'),
        'Content-Type: application/json',
    ],
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

if (! $response['success']) {
    exit($response['error']['code'] . ' : ' . $response['error']['message']);
}
print_r($response['data']);
const response = await fetch('https://qentrasystem.com/api/public/v1/vehicles/9001', {
  headers: {
    Authorization: `Bearer ${process.env.QENTRA_API_KEY}`,
    'Content-Type': 'application/json',
  },
});
const json = await response.json();

if (!json.success) {
  throw new Error(`${json.error.code} : ${json.error.message}`);
}
console.log(json.data);
import os
import requests

response = requests.request(
    "GET",
    "https://qentrasystem.com/api/public/v1/vehicles/9001",
    headers={"Authorization": f"Bearer {os.environ['QENTRA_API_KEY']}"},
    timeout=15,
)
data = response.json()

if not data["success"]:
    raise Exception(f"{data['error']['code']} : {data['error']['message']}")
print(data["data"])

Réponse

{
    "success": true,
    "data": {
        "id": 9001,
        "name": "Véhicule de test 1",
        "plate": "AB-1234-CI",
        "imei": "860000000009001",
        "model": "Simulateur",
        "vehicle_type": "car",
        "status": "online",
        "last_update": "2026-10-01T14:20:05Z",
        "position": {
            "latitude": 5.341224,
            "longitude": -4.017311,
            "speed_kmh": 40,
            "course": 212,
            "altitude": 12,
            "fix_time": "2026-10-01T14:20:05Z",
            "valid": true,
            "ignition": true,
            "motion": true,
            "battery_level": 100,
            "power_volts": 12.6,
            "odometer_km": 15230.4
        }
    }
}

Dernières positions

GET /positions Droit : vehicles:read

La dernière position de tous les véhicules en un seul appel. Pour un suivi en continu, interrogez cette route toutes les 15 à 30 secondes, ou utilisez les webhooks pour les alertes.

Requête

curl "https://qentrasystem.com/api/public/v1/positions" \
  -H "Authorization: Bearer $QENTRA_API_KEY"
<?php
$ch = curl_init('https://qentrasystem.com/api/public/v1/positions');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('QENTRA_API_KEY'),
        'Content-Type: application/json',
    ],
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

if (! $response['success']) {
    exit($response['error']['code'] . ' : ' . $response['error']['message']);
}
print_r($response['data']);
const response = await fetch('https://qentrasystem.com/api/public/v1/positions', {
  headers: {
    Authorization: `Bearer ${process.env.QENTRA_API_KEY}`,
    'Content-Type': 'application/json',
  },
});
const json = await response.json();

if (!json.success) {
  throw new Error(`${json.error.code} : ${json.error.message}`);
}
console.log(json.data);
import os
import requests

response = requests.request(
    "GET",
    "https://qentrasystem.com/api/public/v1/positions",
    headers={"Authorization": f"Bearer {os.environ['QENTRA_API_KEY']}"},
    timeout=15,
)
data = response.json()

if not data["success"]:
    raise Exception(f"{data['error']['code']} : {data['error']['message']}")
print(data["data"])

Réponse

{
    "success": true,
    "data": [
        {
            "id": 9001,
            "name": "Véhicule de test 1",
            "status": "online",
            "position": {
                "latitude": 5.341224,
                "longitude": -4.017311,
                "speed_kmh": 40,
                "course": 212,
                "fix_time": "2026-10-01T14:20:05Z",
                "ignition": true
            }
        }
    ],
    "meta": {
        "count": 1
    }
}

Historique et rapports

Historique des positions

GET /vehicles/9001/history Droit : history:read

Tous les points GPS d’une période (7 jours au plus, 5 000 points au plus).

ParamètreTypeObligatoireDescription
from date ISO 8601 Oui Début de la période (UTC conseillé).
to date ISO 8601 Oui Fin de la période, postérieure à from.

Requête

curl "https://qentrasystem.com/api/public/v1/vehicles/9001/history?from=2026-10-01T06:00:00Z&to=2026-10-01T12:00:00Z" \
  -H "Authorization: Bearer $QENTRA_API_KEY"
<?php
$ch = curl_init('https://qentrasystem.com/api/public/v1/vehicles/9001/history?from=2026-10-01T06:00:00Z&to=2026-10-01T12:00:00Z');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('QENTRA_API_KEY'),
        'Content-Type: application/json',
    ],
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

if (! $response['success']) {
    exit($response['error']['code'] . ' : ' . $response['error']['message']);
}
print_r($response['data']);
const response = await fetch('https://qentrasystem.com/api/public/v1/vehicles/9001/history?from=2026-10-01T06:00:00Z&to=2026-10-01T12:00:00Z', {
  headers: {
    Authorization: `Bearer ${process.env.QENTRA_API_KEY}`,
    'Content-Type': 'application/json',
  },
});
const json = await response.json();

if (!json.success) {
  throw new Error(`${json.error.code} : ${json.error.message}`);
}
console.log(json.data);
import os
import requests

response = requests.request(
    "GET",
    "https://qentrasystem.com/api/public/v1/vehicles/9001/history?from=2026-10-01T06:00:00Z&to=2026-10-01T12:00:00Z",
    headers={"Authorization": f"Bearer {os.environ['QENTRA_API_KEY']}"},
    timeout=15,
)
data = response.json()

if not data["success"]:
    raise Exception(f"{data['error']['code']} : {data['error']['message']}")
print(data["data"])

Réponse

{
    "success": true,
    "data": [
        {
            "latitude": 5.336401,
            "longitude": -4.008712,
            "speed_kmh": 38,
            "course": 95,
            "altitude": 12,
            "fix_time": "2026-10-01T06:00:00Z",
            "valid": true,
            "ignition": true,
            "motion": true,
            "battery_level": 100,
            "power_volts": 12.6,
            "odometer_km": 15190.2
        }
    ],
    "meta": {
        "vehicle_id": 9001,
        "from": "2026-10-01T06:00:00Z",
        "to": "2026-10-01T12:00:00Z",
        "count": 361
    }
}

Trajets

GET /vehicles/9001/trips Droit : history:read

Les trajets de la période, avec départ, arrivée, durée, distance et vitesses.

ParamètreTypeObligatoireDescription
from date ISO 8601 Oui Début de la période.
to date ISO 8601 Oui Fin de la période.

Requête

curl "https://qentrasystem.com/api/public/v1/vehicles/9001/trips?from=2026-10-01T00:00:00Z&to=2026-10-02T00:00:00Z" \
  -H "Authorization: Bearer $QENTRA_API_KEY"
<?php
$ch = curl_init('https://qentrasystem.com/api/public/v1/vehicles/9001/trips?from=2026-10-01T00:00:00Z&to=2026-10-02T00:00:00Z');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('QENTRA_API_KEY'),
        'Content-Type: application/json',
    ],
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

if (! $response['success']) {
    exit($response['error']['code'] . ' : ' . $response['error']['message']);
}
print_r($response['data']);
const response = await fetch('https://qentrasystem.com/api/public/v1/vehicles/9001/trips?from=2026-10-01T00:00:00Z&to=2026-10-02T00:00:00Z', {
  headers: {
    Authorization: `Bearer ${process.env.QENTRA_API_KEY}`,
    'Content-Type': 'application/json',
  },
});
const json = await response.json();

if (!json.success) {
  throw new Error(`${json.error.code} : ${json.error.message}`);
}
console.log(json.data);
import os
import requests

response = requests.request(
    "GET",
    "https://qentrasystem.com/api/public/v1/vehicles/9001/trips?from=2026-10-01T00:00:00Z&to=2026-10-02T00:00:00Z",
    headers={"Authorization": f"Bearer {os.environ['QENTRA_API_KEY']}"},
    timeout=15,
)
data = response.json()

if not data["success"]:
    raise Exception(f"{data['error']['code']} : {data['error']['message']}")
print(data["data"])

Réponse

{
    "success": true,
    "data": [
        {
            "start_time": "2026-10-01T07:12:00Z",
            "end_time": "2026-10-01T07:57:00Z",
            "duration_seconds": 2700,
            "distance_km": 18.4,
            "average_speed_kmh": 32,
            "max_speed_kmh": 61,
            "start": {
                "latitude": 5.3364,
                "longitude": -4.0267,
                "address": "Plateau, Abidjan"
            },
            "end": {
                "latitude": 5.3464,
                "longitude": -4.0167,
                "address": "Cocody, Abidjan"
            }
        }
    ],
    "meta": {
        "vehicle_id": 9001,
        "count": 1
    }
}

Arrêts

GET /vehicles/9001/stops Droit : history:read

Les arrêts de la période, avec leur durée et leur position.

ParamètreTypeObligatoireDescription
from date ISO 8601 Oui Début de la période.
to date ISO 8601 Oui Fin de la période.

Requête

curl "https://qentrasystem.com/api/public/v1/vehicles/9001/stops?from=2026-10-01T00:00:00Z&to=2026-10-02T00:00:00Z" \
  -H "Authorization: Bearer $QENTRA_API_KEY"
<?php
$ch = curl_init('https://qentrasystem.com/api/public/v1/vehicles/9001/stops?from=2026-10-01T00:00:00Z&to=2026-10-02T00:00:00Z');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('QENTRA_API_KEY'),
        'Content-Type: application/json',
    ],
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

if (! $response['success']) {
    exit($response['error']['code'] . ' : ' . $response['error']['message']);
}
print_r($response['data']);
const response = await fetch('https://qentrasystem.com/api/public/v1/vehicles/9001/stops?from=2026-10-01T00:00:00Z&to=2026-10-02T00:00:00Z', {
  headers: {
    Authorization: `Bearer ${process.env.QENTRA_API_KEY}`,
    'Content-Type': 'application/json',
  },
});
const json = await response.json();

if (!json.success) {
  throw new Error(`${json.error.code} : ${json.error.message}`);
}
console.log(json.data);
import os
import requests

response = requests.request(
    "GET",
    "https://qentrasystem.com/api/public/v1/vehicles/9001/stops?from=2026-10-01T00:00:00Z&to=2026-10-02T00:00:00Z",
    headers={"Authorization": f"Bearer {os.environ['QENTRA_API_KEY']}"},
    timeout=15,
)
data = response.json()

if not data["success"]:
    raise Exception(f"{data['error']['code']} : {data['error']['message']}")
print(data["data"])

Réponse

{
    "success": true,
    "data": [
        {
            "start_time": "2026-10-01T08:00:00Z",
            "end_time": "2026-10-01T08:15:00Z",
            "duration_seconds": 900,
            "latitude": 5.3464,
            "longitude": -4.0167,
            "address": "Cocody, Abidjan"
        }
    ],
    "meta": {
        "vehicle_id": 9001,
        "count": 1
    }
}

Résumé d’activité

GET /vehicles/9001/summary Droit : history:read

Distance, vitesses et heures moteur de la période, ou jour par jour avec daily=1.

ParamètreTypeObligatoireDescription
from date ISO 8601 Oui Début de la période.
to date ISO 8601 Oui Fin de la période.
daily 0 ou 1 Non 1 : un résumé par jour.

Requête

curl "https://qentrasystem.com/api/public/v1/vehicles/9001/summary?from=2026-09-24T00:00:00Z&to=2026-10-01T00:00:00Z&daily=1" \
  -H "Authorization: Bearer $QENTRA_API_KEY"
<?php
$ch = curl_init('https://qentrasystem.com/api/public/v1/vehicles/9001/summary?from=2026-09-24T00:00:00Z&to=2026-10-01T00:00:00Z&daily=1');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('QENTRA_API_KEY'),
        'Content-Type: application/json',
    ],
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

if (! $response['success']) {
    exit($response['error']['code'] . ' : ' . $response['error']['message']);
}
print_r($response['data']);
const response = await fetch('https://qentrasystem.com/api/public/v1/vehicles/9001/summary?from=2026-09-24T00:00:00Z&to=2026-10-01T00:00:00Z&daily=1', {
  headers: {
    Authorization: `Bearer ${process.env.QENTRA_API_KEY}`,
    'Content-Type': 'application/json',
  },
});
const json = await response.json();

if (!json.success) {
  throw new Error(`${json.error.code} : ${json.error.message}`);
}
console.log(json.data);
import os
import requests

response = requests.request(
    "GET",
    "https://qentrasystem.com/api/public/v1/vehicles/9001/summary?from=2026-09-24T00:00:00Z&to=2026-10-01T00:00:00Z&daily=1",
    headers={"Authorization": f"Bearer {os.environ['QENTRA_API_KEY']}"},
    timeout=15,
)
data = response.json()

if not data["success"]:
    raise Exception(f"{data['error']['code']} : {data['error']['message']}")
print(data["data"])

Réponse

{
    "success": true,
    "data": [
        {
            "date": "2026-09-24T00:00:00Z",
            "distance_km": 42.7,
            "average_speed_kmh": 31,
            "max_speed_kmh": 64,
            "engine_hours": 2.4
        }
    ],
    "meta": {
        "vehicle_id": 9001,
        "count": 7
    }
}

Alertes et zones

Alertes

GET /alerts Droit : alerts:read

Les alertes du compte, de la plus récente à la plus ancienne, avec pagination.

ParamètreTypeObligatoireDescription
status open, resolved ou all Non Filtre sur l’état (all par défaut).
vehicle_id entier Non Un seul véhicule.
type texte Non Type d’alerte, par exemple powerCut, overspeed, geofenceExit.
from / to date ISO 8601 Non Période de survenue.
per_page 1 à 100 Non 50 par défaut.
page entier Non Numéro de page.

Requête

curl "https://qentrasystem.com/api/public/v1/alerts?status=open&per_page=50" \
  -H "Authorization: Bearer $QENTRA_API_KEY"
<?php
$ch = curl_init('https://qentrasystem.com/api/public/v1/alerts?status=open&per_page=50');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('QENTRA_API_KEY'),
        'Content-Type: application/json',
    ],
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

if (! $response['success']) {
    exit($response['error']['code'] . ' : ' . $response['error']['message']);
}
print_r($response['data']);
const response = await fetch('https://qentrasystem.com/api/public/v1/alerts?status=open&per_page=50', {
  headers: {
    Authorization: `Bearer ${process.env.QENTRA_API_KEY}`,
    'Content-Type': 'application/json',
  },
});
const json = await response.json();

if (!json.success) {
  throw new Error(`${json.error.code} : ${json.error.message}`);
}
console.log(json.data);
import os
import requests

response = requests.request(
    "GET",
    "https://qentrasystem.com/api/public/v1/alerts?status=open&per_page=50",
    headers={"Authorization": f"Bearer {os.environ['QENTRA_API_KEY']}"},
    timeout=15,
)
data = response.json()

if not data["success"]:
    raise Exception(f"{data['error']['code']} : {data['error']['message']}")
print(data["data"])

Réponse

{
    "success": true,
    "data": [
        {
            "id": 1,
            "vehicle_id": 9001,
            "type": "geofenceExit",
            "label": "Sortie d'une zone",
            "level": "warning",
            "latitude": 5.3501,
            "longitude": -4.0102,
            "occurrences": 1,
            "occurred_at": "2026-10-01T14:08:00Z",
            "last_seen_at": "2026-10-01T14:08:00Z",
            "resolved": false,
            "resolved_at": null
        }
    ],
    "meta": {
        "page": 1,
        "per_page": 50,
        "total": 1,
        "last_page": 1
    }
}

Zones

GET /zones Droit : zones:read

Les zones (géorepérage) du compte et les véhicules qui y sont rattachés.

Requête

curl "https://qentrasystem.com/api/public/v1/zones" \
  -H "Authorization: Bearer $QENTRA_API_KEY"
<?php
$ch = curl_init('https://qentrasystem.com/api/public/v1/zones');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('QENTRA_API_KEY'),
        'Content-Type: application/json',
    ],
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

if (! $response['success']) {
    exit($response['error']['code'] . ' : ' . $response['error']['message']);
}
print_r($response['data']);
const response = await fetch('https://qentrasystem.com/api/public/v1/zones', {
  headers: {
    Authorization: `Bearer ${process.env.QENTRA_API_KEY}`,
    'Content-Type': 'application/json',
  },
});
const json = await response.json();

if (!json.success) {
  throw new Error(`${json.error.code} : ${json.error.message}`);
}
console.log(json.data);
import os
import requests

response = requests.request(
    "GET",
    "https://qentrasystem.com/api/public/v1/zones",
    headers={"Authorization": f"Bearer {os.environ['QENTRA_API_KEY']}"},
    timeout=15,
)
data = response.json()

if not data["success"]:
    raise Exception(f"{data['error']['code']} : {data['error']['message']}")
print(data["data"])

Réponse

{
    "success": true,
    "data": [
        {
            "id": 1,
            "name": "Dépôt de test",
            "type": "circle",
            "geometry": "CIRCLE (5.3364 -4.0267, 500)",
            "alert_on_enter": true,
            "alert_on_exit": true,
            "vehicle_ids": [
                9001,
                9002,
                9003
            ]
        }
    ],
    "meta": {
        "count": 1
    }
}

Partage

Partager une position

POST /vehicles/9001/shares Droit : shares:write

Crée un lien public de suivi en direct, valable pour la durée choisie.

ParamètreTypeObligatoireDescription
duration 1h, 6h, 24h ou 7j Oui Durée de validité du lien.
label texte Non Libellé interne (60 caractères).

Requête

curl -X POST "https://qentrasystem.com/api/public/v1/vehicles/9001/shares" \
  -H "Authorization: Bearer $QENTRA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"duration":"24h","label":"Livraison client 4587"}'
<?php
$ch = curl_init('https://qentrasystem.com/api/public/v1/vehicles/9001/shares');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_POSTFIELDS => '{"duration":"24h","label":"Livraison client 4587"}',
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('QENTRA_API_KEY'),
        'Content-Type: application/json',
    ],
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

if (! $response['success']) {
    exit($response['error']['code'] . ' : ' . $response['error']['message']);
}
print_r($response['data']);
const response = await fetch('https://qentrasystem.com/api/public/v1/vehicles/9001/shares', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.QENTRA_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({"duration":"24h","label":"Livraison client 4587"}),
});
const json = await response.json();

if (!json.success) {
  throw new Error(`${json.error.code} : ${json.error.message}`);
}
console.log(json.data);
import os
import requests

response = requests.request(
    "POST",
    "https://qentrasystem.com/api/public/v1/vehicles/9001/shares",
    headers={"Authorization": f"Bearer {os.environ['QENTRA_API_KEY']}"},
    json={"duration":"24h","label":"Livraison client 4587"},
    timeout=15,
)
data = response.json()

if not data["success"]:
    raise Exception(f"{data['error']['code']} : {data['error']['message']}")
print(data["data"])

Réponse

{
    "success": true,
    "data": {
        "url": "https://qentrasystem.com/p/Hk2…",
        "expires_at": "2026-10-02T14:20:00Z"
    }
}

Commandes moteur

Couper le moteur

POST /vehicles/9001/engine/stop Droit : commands:engine

Immobilise le véhicule. Refusé si le véhicule roule à plus de 10 km/h. Le propriétaire est notifié et un webhook command.sent est envoyé.

ParamètreTypeObligatoireDescription
confirm true Oui Confirmation explicite obligatoire.

Requête

curl -X POST "https://qentrasystem.com/api/public/v1/vehicles/9001/engine/stop" \
  -H "Authorization: Bearer $QENTRA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"confirm":true}'
<?php
$ch = curl_init('https://qentrasystem.com/api/public/v1/vehicles/9001/engine/stop');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_POSTFIELDS => '{"confirm":true}',
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('QENTRA_API_KEY'),
        'Content-Type: application/json',
    ],
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

if (! $response['success']) {
    exit($response['error']['code'] . ' : ' . $response['error']['message']);
}
print_r($response['data']);
const response = await fetch('https://qentrasystem.com/api/public/v1/vehicles/9001/engine/stop', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.QENTRA_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({"confirm":true}),
});
const json = await response.json();

if (!json.success) {
  throw new Error(`${json.error.code} : ${json.error.message}`);
}
console.log(json.data);
import os
import requests

response = requests.request(
    "POST",
    "https://qentrasystem.com/api/public/v1/vehicles/9001/engine/stop",
    headers={"Authorization": f"Bearer {os.environ['QENTRA_API_KEY']}"},
    json={"confirm":True},
    timeout=15,
)
data = response.json()

if not data["success"]:
    raise Exception(f"{data['error']['code']} : {data['error']['message']}")
print(data["data"])

Réponse

{
    "success": true,
    "data": {
        "status": "sent",
        "message": "Commande envoyée au boîtier.",
        "command": "engine_stop",
        "vehicle_id": 9001
    }
}

Remettre le moteur en marche

POST /vehicles/9001/engine/resume Droit : commands:engine

Lève l’immobilisation. Le propriétaire est notifié.

ParamètreTypeObligatoireDescription
confirm true Oui Confirmation explicite obligatoire.

Requête

curl -X POST "https://qentrasystem.com/api/public/v1/vehicles/9001/engine/resume" \
  -H "Authorization: Bearer $QENTRA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"confirm":true}'
<?php
$ch = curl_init('https://qentrasystem.com/api/public/v1/vehicles/9001/engine/resume');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_POSTFIELDS => '{"confirm":true}',
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('QENTRA_API_KEY'),
        'Content-Type: application/json',
    ],
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

if (! $response['success']) {
    exit($response['error']['code'] . ' : ' . $response['error']['message']);
}
print_r($response['data']);
const response = await fetch('https://qentrasystem.com/api/public/v1/vehicles/9001/engine/resume', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.QENTRA_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({"confirm":true}),
});
const json = await response.json();

if (!json.success) {
  throw new Error(`${json.error.code} : ${json.error.message}`);
}
console.log(json.data);
import os
import requests

response = requests.request(
    "POST",
    "https://qentrasystem.com/api/public/v1/vehicles/9001/engine/resume",
    headers={"Authorization": f"Bearer {os.environ['QENTRA_API_KEY']}"},
    json={"confirm":True},
    timeout=15,
)
data = response.json()

if not data["success"]:
    raise Exception(f"{data['error']['code']} : {data['error']['message']}")
print(data["data"])

Réponse

{
    "success": true,
    "data": {
        "status": "sent",
        "message": "Commande envoyée au boîtier.",
        "command": "engine_resume",
        "vehicle_id": 9001
    }
}

Webhooks

Lister les webhooks

GET /webhooks Droit : webhooks:manage

Les webhooks du compte et l’état de leur dernier envoi.

Requête

curl "https://qentrasystem.com/api/public/v1/webhooks" \
  -H "Authorization: Bearer $QENTRA_API_KEY"
<?php
$ch = curl_init('https://qentrasystem.com/api/public/v1/webhooks');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('QENTRA_API_KEY'),
        'Content-Type: application/json',
    ],
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

if (! $response['success']) {
    exit($response['error']['code'] . ' : ' . $response['error']['message']);
}
print_r($response['data']);
const response = await fetch('https://qentrasystem.com/api/public/v1/webhooks', {
  headers: {
    Authorization: `Bearer ${process.env.QENTRA_API_KEY}`,
    'Content-Type': 'application/json',
  },
});
const json = await response.json();

if (!json.success) {
  throw new Error(`${json.error.code} : ${json.error.message}`);
}
console.log(json.data);
import os
import requests

response = requests.request(
    "GET",
    "https://qentrasystem.com/api/public/v1/webhooks",
    headers={"Authorization": f"Bearer {os.environ['QENTRA_API_KEY']}"},
    timeout=15,
)
data = response.json()

if not data["success"]:
    raise Exception(f"{data['error']['code']} : {data['error']['message']}")
print(data["data"])

Réponse

{
    "success": true,
    "data": [
        {
            "id": 3,
            "url": "https://mon-logiciel.ci/webhooks/qentra",
            "events": [
                "alert.created",
                "alert.resolved"
            ],
            "active": true,
            "failures": 0,
            "last_status": 200,
            "last_delivery_at": "2026-10-01T14:08:02Z",
            "created_at": "2026-09-30T10:00:00Z"
        }
    ]
}

Créer un webhook

POST /webhooks Droit : webhooks:manage

Enregistre une adresse HTTPS publique. Le secret de signature n’est renvoyé qu’à la création.

ParamètreTypeObligatoireDescription
url adresse HTTPS Oui Adresse de votre serveur (les adresses internes sont refusées).
events liste Oui alert.created, alert.resolved, command.sent.

Requête

curl -X POST "https://qentrasystem.com/api/public/v1/webhooks" \
  -H "Authorization: Bearer $QENTRA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://mon-logiciel.ci/webhooks/qentra","events":["alert.created","alert.resolved"]}'
<?php
$ch = curl_init('https://qentrasystem.com/api/public/v1/webhooks');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_POSTFIELDS => '{"url":"https://mon-logiciel.ci/webhooks/qentra","events":["alert.created","alert.resolved"]}',
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('QENTRA_API_KEY'),
        'Content-Type: application/json',
    ],
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

if (! $response['success']) {
    exit($response['error']['code'] . ' : ' . $response['error']['message']);
}
print_r($response['data']);
const response = await fetch('https://qentrasystem.com/api/public/v1/webhooks', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.QENTRA_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({"url":"https://mon-logiciel.ci/webhooks/qentra","events":["alert.created","alert.resolved"]}),
});
const json = await response.json();

if (!json.success) {
  throw new Error(`${json.error.code} : ${json.error.message}`);
}
console.log(json.data);
import os
import requests

response = requests.request(
    "POST",
    "https://qentrasystem.com/api/public/v1/webhooks",
    headers={"Authorization": f"Bearer {os.environ['QENTRA_API_KEY']}"},
    json={"url":"https://mon-logiciel.ci/webhooks/qentra","events":["alert.created","alert.resolved"]},
    timeout=15,
)
data = response.json()

if not data["success"]:
    raise Exception(f"{data['error']['code']} : {data['error']['message']}")
print(data["data"])

Réponse

{
    "success": true,
    "data": {
        "id": 3,
        "url": "https://mon-logiciel.ci/webhooks/qentra",
        "events": [
            "alert.created",
            "alert.resolved"
        ],
        "active": true,
        "failures": 0,
        "last_status": null,
        "last_delivery_at": null,
        "created_at": "2026-10-01T14:20:00Z",
        "secret": "whsec_…"
    },
    "meta": {
        "notice": "Conservez ce secret : il ne sera plus jamais affiché."
    }
}

Tester un webhook

POST /webhooks/3/test Droit : webhooks:manage

Envoie un événement test.ping signé à votre adresse.

Requête

curl -X POST "https://qentrasystem.com/api/public/v1/webhooks/3/test" \
  -H "Authorization: Bearer $QENTRA_API_KEY"
<?php
$ch = curl_init('https://qentrasystem.com/api/public/v1/webhooks/3/test');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('QENTRA_API_KEY'),
        'Content-Type: application/json',
    ],
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

if (! $response['success']) {
    exit($response['error']['code'] . ' : ' . $response['error']['message']);
}
print_r($response['data']);
const response = await fetch('https://qentrasystem.com/api/public/v1/webhooks/3/test', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.QENTRA_API_KEY}`,
    'Content-Type': 'application/json',
  },
});
const json = await response.json();

if (!json.success) {
  throw new Error(`${json.error.code} : ${json.error.message}`);
}
console.log(json.data);
import os
import requests

response = requests.request(
    "POST",
    "https://qentrasystem.com/api/public/v1/webhooks/3/test",
    headers={"Authorization": f"Bearer {os.environ['QENTRA_API_KEY']}"},
    timeout=15,
)
data = response.json()

if not data["success"]:
    raise Exception(f"{data['error']['code']} : {data['error']['message']}")
print(data["data"])

Réponse

{
    "success": true,
    "data": {
        "event_id": "5b0c7e2a-1f7d-4a38-9a51-0c8a2b7d1e44",
        "status": "queued"
    }
}

Historique des envois

GET /webhooks/3/deliveries Droit : webhooks:manage

Les 50 derniers envois d’un webhook, avec le code HTTP reçu.

Requête

curl "https://qentrasystem.com/api/public/v1/webhooks/3/deliveries" \
  -H "Authorization: Bearer $QENTRA_API_KEY"
<?php
$ch = curl_init('https://qentrasystem.com/api/public/v1/webhooks/3/deliveries');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('QENTRA_API_KEY'),
        'Content-Type: application/json',
    ],
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

if (! $response['success']) {
    exit($response['error']['code'] . ' : ' . $response['error']['message']);
}
print_r($response['data']);
const response = await fetch('https://qentrasystem.com/api/public/v1/webhooks/3/deliveries', {
  headers: {
    Authorization: `Bearer ${process.env.QENTRA_API_KEY}`,
    'Content-Type': 'application/json',
  },
});
const json = await response.json();

if (!json.success) {
  throw new Error(`${json.error.code} : ${json.error.message}`);
}
console.log(json.data);
import os
import requests

response = requests.request(
    "GET",
    "https://qentrasystem.com/api/public/v1/webhooks/3/deliveries",
    headers={"Authorization": f"Bearer {os.environ['QENTRA_API_KEY']}"},
    timeout=15,
)
data = response.json()

if not data["success"]:
    raise Exception(f"{data['error']['code']} : {data['error']['message']}")
print(data["data"])

Réponse

{
    "success": true,
    "data": [
        {
            "event_id": "5b0c7e2a-1f7d-4a38-9a51-0c8a2b7d1e44",
            "event": "test.ping",
            "status": "success",
            "attempts": 1,
            "response_code": 200,
            "error": null,
            "created_at": "2026-10-01T14:20:01Z",
            "delivered_at": "2026-10-01T14:20:02Z"
        }
    ]
}

Supprimer un webhook

DELETE /webhooks/3 Droit : webhooks:manage

Arrête définitivement les envois vers cette adresse.

Requête

curl -X DELETE "https://qentrasystem.com/api/public/v1/webhooks/3" \
  -H "Authorization: Bearer $QENTRA_API_KEY"
<?php
$ch = curl_init('https://qentrasystem.com/api/public/v1/webhooks/3');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'DELETE',
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('QENTRA_API_KEY'),
        'Content-Type: application/json',
    ],
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

if (! $response['success']) {
    exit($response['error']['code'] . ' : ' . $response['error']['message']);
}
print_r($response['data']);
const response = await fetch('https://qentrasystem.com/api/public/v1/webhooks/3', {
  method: 'DELETE',
  headers: {
    Authorization: `Bearer ${process.env.QENTRA_API_KEY}`,
    'Content-Type': 'application/json',
  },
});
const json = await response.json();

if (!json.success) {
  throw new Error(`${json.error.code} : ${json.error.message}`);
}
console.log(json.data);
import os
import requests

response = requests.request(
    "DELETE",
    "https://qentrasystem.com/api/public/v1/webhooks/3",
    headers={"Authorization": f"Bearer {os.environ['QENTRA_API_KEY']}"},
    timeout=15,
)
data = response.json()

if not data["success"]:
    raise Exception(f"{data['error']['code']} : {data['error']['message']}")
print(data["data"])

Réponse

{
    "success": true,
    "data": {
        "deleted": true,
        "id": 3
    }
}

Webhooks

Plutôt que d’interroger l’API en boucle, recevez les événements sur votre serveur dès qu’ils se produisent. Chaque envoi est une requête POST en JSON vers votre adresse HTTPS, signée avec le secret du webhook.

ÉvénementEnvoyé quand
alert.createdNouvelle alerte
alert.resolvedAlerte résolue
command.sentCommande moteur envoyée
test.pingEnvoi de test demandé par la route /webhooks/{id}/test

Contenu envoyé

{
    "id": "0f6e2c1a-8d4b-4a1e-9b7c-3e2d1f0a9b8c",
    "type": "alert.created",
    "created_at": "2026-10-01T14:08:01Z",
    "data": {
        "id": 128,
        "vehicle_id": 9001,
        "type": "powerCut",
        "label": "Coupure d'alimentation",
        "level": "critical",
        "latitude": 5.3501,
        "longitude": -4.0102,
        "occurrences": 1,
        "occurred_at": "2026-10-01T14:08:00Z",
        "last_seen_at": "2026-10-01T14:08:00Z",
        "resolved": false,
        "resolved_at": null,
        "vehicle": {
            "id": 9001,
            "name": "Véhicule de test 1",
            "plate": "AB-1234-CI"
        }
    }
}

En-têtes

Qentra-Signaturet=1759327681,v1=5f2b… : horodatage et signature HMAC-SHA256.
Qentra-EventType de l’événement, par exemple alert.created.
Qentra-DeliveryIdentifiant unique de l’envoi : servez-vous-en pour ignorer un doublon.
Content-Typeapplication/json
User-AgentQentra-Webhooks/1.0

Vérifier la signature

Calculez HMAC-SHA256(secret, t + "." + corps brut) en hexadécimal et comparez-le à v1 avec une comparaison à temps constant. Refusez les envois de plus de 5 minutes pour bloquer les rejeux. Utilisez toujours le corps brut, avant tout décodage JSON.

<?php
// Laravel, Symfony ou PHP natif : lire le corps brut, avant tout décodage
$payload   = file_get_contents('php://input');
$header    = $_SERVER['HTTP_QENTRA_SIGNATURE'] ?? '';
$secret    = getenv('QENTRA_WEBHOOK_SECRET'); // whsec_...

parse_str(str_replace(',', '&', $header), $parts); // t=...&v1=...
$expected = hash_hmac('sha256', $parts['t'] . '.' . $payload, $secret);

if (! hash_equals($expected, $parts['v1'] ?? '') || abs(time() - (int) $parts['t']) > 300) {
    http_response_code(400);
    exit('Signature invalide');
}

$event = json_decode($payload, true);
// $event['type'] : alert.created, alert.resolved, command.sent, test.ping
http_response_code(200);
// Node.js + Express : garder le corps brut pour vérifier la signature
import crypto from 'node:crypto';
import express from 'express';

const app = express();

app.post('/webhooks/qentra', express.raw({ type: 'application/json' }), (req, res) => {
  const header = req.get('Qentra-Signature') || '';
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  const expected = crypto
    .createHmac('sha256', process.env.QENTRA_WEBHOOK_SECRET)
    .update(`${parts.t}.${req.body}`)
    .digest('hex');

  const valid = parts.v1 && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
  if (!valid || Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) {
    return res.status(400).send('Signature invalide');
  }

  const event = JSON.parse(req.body);
  console.log(event.type, event.data);
  res.sendStatus(200);
});

app.listen(3000);
# Flask : lire le corps brut avant de le décoder
import hashlib, hmac, json, os, time
from flask import Flask, request, abort

app = Flask(__name__)

@app.post("/webhooks/qentra")
def qentra_webhook():
    payload = request.get_data()
    parts = dict(p.split("=", 1) for p in request.headers.get("Qentra-Signature", "").split(",") if "=" in p)
    expected = hmac.new(
        os.environ["QENTRA_WEBHOOK_SECRET"].encode(),
        f"{parts.get('t')}.".encode() + payload,
        hashlib.sha256,
    ).hexdigest()

    if not hmac.compare_digest(expected, parts.get("v1", "")) or abs(time.time() - int(parts.get("t", 0))) > 300:
        abort(400, "Signature invalide")

    event = json.loads(payload)
    print(event["type"], event["data"])
    return "", 200

Réponse attendue et nouvelles tentatives

  • Répondez par un code 2xx en moins de 10 secondes, puis traitez l’événement en arrière-plan.
  • Sinon, l’envoi est retenté 5 fois : après 1 minute, 5 minutes, 30 minutes, 2 heures puis 6 heures.
  • Après 20 échecs d’affilée, le webhook est désactivé. Réactivez-le depuis votre espace une fois votre serveur réparé.
  • Un même événement peut arriver deux fois : dédoublonnez avec Qentra-Delivery.
  • Seules les adresses HTTPS publiques sont acceptées (pas d’adresse locale ni privée). 5 webhooks au plus par compte.

Codes d’erreur

En cas d’erreur, success vaut false et error.code contient l’un des codes ci-dessous. Basez votre logique sur ce code, pas sur le message, qui peut changer.

CodeHTTPSignificationQue faire
missing_api_key 401 En-tête Authorization absent. Ajoutez « Authorization: Bearer qsk_… ».
invalid_api_key 401 Clé inconnue ou mal recopiée. Vérifiez la clé, sans espace ni retour à la ligne.
revoked_api_key 401 Clé révoquée. Créez une nouvelle clé dans votre espace.
expired_api_key 401 Clé expirée. Créez une nouvelle clé dans votre espace.
account_disabled 403 Compte désactivé. Contactez le service client.
ip_not_allowed 403 Appel depuis une IP non autorisée pour cette clé. Ajoutez l’IP de votre serveur à la clé.
insufficient_scope 403 La clé n’a pas le droit nécessaire (indiqué dans required_scope). Créez une clé avec ce droit.
forbidden 403 Le compte n’est pas autorisé à cette action. Demandez l’autorisation à votre gestionnaire.
vehicle_not_found 404 Véhicule inconnu ou absent de ce compte. Utilisez un id renvoyé par /vehicles.
webhook_not_found 404 Webhook inconnu. Utilisez un id renvoyé par /webhooks.
route_not_found 404 Adresse ou méthode inconnue. Vérifiez l’adresse et la méthode (GET, POST, DELETE).
vehicle_moving 409 Coupure moteur refusée : le véhicule roule (vitesse dans speed_kmh). Réessayez quand le véhicule est à l’arrêt.
validation_failed 422 Paramètre manquant ou invalide (détail par champ dans fields). Corrigez les champs indiqués.
period_too_long 422 Période supérieure au maximum (max_days). Découpez la période.
too_many_points 422 Trop de points GPS sur la période. Réduisez la période.
invalid_webhook_url 422 Adresse refusée : pas en HTTPS, interne ou introuvable. Utilisez une adresse HTTPS publique.
limit_reached 422 Nombre maximal de webhooks atteint. Supprimez un webhook inutilisé.
share_limit_reached 422 Trop de liens de partage actifs pour ce véhicule. Révoquez un lien existant.
command_unavailable 422 Commande non disponible sur la plateforme. Contactez le service client.
service_required 402 Service non souscrit pour ce véhicule (option indiquée dans feature). Activez le service dans votre espace, rubrique Abonnement.
rate_limited 429 Plus de 60 requêtes par minute (attente dans retry_after). Attendez le délai indiqué par l’en-tête Retry-After.
quota_exceeded 429 Quota du mois (ou du jour en test) atteint (reprise dans resets_at). Attendez la remise à zéro ou contactez-nous.
command_failed 502 Le boîtier n’a pas accepté la commande. Vérifiez que le boîtier est en ligne, puis réessayez.
gps_unavailable 503 Serveur GPS momentanément indisponible. Réessayez après quelques secondes.

Exemples de réponses d’erreur :

422 validation_failed

{
    "success": false,
    "error": {
        "code": "validation_failed",
        "message": "Paramètres invalides.",
        "fields": {
            "from": [
                "Le champ from est obligatoire."
            ]
        },
        "request_id": "c3a1…"
    }
}

429 rate_limited

{
    "success": false,
    "error": {
        "code": "rate_limited",
        "message": "Trop de requêtes, réessayez dans 12 secondes.",
        "retry_after": 12,
        "request_id": "d4b2…"
    }
}

Pour les erreurs 429, 502 et 503, réessayez avec un délai croissant (1 s, 2 s, 4 s…). Ne réessayez pas automatiquement les erreurs 401, 403, 404 et 422 : la même requête échouerait encore.

Bonnes pratiques

  • Une clé par logiciel, avec seulement les droits utiles : si une clé fuit, vous la révoquez sans couper les autres.
  • Limitez les clés de production aux adresses IP de vos serveurs.
  • Ne mettez jamais une clé dans une application mobile, un site public ou un dépôt Git. Appelez l’API depuis votre serveur.
  • Remplacez vos clés régulièrement : créez la nouvelle, déployez-la, puis révoquez l’ancienne.
  • Réservez le droit commands:engine aux logiciels qui en ont vraiment besoin et journalisez chaque coupure de votre côté.
  • Surveillez la page « Clés API » de votre espace : elle montre les derniers appels, leur code de réponse et leur identifiant.

Vous pensez avoir trouvé une faille ? Écrivez à service@qentrasystem.com avant toute publication.

Versions

La version actuelle est v1, indiquée dans l’adresse (/api/public/v1). Dans une même version, nous ajoutons des champs et des routes sans rien retirer : ignorez les champs que vous ne connaissez pas. Un changement incompatible donnera lieu à une v2, annoncée par e-mail au moins 6 mois avant l’arrêt de la v1.

Une question sur l’intégration ?

Indiquez l’identifiant de requête (X-Request-Id) concerné : nous retrouvons l’appel immédiatement.

Contacter l’équipe technique