# 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. |
