pycar

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

  1. L’app s’authentifie (jeton OAuth 2.0, ou HTTP Basic).
  2. PUT /v1/vehicles/{vehicle_id} — la fiche du véhicule (JSON).
  3. PUT /v1/vehicles/{vehicle_id}/photos/{photo_id} — une fois par photo (multipart).
  4. 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

ServeurAdresse de base de votre API, par ex. https://api.exemple.com/pycar. HTTPS obligatoire. L’app ajoute /v1/… et /oauth/token.
MéthodeOAuth 2.0 par identifiant / mot de passe, OAuth 2.0 par client ID / secret, ou HTTP Basic.
Identifiant, mot de passeEnregistrés dans le trousseau sécurisé du téléphone.
Client ID / secretFacultatifs 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).
ContenuChoix 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.upload
HTTP/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

kindContenuChamps de métadonnées en plus
exteriorPhoto extérieure d’origine (plaques déjà masquées)view
studioPhoto extérieure dans le décor studio du véhiculeview, decor, source_photo_id
detailPhoto de détail de la liste (jante, compteur…)slot, slot_name
hotspotPhoto de détail d’un point qualité ou défauthotspot_id, hotspot_kind, zone, zone_name, note
panorama_360Photo 360° (JPEG équirectangulaire, 2:1)projection
vinPhoto 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

2xxSuccès.
401Jeton refusé : pycar redemande un jeton une fois, puis signale l’erreur.
413Trop volumineux : acceptez au moins 25 Mo par requête (photos 48 Mpx).
422Contenu ou empreinte invalide : signalé à l’utilisateur.
429 / 5xx3 essais avec délai croissant ; Retry-After (secondes) est respecté.
Erreur réseau3 essais, puis signalée. Un nouvel envoi reprend là où il s’était arrêté.

Sécurité

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.

Télécharger pycar_receiver.py

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'