Tu es un développeur expérimenté. Je veux construire : [DÉCRIS TON PROJET EN UNE OU DEUX PHRASES].
Ce projet utilise l’API GPS Qentra System. Sa documentation complète est ci-dessous : respecte-la strictement et applique toutes ses règles impératives.
Avant d’écrire du code, pose-moi les questions utiles (langage, hébergement, base de données). Ensuite, avance étape par étape : explique où placer chaque fichier et comment le tester avec ma clé de test.
Ne me demande jamais ma clé API dans la conversation : utilise la variable d’environnement QENTRA_API_KEY.
---
# API Qentra System : contexte pour assistant de code
API REST de suivi GPS de véhicules (positions, historique, trajets, alertes, zones, partage, webhooks, commandes moteur).
Documentation officielle : https://qentrasystem.com/developpeurs/documentation
## Règles impératives
- La clé API reste côté serveur, dans une variable d’environnement QENTRA_API_KEY. Jamais dans le navigateur, une application mobile, une capture d’écran ou un dépôt Git.
- Commencer avec une clé de test qsk_test_ (véhicules simulés 9001, 9002 et 9003), puis passer à une clé qsk_live_ sans changer le code.
- Toujours lire le champ success, puis error.code en cas d’échec. Ne jamais baser la logique sur error.message.
- Respecter 60 requêtes par minute et par clé. Sur une erreur 429, attendre la durée de l’en-tête Retry-After. Réessayer avec un délai croissant uniquement pour 429, 502 et 503.
- Ne pas appeler /positions plus d’une fois toutes les 15 secondes. Utiliser les webhooks pour les alertes au lieu d’interroger en boucle.
- Historique : 7 jours au plus par appel. Découper les périodes plus longues.
- Webhooks : vérifier la signature Qentra-Signature (HMAC-SHA256 de « t.corps_brut » avec le secret whsec_) par comparaison à temps constant, refuser les envois de plus de 5 minutes, répondre 2xx en moins de 10 secondes, dédoublonner avec Qentra-Delivery.
- Commandes moteur : demander une confirmation explicite à l’utilisateur dans l’interface, envoyer {"confirm":true} et gérer l’erreur vehicle_moving.
- N’utiliser que les routes et les champs décrits dans ce document. Ne rien inventer : en cas de doute, demander.
## Bases
- URL de base : `https://qentrasystem.com/api/public/v1`
- Authentification : en-tête `Authorization: Bearer <clé>` sur chaque requête, HTTPS uniquement.
- Corps des requêtes POST : JSON avec `Content-Type: application/json`. Toujours envoyer `Accept: application/json`.
- Succès : `{"success":true,"data":...,"meta":{...}}`. Erreur : `{"success":false,"error":{"code":"...","message":"...","request_id":"..."}}`.
- Listes paginées : `meta.page`, `meta.per_page`, `meta.total`, `meta.last_page`.
- Dates ISO 8601 en UTC, vitesses en km/h, distances en kilomètres.
- En-têtes de réponse : `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-Quota-Limit`, `X-Quota-Remaining`, `Retry-After` (après 429), `X-Request-Id`.
- Quota : 20 000 appels par véhicule et par mois en production, 1 000 par jour avec une clé de test.
## Droits des clés (scopes)
- `vehicles:read` : Lire les véhicules et leurs positions
- `history:read` : Lire l'historique et les rapports
- `alerts:read` : Lire les alertes
- `zones:read` : Lire les zones
- `shares:write` : Créer des liens de partage de position
- `webhooks:manage` : Gérer les webhooks
- `commands:engine` : Couper et remettre le moteur
## Routes
### 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.
Exemple : `GET https://qentrasystem.com/api/public/v1/me`
Réponse :
```json
{
"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
}
}
```
### 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.
Exemple : `GET https://qentrasystem.com/api/public/v1/vehicles`
Réponse :
```json
{
"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ètres :
- `id` (entier, obligatoire) : Identifiant du véhicule (dans l’adresse).
Exemple : `GET https://qentrasystem.com/api/public/v1/vehicles/9001`
Réponse :
```json
{
"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.
Exemple : `GET https://qentrasystem.com/api/public/v1/positions`
Réponse :
```json
{
"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 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ètres :
- `from` (date ISO 8601, obligatoire) : Début de la période (UTC conseillé).
- `to` (date ISO 8601, obligatoire) : Fin de la période, postérieure à from.
Exemple : `GET https://qentrasystem.com/api/public/v1/vehicles/9001/history?from=2026-10-01T06:00:00Z&to=2026-10-01T12:00:00Z`
Réponse :
```json
{
"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ètres :
- `from` (date ISO 8601, obligatoire) : Début de la période.
- `to` (date ISO 8601, obligatoire) : Fin de la période.
Exemple : `GET https://qentrasystem.com/api/public/v1/vehicles/9001/trips?from=2026-10-01T00:00:00Z&to=2026-10-02T00:00:00Z`
Réponse :
```json
{
"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ètres :
- `from` (date ISO 8601, obligatoire) : Début de la période.
- `to` (date ISO 8601, obligatoire) : Fin de la période.
Exemple : `GET https://qentrasystem.com/api/public/v1/vehicles/9001/stops?from=2026-10-01T00:00:00Z&to=2026-10-02T00:00:00Z`
Réponse :
```json
{
"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ètres :
- `from` (date ISO 8601, obligatoire) : Début de la période.
- `to` (date ISO 8601, obligatoire) : Fin de la période.
- `daily` (0 ou 1, facultatif) : 1 : un résumé par jour.
Exemple : `GET https://qentrasystem.com/api/public/v1/vehicles/9001/summary?from=2026-09-24T00:00:00Z&to=2026-10-01T00:00:00Z&daily=1`
Réponse :
```json
{
"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
`GET /alerts` (droit `alerts:read`)
Les alertes du compte, de la plus récente à la plus ancienne, avec pagination.
Paramètres :
- `status` (open, resolved ou all, facultatif) : Filtre sur l’état (all par défaut).
- `vehicle_id` (entier, facultatif) : Un seul véhicule.
- `type` (texte, facultatif) : Type d’alerte, par exemple powerCut, overspeed, geofenceExit.
- `from / to` (date ISO 8601, facultatif) : Période de survenue.
- `per_page` (1 à 100, facultatif) : 50 par défaut.
- `page` (entier, facultatif) : Numéro de page.
Exemple : `GET https://qentrasystem.com/api/public/v1/alerts?status=open&per_page=50`
Réponse :
```json
{
"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.
Exemple : `GET https://qentrasystem.com/api/public/v1/zones`
Réponse :
```json
{
"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
}
}
```
### 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ètres :
- `duration` (1h, 6h, 24h ou 7j, obligatoire) : Durée de validité du lien.
- `label` (texte, facultatif) : Libellé interne (60 caractères).
Exemple : `POST https://qentrasystem.com/api/public/v1/vehicles/9001/shares`
Corps :
```json
{
"duration": "24h",
"label": "Livraison client 4587"
}
```
Réponse :
```json
{
"success": true,
"data": {
"url": "https://qentrasystem.com/p/Hk2…",
"expires_at": "2026-10-02T14:20:00Z"
}
}
```
### 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ètres :
- `confirm` (true, obligatoire) : Confirmation explicite obligatoire.
Exemple : `POST https://qentrasystem.com/api/public/v1/vehicles/9001/engine/stop`
Corps :
```json
{
"confirm": true
}
```
Réponse :
```json
{
"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ètres :
- `confirm` (true, obligatoire) : Confirmation explicite obligatoire.
Exemple : `POST https://qentrasystem.com/api/public/v1/vehicles/9001/engine/resume`
Corps :
```json
{
"confirm": true
}
```
Réponse :
```json
{
"success": true,
"data": {
"status": "sent",
"message": "Commande envoyée au boîtier.",
"command": "engine_resume",
"vehicle_id": 9001
}
}
```
### Lister les webhooks
`GET /webhooks` (droit `webhooks:manage`)
Les webhooks du compte et l’état de leur dernier envoi.
Exemple : `GET https://qentrasystem.com/api/public/v1/webhooks`
Réponse :
```json
{
"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ètres :
- `url` (adresse HTTPS, obligatoire) : Adresse de votre serveur (les adresses internes sont refusées).
- `events` (liste, obligatoire) : alert.created, alert.resolved, command.sent.
Exemple : `POST https://qentrasystem.com/api/public/v1/webhooks`
Corps :
```json
{
"url": "https://mon-logiciel.ci/webhooks/qentra",
"events": [
"alert.created",
"alert.resolved"
]
}
```
Réponse :
```json
{
"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.
Exemple : `POST https://qentrasystem.com/api/public/v1/webhooks/3/test`
Réponse :
```json
{
"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.
Exemple : `GET https://qentrasystem.com/api/public/v1/webhooks/3/deliveries`
Réponse :
```json
{
"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.
Exemple : `DELETE https://qentrasystem.com/api/public/v1/webhooks/3`
Réponse :
```json
{
"success": true,
"data": {
"deleted": true,
"id": 3
}
}
```
## Webhooks
Requête POST JSON envoyée à votre adresse HTTPS publique pour chaque événement.
- `alert.created` : Nouvelle alerte
- `alert.resolved` : Alerte résolue
- `command.sent` : Commande moteur envoyée
- `test.ping` : envoi de test
Contenu : `{"id":"<uuid>","type":"alert.created","created_at":"2026-10-01T14:08:01Z","data":{ alerte au format de GET /alerts + "vehicle":{"id","name","plate"} }}`
En-têtes : `Qentra-Signature: t=<horodatage>,v1=<hex>`, `Qentra-Event`, `Qentra-Delivery` (identifiant unique de l’envoi), `User-Agent: Qentra-Webhooks/1.0`.
Signature : `v1 = hex(HMAC_SHA256(secret_whsec, t + "." + corps_brut))`. Comparer à temps constant et refuser si l’horodatage a plus de 300 secondes.
Nouvelles tentatives si la réponse n’est pas 2xx : 5 fois (1 min, 5 min, 30 min, 2 h, 6 h). Désactivation après 20 échecs consécutifs.
## Codes d’erreur
| Code | HTTP | Signification | Que 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. |