pycar Upload API v1
API version 1 · 30 octobre 2026
pycar peut envoyer chaque dossier véhicule — photos et fiche du véhicule — vers un serveur que vous exploitez (DMS, site web, place de marché, stockage interne). Cette page décrit les points d’accès que votre serveur doit fournir. pycar n’envoie jamais les photos ailleurs.
Déroulement d’un envoi
- L’app s’authentifie (jeton OAuth 2.0, ou HTTP Basic).
PUT /v1/vehicles/{vehicle_id}— la fiche du véhicule (JSON).PUT /v1/vehicles/{vehicle_id}/photos/{photo_id}— une fois par photo (multipart).POST /v1/vehicles/{vehicle_id}/complete— fin de l’envoi, avec la liste des photos.
Chaque appel est idempotent : les identifiants sont stables, un envoi interrompu reprend simplement, et une photo déjà reçue avec la même empreinte SHA-256 n’est pas renvoyée.
Réglages dans l’app
| Serveur | Adresse de base de votre API, par ex. https://api.exemple.com/pycar. HTTPS obligatoire. L’app ajoute /v1/… et /oauth/token. |
| Méthode | OAuth 2.0 par identifiant / mot de passe, OAuth 2.0 par client ID / secret, ou HTTP Basic. |
| Identifiant, mot de passe | Enregistrés dans le trousseau sécurisé du téléphone. |
| Client ID / secret | Facultatifs avec identifiant / mot de passe ; obligatoires en client credentials. |
| Avancé | URL du jeton (par défaut {serveur}/oauth/token) et scope (par défaut pycar.upload). |
| Contenu | Choix de ce qui est envoyé : extérieurs d’origine, photos studio, détails, points, 360°, photo du VIN. Envoi automatique possible à la validation du dossier. |
Authentification
OAuth 2.0, identifiant / mot de passe (grant password, RFC 6749 §4.3). Si un client ID est renseigné, il est transmis en HTTP Basic (RFC 6749 §2.3.1).
POST /oauth/token HTTP/1.1
Host: api.example.com
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64(client_id:client_secret) ← only if a client ID is set
grant_type=password&username=dealer42&password=••••••&scope=pycar.uploadHTTP/1.1 200 OK
Content-Type: application/json
{
"access_token": "eyJhbGciOi…",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "8xLOxBtZp8…",
"scope": "pycar.upload"
}
OAuth 2.0, client credentials (grant client_credentials, §4.4) :
grant_type=client_credentials&scope=pycar.upload
(client_id / client_secret in the Authorization: Basic header)Renouvellement : à l’expiration, pycar utilise le refresh token s’il a été fourni, sinon il se reconnecte :
grant_type=refresh_token&refresh_token=8xLOxBtZp8…HTTP Basic : chaque requête porte Authorization: Basic base64(identifiant:mot_de_passe) ; aucun point d’accès de jeton n’est nécessaire.
Les erreurs suivent la RFC 6749 : renvoyez 400 invalid_grant ou 401 invalid_client avec error_description — pycar l’affiche à l’utilisateur.
Points d’accès
GET /v1/ping
Test de connexion (bouton « Tester la connexion » de l’app). Renvoyez 200 avec un objet JSON ; le champ facultatif name est affiché.
PUT /v1/vehicles/{vehicle_id}
Crée ou met à jour la fiche du véhicule. Renvoyez 200 ou 201. vehicle_id est unique par téléphone et par véhicule (préfixe d’installation + dossier local). Les champs peuvent être vides ; mileage_km peut valoir null.
PUT /v1/vehicles/3f9a1c2e-Scan_2026-10-28_14-02-20 HTTP/1.1
Authorization: Bearer eyJhbGciOi…
Content-Type: application/json
{
"vehicle_id": "3f9a1c2e-Scan_2026-10-28_14-02-20",
"plate": "AB-123-CD",
"vin": "WMWXJ1C0X0TV12345",
"brand": "Mini",
"model": "Countryman ALL4",
"mileage_km": 48200,
"stock_ref": "VO-2026-0142",
"notes": "First owner, full service history",
"shot_plan": "standard",
"template": "Standard listing",
"decor": "studio_blanc",
"validated": true,
"created_at": "2026-10-28T14:02:20Z",
"hotspots": [
{ "hotspot_id": "6e05672e-…", "kind": "quality", "photo_id": "ext-02_left",
"x": 0.448, "y": 0.539, "zone": "front_left_door", "zone_name": "Front left door",
"note": "New paint", "detail_photo_id": "hs-6e05672e-…" }
],
"app": { "name": "pycar", "platform": "ios", "version": "1.0" }
}
PUT /v1/vehicles/{vehicle_id}/photos/{photo_id}
Crée ou remplace une photo. Deux parties : metadata (JSON) puis file (JPEG). Vérifiez le SHA-256 et renvoyez 200/201 ; renvoyez 422 si l’empreinte ne correspond pas.
PUT /v1/vehicles/3f9a1c2e-Scan_2026-10-28_14-02-20/photos/studio-02_left-studio_blanc HTTP/1.1
Authorization: Bearer eyJhbGciOi…
Content-Type: multipart/form-data; boundary=pycar-7F3A…
--pycar-7F3A…
Content-Disposition: form-data; name="metadata"
Content-Type: application/json
{ "photo_id": "studio-02_left-studio_blanc", "vehicle_id": "3f9a1c2e-Scan_2026-10-28_14-02-20",
"kind": "studio", "view": "left", "decor": "studio_blanc", "source_photo_id": "ext-02_left",
"taken_at": "2026-10-28T17:34:02Z", "width": 3000, "height": 1688, "size": 861455,
"sha256": "9b1f…", "content_type": "image/jpeg", "plates_masked": true }
--pycar-7F3A…
Content-Disposition: form-data; name="file"; filename="studio-02_left-studio_blanc.jpg"
Content-Type: image/jpeg
<JPEG bytes>
--pycar-7F3A…--
POST /v1/vehicles/{vehicle_id}/complete
Envoyé après la dernière photo, avec tous les identifiants de photos de l’envoi. Utilisez-le pour déclencher votre import (publication, synchronisation DMS…).
POST /v1/vehicles/3f9a1c2e-Scan_2026-10-28_14-02-20/complete HTTP/1.1
Authorization: Bearer eyJhbGciOi…
Content-Type: application/json
{ "vehicle_id": "3f9a1c2e-Scan_2026-10-28_14-02-20",
"photo_ids": ["ext-02_left", "studio-02_left-studio_blanc", "det-07_jante", "pano-360", "vin"],
"sent_at": "2026-10-30T15:12:44Z" }
Types de photos
| kind | Contenu | Champs de métadonnées en plus |
|---|---|---|
exterior | Photo extérieure d’origine (plaques déjà masquées) | view |
studio | Photo extérieure dans le décor studio du véhicule | view, decor, source_photo_id |
detail | Photo de détail de la liste (jante, compteur…) | slot, slot_name |
hotspot | Photo de détail d’un point qualité ou défaut | hotspot_id, hotspot_kind, zone, zone_name, note |
panorama_360 | Photo 360° (JPEG équirectangulaire, 2:1) | projection |
vin | Photo du numéro de châssis (VIN) | — |
Toujours présents : photo_id, vehicle_id, kind, sha256, size, width, height, content_type (image/jpeg) ; en général taken_at (ISO 8601) et plates_masked.
Identifiants de photos : ext-… extérieur, studio-…-{décor} studio, det-… détail, hs-{hotspot_id} point, pano-360, vin.
Codes de retour et reprises
| 2xx | Succès. |
| 401 | Jeton refusé : pycar redemande un jeton une fois, puis signale l’erreur. |
| 413 | Trop volumineux : acceptez au moins 25 Mo par requête (photos 48 Mpx). |
| 422 | Contenu ou empreinte invalide : signalé à l’utilisateur. |
| 429 / 5xx | 3 essais avec délai croissant ; Retry-After (secondes) est respecté. |
| Erreur réseau | 3 essais, puis signalée. Un nouvel envoi reprend là où il s’était arrêté. |
Sécurité
- HTTPS uniquement, certificat valide (pycar refuse le HTTP simple).
- Scope
pycar.uploadlimité à l’écriture des photos. - Jetons d’accès courts (1 h) et refresh tokens révocables.
- Vérifiez
sha256, le type JPEG et la taille avant d’enregistrer. - Ne jamais utiliser
vehicle_id/photo_idcomme chemins de fichiers sans contrôle.
Serveur de référence
Un serveur de réception complet en Python (Flask, ~130 lignes) : OAuth (password, client credentials, refresh), HTTP Basic et tous les points d’accès. Lancez-le, placez-le derrière HTTPS et indiquez son adresse dans pycar.
Tester avec curl
# 1. Token
curl -s https://api.example.com/oauth/token \
-d grant_type=password -d username=dealer42 -d password=secret -d scope=pycar.upload
# 2. Upload a photo the way pycar does
curl -X PUT https://api.example.com/v1/vehicles/test-1/photos/ext-01 \
-H "Authorization: Bearer $TOKEN" \
-F 'metadata={"kind":"exterior","sha256":"…"};type=application/json' \
-F 'file=@photo.jpg;type=image/jpeg'