{"openapi":"3.1.0","info":{"title":"Homotope — API v1","version":"1.0.0","description":"Vectorisation raster → SVG par le calcul serveur. Authentification par clé d’API (page « Mon compte »), en-tête `Authorization: Bearer hmt_live_…`. Aucune réponse n’attend la fin d’un calcul : envoyez, sondez `GET /api/v1/jobs/{id}` jusqu’à `done`, puis téléchargez le SVG. Pas d’essai gratuit par l’API : quota Pro du mois, puis crédits ; sinon 402. Limites : 60 requêtes/min et 30 calculs/heure par clé ; 1 calcul en cours et 3 en file par clé.","license":{"name":"MIT","identifier":"MIT"}},"servers":[{"url":"/"}],"security":[{"bearer":[]}],"paths":{"/api/v1/openapi.json":{"get":{"operationId":"getOpenApi","summary":"Ce document","security":[],"responses":{"200":{"description":"Document OpenAPI 3.1","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/api/v1/me":{"get":{"operationId":"getMe","summary":"Vérifier sa clé","responses":{"200":{"description":"Clé valide","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Me"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/Unavailable"}}}},"/api/v1/jobs":{"post":{"operationId":"createJob","summary":"Envoyer une image (PNG ou JPEG)","description":"Admission immédiate (202) ou refus ; le calcul se fait ensuite dans la file. Le droit (Pro puis crédits) est vérifié AVANT la lecture du corps.","requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","description":"Image PNG ou JPEG, 80 Mo au plus (signature vérifiée)."},"options":{"type":"string","contentMediaType":"application/json","contentSchema":{"$ref":"#/components/schemas/JobOptions"},"maxLength":65536,"description":"Texte JSON (64 Ko au plus)."}}},"encoding":{"options":{"contentType":"application/json"}}}}},"responses":{"202":{"description":"Calcul admis, en file","headers":{"Location":{"description":"URL du travail","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobCreated"}}}},"400":{"description":"Envoi invalide (`invalid_options`, `missing_source`…)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"408":{"description":"Envoi trop long ou trop lent (`upload_timeout`) : délai max(60 s, taille annoncée ÷ 256 Ko/s), débit d’au moins 32 Ko/s","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"Trop lourd (`payload_too_large`), trop grand (`too_large`) ou quota de stockage atteint (`quota_exceeded`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"description":"Ni PNG ni JPEG (`unsupported_media_type`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Limite atteinte : calculs par clé (`rate_limited`), calculs en cours (`too_many_jobs`) ou envois simultanés (`too_many_uploads`)","headers":{"Retry-After":{"description":"Secondes avant de réessayer","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Calcul serveur indisponible (`storage_unavailable`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"operationId":"listJobs","summary":"Derniers travaux (du plus récent au plus ancien)","parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}},{"name":"cursor","in":"query","description":"`nextCursor` de la page précédente","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Une page","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobList"}}}},"400":{"description":"`invalid_limit` ou `invalid_cursor`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/Unavailable"}}}},"/api/v1/jobs/{id}":{"get":{"operationId":"getJob","summary":"État d’un travail (à sonder)","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Le travail","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Job"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/Unavailable"}}},"delete":{"operationId":"deleteJob","summary":"Annuler (en file : remboursé) ou supprimer le résultat","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"Fait (idempotent)"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/Unavailable"}}}},"/api/v1/jobs/{id}/svg":{"get":{"operationId":"getJobSvg","summary":"Télécharger le SVG d’un travail `done`","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Le SVG","content":{"image/svg+xml":{"schema":{"type":"string"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Pas encore prêt, ou terminé sans résultat (`not_ready`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"Résultat supprimé (`gone`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/Unavailable"}}}},"/api/v1/jobs/{id}/result":{"get":{"operationId":"getJobResult","summary":"Télécharger le résultat d’un travail `done` en SVG, PDF, DXF ou EPS","description":"Produit à la volée depuis le SVG stocké (même géométrie). PDF 1.4 et EPS 3.0 : 1 px = 1 pt ; l’EPS n’a pas de transparence : une couleur translucide y est peinte OPAQUE. DXF R12 : contours fermés, une couche par couleur (hexadécimal), en mm (1 px = 1 pt), courbes aplaties, sans le rectangle de fond (sauf `include_background=true`). Toute réponse 200 est servie en `attachment` (`format=svg` compris), contrairement à `GET …/svg`. Séparations de couleur (`layers`) : `1` → SVG à calques Inkscape, DXF à couches, ZIP d’un fichier par couleur pour PDF et EPS ; `zip` → ZIP d’un fichier par couleur (`<nom>-<hex>.<ext>`), chacun la zone VISIBLE de sa couleur (fichiers disjoints) : mode « découpé » requis (422 sinon) ; le SVG à calques suit l’ordre de peinture (un calque par série contiguë de même couleur). Conversions limitées par clé et par compte (429) ; conversion bornée à 4 Mo de SVG.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"format","in":"query","schema":{"type":"string","enum":["svg","pdf","dxf","eps"],"default":"svg"}},{"name":"tolerance","in":"query","description":"DXF seulement : écart maximal des courbes aplaties, en px d’origine.","schema":{"type":"number","minimum":0.001,"maximum":10,"default":0.1}},{"name":"include_background","in":"query","description":"DXF seulement : garder le rectangle de fond comme contour (un cadre de la taille de l’image).","schema":{"type":"boolean","default":false}},{"name":"merge_shared","in":"query","description":"DXF seulement : fusionner les frontières communes (un segment partagé par deux formes n’est écrit, donc coupé, qu’une fois).","schema":{"type":"boolean","default":false}},{"name":"layers","in":"query","description":"Séparations : une couche par couleur (`1`), ou un fichier par couleur en ZIP (`zip`).","schema":{"type":"string","enum":["0","1","zip"],"default":"0"}}],"responses":{"200":{"description":"Le fichier (`Content-Disposition: attachment`)","headers":{"Content-Disposition":{"description":"Nom du fichier (extension du format)","schema":{"type":"string"}}},"content":{"image/svg+xml":{"schema":{"type":"string"}},"application/pdf":{"schema":{"type":"string","format":"binary"}},"application/dxf":{"schema":{"type":"string"}},"application/postscript":{"schema":{"type":"string"}},"application/zip":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"`invalid_format`, `invalid_tolerance`, `invalid_include_background`, `invalid_merge_shared` ou `invalid_layers`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Pas encore prêt, ou terminé sans résultat (`not_ready`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"Résultat supprimé (`gone`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"SVG trop volumineux pour être converti, ou séparations trop lourdes (`result_too_large`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"SVG stocké non convertible (`unconvertible`), conversion trop longue ou trop lourde (`too_complex`), ou un fichier par couleur demandé sur un résultat en mode empilé (`stacked_separations` : choisir le mode « découpé »)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"description":"Calcul serveur indisponible, ou places de conversion prises (`busy`, avec `Retry-After`) : une par compte, deux par serveur","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/jobs/{id}/cutcheck":{"get":{"operationId":"getJobCutCheck","summary":"Contrôle « prêt pour la découpe » du DXF d’un travail `done`","description":"Analyse le DXF que produirait `…/result?format=dxf` (mêmes réglages) : contours ouverts, doubles découpes, chevauchements, auto-intersections, détails plus fins que l’outil, petits îlots, nœuds par contour et par couleur. Lecture seule ; positions en px de l’image (Y vers le bas), mesures en mm (1 px = 1 pt). Mêmes limites et places que les conversions (429, 503 `busy`), 4 Mo de SVG au plus. Liste bornée à 100 problèmes par type (`totals` reste exact).","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"tool_mm","in":"query","description":"Diamètre de l’outil (faisceau laser, lame), en mm.","schema":{"type":"number","exclusiveMinimum":0,"minimum":0.01,"maximum":10,"default":0.2}},{"name":"island_mm2","in":"query","description":"Aire sous laquelle une forme fermée est un petit îlot, en mm².","schema":{"type":"number","minimum":0,"maximum":1000,"default":1}},{"name":"merge_shared","in":"query","description":"Le DXF sera demandé avec `merge_shared=true` : aucune frontière commune n’y est coupée deux fois.","schema":{"type":"boolean","default":false}},{"name":"include_background","in":"query","description":"Le DXF gardera le rectangle de fond (cadre).","schema":{"type":"boolean","default":false}},{"name":"tolerance","in":"query","description":"Écart maximal des courbes aplaties du DXF, en px d’origine.","schema":{"type":"number","minimum":0.001,"maximum":10,"default":0.1}}],"responses":{"200":{"description":"Le rapport","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CutCheck"}}}},"400":{"description":"`invalid_tool_mm`, `invalid_island_mm2`, `invalid_merge_shared`, `invalid_include_background` ou `invalid_tolerance`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Pas encore prêt, ou terminé sans résultat (`not_ready`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"Résultat supprimé (`gone`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"SVG trop volumineux pour être analysé (`result_too_large`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"SVG stocké non analysable (`unconvertible`), ou analyse trop lourde (`too_complex`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"description":"Calcul serveur indisponible, ou places de conversion prises (`busy`, avec `Retry-After`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"components":{"securitySchemes":{"bearer":{"type":"http","scheme":"bearer","description":"Clé d’API `hmt_live_…`, créée dans « Mon compte ». Jamais en query string."}},"responses":{"Unauthorized":{"description":"Clé absente, inconnue ou révoquée (`invalid_api_key`)","headers":{"WWW-Authenticate":{"schema":{"type":"string","const":"Bearer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"PaymentRequired":{"description":"Aucun droit au calcul (`payment_required`) : ni quota Pro restant, ni crédit","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotFound":{"description":"Travail inconnu, ou d’un autre compte (`not_found`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Unavailable":{"description":"Service momentanément indisponible (base de données : `unavailable` ; stockage : `storage_unavailable`) : réessayer plus tard","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"RateLimited":{"description":"Limite atteinte (`rate_limited`, `too_many_jobs`, `too_many_uploads`)","headers":{"Retry-After":{"description":"Secondes avant de réessayer","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"schemas":{"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"object","required":["code","message"],"additionalProperties":true,"properties":{"code":{"type":"string"},"message":{"type":"string","description":"En français"}}}}},"Me":{"type":"object","required":["userId","keyPrefix"],"properties":{"userId":{"type":"string","format":"uuid"},"keyPrefix":{"type":"string"}}},"Links":{"type":"object","required":["self","svg"],"properties":{"self":{"type":"string"},"svg":{"type":"string"}}},"JobCreated":{"type":"object","required":["id","state","links"],"properties":{"id":{"type":"string","format":"uuid"},"state":{"const":"queued"},"links":{"$ref":"#/components/schemas/Links"}}},"Job":{"type":"object","required":["id","state","progress","createdAt","width","height","charge","links"],"properties":{"id":{"type":"string","format":"uuid"},"state":{"enum":["queued","running","done","failed","cancelled","interrupted"]},"progress":{"type":"number","minimum":0,"maximum":1},"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string"},"message":{"type":"string"}}},"createdAt":{"type":"string","format":"date-time"},"startedAt":{"type":"string","format":"date-time"},"finishedAt":{"type":"string","format":"date-time"},"width":{"type":["integer","null"]},"height":{"type":["integer","null"]},"charge":{"type":"object","required":["source","refunded"],"properties":{"source":{"enum":["pro","credit","trial",null]},"refunded":{"type":"boolean"}}},"links":{"$ref":"#/components/schemas/Links"}}},"JobList":{"type":"object","required":["jobs","nextCursor"],"properties":{"jobs":{"type":"array","items":{"$ref":"#/components/schemas/Job"}},"nextCursor":{"type":["string","null"]}}},"JobOptions":{"type":"object","additionalProperties":true,"properties":{"preset":{"enum":["logo","illustration","pixel-art","line-art","centerline","photo"],"default":"logo","description":"Préréglage de départ."},"name":{"type":["string","null"],"maxLength":200,"description":"Nom du travail."},"palette":{"type":"array","minItems":2,"maxItems":64,"uniqueItems":true,"items":{"type":"string","pattern":"^#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})$"},"description":"Palette IMPOSÉE (nuancier, encres) : chaque pixel prend la couleur la plus proche (OKLab), aucune autre n’est inventée. Couleurs distinctes (une même couleur écrite deux fois est refusée)."},"options":{"type":"object","additionalProperties":false,"description":"Surcharges partielles du préréglage, par section ; toute clé absente garde la valeur du préréglage.","properties":{"preprocess":{"type":"object","additionalProperties":false,"properties":{"denoise":{"enum":["none","bilateral","median"]},"denoiseRadius":{"type":"integer","minimum":1,"maximum":4},"denoiseStrength":{"type":"number","minimum":0,"maximum":1},"alphaMode":{"enum":["keep","flatten"]},"alphaThreshold":{"type":"integer","minimum":0,"maximum":255},"background":{"type":"array","items":{"type":"integer","minimum":0,"maximum":255},"minItems":3,"maxItems":3},"upsample":{"enum":["auto",1,2,3,4]}}},"quantize":{"type":"object","additionalProperties":false,"properties":{"colors":{"oneOf":[{"const":"auto"},{"type":"integer","minimum":2,"maximum":64}]},"maxColors":{"type":"integer","minimum":2,"maximum":64},"autoTolerance":{"type":"number","minimum":0.002,"maximum":0.1},"seed":{"type":"integer","minimum":0,"maximum":999},"constraints":{"type":"object","additionalProperties":false,"properties":{"locked":{"type":"array","items":{"type":"array","items":{"type":"integer","minimum":0,"maximum":255},"minItems":3,"maxItems":3},"maxItems":64},"merged":{"type":"array","items":{"type":"array","items":{"type":"integer","minimum":0,"maximum":63},"maxItems":64},"maxItems":64},"removed":{"type":"array","items":{"type":"integer","minimum":0,"maximum":63},"maxItems":64}}},"fixedPalette":{"type":"object","additionalProperties":false,"required":["colors"],"description":"Palette imposée : couleurs [r, v, b] distinctes ; tramage Floyd–Steinberg ; seuil de couverture (fraction des pixels) sous lequel une couleur est retirée.","properties":{"colors":{"type":"array","items":{"type":"array","items":{"type":"integer","minimum":0,"maximum":255},"minItems":3,"maxItems":3},"minItems":2,"maxItems":64,"uniqueItems":true},"dither":{"type":"boolean","default":false},"minCoverage":{"type":"number","minimum":0,"maximum":0.25,"default":0}}}}},"segment":{"type":"object","additionalProperties":false,"properties":{"minRegionArea":{"type":"number","minimum":1,"maximum":200},"blendBand":{"type":"number","minimum":0,"maximum":4},"connectivity":{"const":4}}},"fit":{"type":"object","additionalProperties":false,"properties":{"tolerance":{"type":"number","minimum":0.2,"maximum":5},"cornerAngle":{"type":"number","minimum":0,"maximum":180},"smoothing":{"type":"number","minimum":0,"maximum":1},"lineTolerance":{"type":"number","minimum":0,"maximum":3},"smoothingReach":{"type":"integer","minimum":0,"maximum":16},"robustLines":{"type":"boolean"},"pixelArt":{"type":"boolean"}}},"export":{"type":"object","additionalProperties":false,"properties":{"mode":{"enum":["cut","stacked"]},"precision":{"type":"integer","minimum":0,"maximum":4},"mergeByColor":{"type":"boolean"},"backgroundRect":{"type":"boolean"},"seamStroke":{"type":"number","minimum":0,"maximum":3}}},"centerline":{"type":"object","additionalProperties":false,"properties":{"enabled":{"type":"boolean"},"minWidth":{"type":"number","minimum":0.5,"maximum":20},"spurLength":{"type":"integer","minimum":0,"maximum":50},"tolerance":{"type":"number","minimum":0.2,"maximum":5}}}}}}},"CutBox":{"type":"object","description":"Boîte en px de l’image (origine en haut à gauche, Y vers le bas).","required":["x","y","width","height"],"properties":{"x":{"type":"number"},"y":{"type":"number"},"width":{"type":"number","minimum":0},"height":{"type":"number","minimum":0}}},"CutIssue":{"type":"object","required":["kind","severity","box","message","value"],"properties":{"kind":{"enum":["open","crossing","self-intersection","overlap","thin","island","nodes"]},"severity":{"enum":["blocking","check","info"],"description":"`blocking` : découpe fausse ; `check` : à vérifier selon la machine ; `info` : frontière commune non fusionnée (réglée par `merge_shared`), hors des comptes"},"box":{"$ref":"#/components/schemas/CutBox"},"message":{"type":"string","description":"En français"},"value":{"type":"number","description":"Largeur (mm) pour `thin`, aire (mm²) pour `island`, longueur (mm) pour `overlap` et `open`, nœuds pour `nodes`, points pour les intersections"},"color":{"type":"string","pattern":"^[0-9A-F]{6}$","description":"Couche DXF"}}},"CutReport":{"type":"object","required":["toolMm","islandMm2","maxNodes","mmPerPx","mergeShared","contours","nodes","totals","blocking","check","info","issues","truncated"],"properties":{"toolMm":{"type":"number"},"islandMm2":{"type":"number"},"maxNodes":{"type":"integer","default":1000},"mmPerPx":{"type":"number"},"mergeShared":{"type":"boolean"},"contours":{"type":"object","required":["total","closed","open","strokes"],"properties":{"total":{"type":"integer"},"closed":{"type":"integer"},"open":{"type":"integer"},"strokes":{"type":"integer","description":"Traits du centre-ligne (ouverts, normaux)"}}},"nodes":{"type":"object","required":["total","maxPerContour","byColor"],"properties":{"total":{"type":"integer"},"maxPerContour":{"type":"integer"},"byColor":{"type":"array","items":{"type":"object","required":["color","nodes","contours"],"properties":{"color":{"type":"string"},"nodes":{"type":"integer"},"contours":{"type":"integer"}}}}}},"totals":{"type":"object","required":["open","crossing","self-intersection","overlap","thin","island","nodes"],"properties":{"open":{"type":"integer"},"crossing":{"type":"integer"},"self-intersection":{"type":"integer"},"overlap":{"type":"integer"},"thin":{"type":"integer"},"island":{"type":"integer"},"nodes":{"type":"integer"}}},"blocking":{"type":"integer"},"info":{"type":"integer","description":"Informations, hors de `blocking` et `check`"},"check":{"type":"integer"},"issues":{"type":"array","items":{"$ref":"#/components/schemas/CutIssue"}},"truncated":{"type":"boolean","description":"Budget de calcul atteint : les totaux sont des minimums"}}},"CutCheck":{"type":"object","required":["id","report"],"properties":{"id":{"type":"string","format":"uuid"},"report":{"$ref":"#/components/schemas/CutReport"}}}}}}