pycar

pycar Upload API v1

API version 1 · 30 October 2026

Dokumentacja techniczna API pycar jest dostępna w języku angielskim.

pycar can send each vehicle file — photos and vehicle data — to a server you operate (DMS, website, marketplace, internal storage). This page describes the endpoints your server must provide. pycar never sends the photos anywhere else.

How a vehicle is sent

  1. The app authenticates (OAuth 2.0 token, or HTTP Basic).
  2. PUT /v1/vehicles/{vehicle_id} — the vehicle file (JSON).
  3. PUT /v1/vehicles/{vehicle_id}/photos/{photo_id} — once per photo (multipart).
  4. POST /v1/vehicles/{vehicle_id}/complete — end of the transfer, with the list of photos.

Every call is idempotent: identifiers are stable, so an interrupted transfer simply resumes, and a photo already received with the same SHA-256 is not sent again.

Settings in the app

ServerBase URL of your API, e.g. https://api.example.com/pycar. HTTPS is mandatory. The app appends /v1/… and /oauth/token.
MethodOAuth 2.0 with login / password, OAuth 2.0 with client ID / secret, or HTTP Basic.
Login, passwordStored in the phone’s secure keychain.
Client ID / secretOptional with login / password; required with client credentials.
AdvancedToken URL (default {server}/oauth/token) and scope (default pycar.upload).
ContentChoose what is sent: exterior originals, studio shots, details, hotspots, 360°, VIN photo. Optional automatic send when the vehicle file is validated.

Authentication

OAuth 2.0, login / password (grant password, RFC 6749 §4.3). If a client ID is set, it is sent with 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)

Refresh: when the token expires, pycar uses the refresh token if one was returned, otherwise it logs in again:

grant_type=refresh_token&refresh_token=8xLOxBtZp8…

HTTP Basic: every request carries Authorization: Basic base64(login:password); no token endpoint is needed.

Errors follow RFC 6749: return 400 invalid_grant or 401 invalid_client with error_description — pycar shows it to the user.

Endpoints

GET /v1/ping

Connection test (button “Test connection” in the app). Return 200 with a JSON object; the optional name field is displayed.

PUT /v1/vehicles/{vehicle_id}

Create or update the vehicle file. Return 200 or 201. vehicle_id is unique per phone and vehicle (install prefix + local folder). Fields may be empty strings; mileage_km may be 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}

Create or replace a photo. Two parts: metadata (JSON) then file (JPEG). Check the SHA-256 and return 200/201; return 422 if the checksum does not match.

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

Sent after the last photo, with every photo ID of this transfer. Use it to trigger your import (publishing, DMS sync…).

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" }

Photo types

kindContentExtra metadata fields
exteriorOriginal exterior shot (plates already masked)view
studioExterior shot placed in the vehicle’s studio backdropview, decor, source_photo_id
detailDetail photo from the checklist (rim, odometer…)slot, slot_name
hotspotDetail photo attached to a quality or defect hotspothotspot_id, hotspot_kind, zone, zone_name, note
panorama_360360° photo (equirectangular JPEG, 2:1)projection
vinPhoto of the chassis number (VIN)—

Always present: photo_id, vehicle_id, kind, sha256, size, width, height, content_type (image/jpeg); usually taken_at (ISO 8601) and plates_masked.

Photo IDs: ext-… exterior, studio-…-{decor} studio, det-… detail, hs-{hotspot_id} hotspot, pano-360, vin.

Status codes and retries

2xxSuccess.
401Token rejected: pycar requests a new token once, then reports the error.
413Too large: accept at least 25 MB per request (48 MP photos).
422Invalid content or checksum: reported to the user.
429 / 5xxRetried 3 times with exponential back-off; Retry-After (seconds) is respected.
Network errorRetried 3 times, then reported. A new send resumes where it stopped.

Security checklist

Reference server

A complete receiver in Python (Flask, ~130 lines) implementing OAuth (password, client credentials, refresh), HTTP Basic and every endpoint. Run it, put it behind HTTPS, and point pycar to it.

Download pycar_receiver.py

Test with 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'