{"openapi":"3.1.0","info":{"title":"PhotoHabita API","version":"1.2.0","description":"Create verified properties, upload listing photos, start processing, and poll jobs. All /api/v1 routes require a tenant API key. Dates are ISO 8601 UTC strings. Resources from another tenant return 404. API keys share a configurable per-tenant request limit in one-minute windows. Responses include X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset; 429 also includes Retry-After."},"servers":[{"url":"https://photohabita.bnsr.dev","description":"Current server"}],"tags":[{"name":"Integrations","description":"Idempotent imports and complete media processing for server integrations."},{"name":"Properties","description":"Verified property records."},{"name":"Photos","description":"Upload and review listing photos."},{"name":"Processing","description":"Asynchronous generation tasks."},{"name":"Jobs","description":"Poll job and progress status."},{"name":"Account","description":"Tenant and API key management using the browser session cookie."}],"security":[{"apiKey":[]}],"paths":{"/api/v1/services":{"get":{"tags":["Integrations"],"summary":"List the services enabled for this tenant","description":"Services are analysis, retouch, order, description and video, and are enabled by the platform administrator. Calling a disabled service returns 403. Uploads and workspaces are always available.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["tenant_id","available","enabled"],"properties":{"tenant_id":{"type":"string"},"available":{"type":"array","items":{"type":"string"}},"enabled":{"type":"array","items":{"type":"string"}}}}}}},"401":{"description":"Missing or invalid tenant key"},"402":{"description":"Processing balance unavailable"},"403":{"description":"The service is not enabled for this tenant"},"404":{"description":"Resource is absent or belongs to another tenant"},"409":{"description":"Conflicting import or unfinished output"},"422":{"description":"Invalid input"},"429":{"description":"Tenant rate limit exceeded; observe Retry-After"}}}},"/api/v1/watermark":{"get":{"tags":["Integrations"],"summary":"Read this tenant’s retouch watermark","description":"The badge drawn in the corner of AI-retouched photos shown in the web app. Images returned by this API are always unwatermarked.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["tenant_id","enabled","text","defaultText","effectiveText"],"properties":{"tenant_id":{"type":"string"},"enabled":{"type":"boolean"},"text":{"type":["string","null"],"description":"Custom text, or null for the default"},"defaultText":{"type":"string"},"effectiveText":{"type":["string","null"],"description":"What is drawn; null when disabled"}}}}}},"401":{"description":"Missing or invalid tenant key"},"402":{"description":"Processing balance unavailable"},"403":{"description":"The service is not enabled for this tenant"},"404":{"description":"Resource is absent or belongs to another tenant"},"409":{"description":"Conflicting import or unfinished output"},"422":{"description":"Invalid input"},"429":{"description":"Tenant rate limit exceeded; observe Retry-After"}}},"put":{"tags":["Integrations"],"summary":"Enable, disable or customize the watermark","description":"Send enabled and/or text. text:null restores the default text. Existing retouched photos are redrawn the next time they are opened.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","minProperties":1,"properties":{"enabled":{"type":"boolean"},"text":{"type":["string","null"],"minLength":1,"maxLength":60}}}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["tenant_id","enabled","text","defaultText","effectiveText"],"properties":{"tenant_id":{"type":"string"},"enabled":{"type":"boolean"},"text":{"type":["string","null"],"description":"Custom text, or null for the default"},"defaultText":{"type":"string"},"effectiveText":{"type":["string","null"],"description":"What is drawn; null when disabled"}}}}}},"401":{"description":"Missing or invalid tenant key"},"402":{"description":"Processing balance unavailable"},"403":{"description":"The service is not enabled for this tenant"},"404":{"description":"Resource is absent or belongs to another tenant"},"409":{"description":"Conflicting import or unfinished output"},"422":{"description":"Invalid input"},"429":{"description":"Tenant rate limit exceeded; observe Retry-After"}}}},"/api/v1/prompts":{"get":{"tags":["Integrations"],"summary":"Read this tenant’s prompts","description":"Returns all five prompts and their inherited global defaults. The tenant comes exclusively from the bearer key. Changes apply when each processing step starts; jobs already executing keep the prompt they loaded. Fixed product constraints still apply.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["tenant_id","prompts"],"properties":{"tenant_id":{"type":"string"},"prompts":{"type":"array","minItems":5,"maxItems":5,"items":{"type":"object","required":["step","content","defaultContent","override"],"properties":{"step":{"type":"string","enum":["qa","edit","order","description","video"]},"content":{"type":"string"},"defaultContent":{"type":"string"},"override":{"type":["string","null"]}}}}}}}}},"401":{"description":"Missing or invalid tenant key"},"402":{"description":"Processing balance unavailable"},"403":{"description":"The service is not enabled for this tenant"},"404":{"description":"Resource is absent or belongs to another tenant"},"409":{"description":"Conflicting import or unfinished output"},"422":{"description":"Invalid input"},"429":{"description":"Tenant rate limit exceeded; observe Retry-After"}}}},"/api/v1/prompts/{step}":{"put":{"tags":["Integrations"],"summary":"Set or reset a tenant prompt","description":"A non-empty content string overrides this step for the authenticated tenant. Send content:null to remove the override and inherit the current global default. No other tenant is modified.","parameters":[{"name":"step","in":"path","required":true,"schema":{"type":"string","enum":["qa","edit","order","description","video"]}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["content"],"properties":{"content":{"type":["string","null"],"minLength":1,"maxLength":20000}}}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["tenant_id","prompts"],"properties":{"tenant_id":{"type":"string"},"prompts":{"type":"array","minItems":5,"maxItems":5,"items":{"type":"object","required":["step","content","defaultContent","override"],"properties":{"step":{"type":"string","enum":["qa","edit","order","description","video"]},"content":{"type":"string"},"defaultContent":{"type":"string"},"override":{"type":["string","null"]}}}}}}}}},"401":{"description":"Missing or invalid tenant key"},"402":{"description":"Processing balance unavailable"},"403":{"description":"The service is not enabled for this tenant"},"404":{"description":"Resource is absent or belongs to another tenant"},"409":{"description":"Conflicting import or unfinished output"},"422":{"description":"Invalid input"},"429":{"description":"Tenant rate limit exceeded; observe Retry-After"}}}},"/api/v1/workspaces/{reference}":{"put":{"tags":["Integrations"],"summary":"Import a processing workspace","description":"Idempotent per tenant and reference. Accepts an existing property address with optional coordinates; when coordinates are omitted Google Places resolves the address. Workspaces have the same IDs and tenant isolation as properties. Use an independent workspace for each image or listing generation attempt.","parameters":[{"name":"reference","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":200}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["address"],"properties":{"address":{"type":"string","maxLength":500},"city":{"type":"string"},"latitude":{"type":"number","minimum":-90,"maximum":90},"longitude":{"type":"number","minimum":-180,"maximum":180}}}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["id"],"properties":{"id":{"type":"string"}}}}}},"201":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["id"],"properties":{"id":{"type":"string"}}}}}},"401":{"description":"Missing or invalid tenant key"},"402":{"description":"Processing balance unavailable"},"403":{"description":"The service is not enabled for this tenant"},"404":{"description":"Resource is absent or belongs to another tenant"},"409":{"description":"Conflicting import or unfinished output"},"422":{"description":"Invalid input"},"429":{"description":"Tenant rate limit exceeded; observe Retry-After"}}}},"/api/v1/workspaces/{id}/photos/{reference}":{"put":{"tags":["Integrations"],"summary":"Import an image idempotently","description":"Multipart photo plus mode (original or final), optional room label and identifier. Original images require analysis. Final images are trusted outputs from the caller’s previous processing stage and are ready for ordering and video. Reusing a reference with different bytes, mode or label returns 409. The identifier is returned as integration_identifier by property reads.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"reference","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":200}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["photo","mode"],"properties":{"photo":{"type":"string","format":"binary"},"mode":{"enum":["original","final"]},"room":{"type":"string"},"identifier":{"type":"string","maxLength":4000}}}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["id"],"properties":{"id":{"type":"string"}}}}}},"201":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["id"],"properties":{"id":{"type":"string"}}}}}},"401":{"description":"Missing or invalid tenant key"},"402":{"description":"Processing balance unavailable"},"403":{"description":"The service is not enabled for this tenant"},"404":{"description":"Resource is absent or belongs to another tenant"},"409":{"description":"Conflicting import or unfinished output"},"422":{"description":"Invalid input"},"429":{"description":"Tenant rate limit exceeded; observe Retry-After"}}}},"/api/v1/photos/{id}":{"get":{"tags":["Integrations"],"summary":"Read photo processing state","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["id","status","decision","reason","room","needsRetouch","ready"],"properties":{"id":{"type":"string"},"status":{"type":"string"},"decision":{"enum":["OK","KO","PENDIENTE"]},"reason":{"type":["string","null"]},"room":{"type":["string","null"]},"needsRetouch":{"type":"boolean"},"ready":{"type":"boolean"}}}}}},"401":{"description":"Missing or invalid tenant key"},"402":{"description":"Processing balance unavailable"},"403":{"description":"The service is not enabled for this tenant"},"404":{"description":"Resource is absent or belongs to another tenant"},"409":{"description":"Conflicting import or unfinished output"},"422":{"description":"Invalid input"},"429":{"description":"Tenant rate limit exceeded; observe Retry-After"}}}},"/api/v1/photos/{id}/analyze":{"post":{"tags":["Integrations"],"summary":"Analyze without automatically retouching","description":"Returns existing analysis or queues an asynchronous analysis job. Poll the job and then read the photo decision and room. Pending analysis never implies acceptance.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","maxLength":200},"description":"Reusing the key on the same resource and operation returns the same job, including after completion or failure. To explicitly retry a failed operation use a new workspace or key."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["id"],"properties":{"id":{"type":"string"}}}}}},"202":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueuedJob"}}}},"401":{"description":"Missing or invalid tenant key"},"402":{"description":"Processing balance unavailable"},"403":{"description":"The service is not enabled for this tenant"},"404":{"description":"Resource is absent or belongs to another tenant"},"409":{"description":"Conflicting import or unfinished output"},"422":{"description":"Invalid input"},"429":{"description":"Tenant rate limit exceeded; observe Retry-After"}}}},"/api/v1/photos/{id}/retouch":{"post":{"tags":["Integrations"],"summary":"Prepare final photo","description":"Requires an OK analysis. Queues an edit if needed; otherwise completes with the accepted original. A failed edit leaves the image unready. API jobs run through durable alarms rather than the HTTP request lifetime.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","maxLength":200},"description":"Reusing the key on the same resource and operation returns the same job, including after completion or failure. To explicitly retry a failed operation use a new workspace or key."}],"responses":{"202":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueuedJob"}}}},"401":{"description":"Missing or invalid tenant key"},"402":{"description":"Processing balance unavailable"},"403":{"description":"The service is not enabled for this tenant"},"404":{"description":"Resource is absent or belongs to another tenant"},"409":{"description":"Conflicting import or unfinished output"},"422":{"description":"Invalid input"},"429":{"description":"Tenant rate limit exceeded; observe Retry-After"}}}},"/api/v1/photos/{id}/image":{"get":{"tags":["Integrations"],"summary":"Download final image","description":"Bearer authentication is required. Edited images use the clean retouched output; accepted originals are returned only when no edit is pending. Unready images return 409.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Tenant-authenticated final media. No public or session cookie URL is required.","content":{"image/jpeg":{"schema":{"type":"string","format":"binary"}}}},"401":{"description":"Missing or invalid tenant key"},"402":{"description":"Processing balance unavailable"},"403":{"description":"The service is not enabled for this tenant"},"404":{"description":"Resource is absent or belongs to another tenant"},"409":{"description":"Conflicting import or unfinished output"},"422":{"description":"Invalid input"},"429":{"description":"Tenant rate limit exceeded; observe Retry-After"}}}},"/api/v1/properties/{id}/features":{"put":{"tags":["Integrations"],"summary":"Set verified description facts","description":"Stores caller-supplied facts for description generation. Once set, different facts require a new workspace.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","maxProperties":100,"additionalProperties":true}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"}}}}}},"401":{"description":"Missing or invalid tenant key"},"402":{"description":"Processing balance unavailable"},"403":{"description":"The service is not enabled for this tenant"},"404":{"description":"Resource is absent or belongs to another tenant"},"409":{"description":"Conflicting import or unfinished output"},"422":{"description":"Invalid input"},"429":{"description":"Tenant rate limit exceeded; observe Retry-After"}}}},"/api/v1/properties/{id}/video":{"post":{"tags":["Integrations"],"summary":"Start video from final photos","description":"Requires five final photos, processing balance and configured video providers. Idempotent per workspace: repeats return the existing video state. Poll /api/v1/properties/{id}/video/status. Failed work needs a new workspace attempt.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"202":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["id"],"properties":{"id":{"type":"string"}}}}}},"401":{"description":"Missing or invalid tenant key"},"402":{"description":"Processing balance unavailable"},"403":{"description":"The service is not enabled for this tenant"},"404":{"description":"Resource is absent or belongs to another tenant"},"409":{"description":"Conflicting import or unfinished output"},"422":{"description":"Invalid input"},"429":{"description":"Tenant rate limit exceeded; observe Retry-After"}}},"get":{"tags":["Integrations"],"summary":"Download finished video","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Tenant-authenticated final media. No public or session cookie URL is required.","content":{"video/mp4":{"schema":{"type":"string","format":"binary"}}}},"401":{"description":"Missing or invalid tenant key"},"402":{"description":"Processing balance unavailable"},"403":{"description":"The service is not enabled for this tenant"},"404":{"description":"Resource is absent or belongs to another tenant"},"409":{"description":"Conflicting import or unfinished output"},"422":{"description":"Invalid input"},"429":{"description":"Tenant rate limit exceeded; observe Retry-After"}}}},"/api/v1/properties":{"get":{"tags":["Properties"],"summary":"List properties","operationId":"listProperties","description":"Returns the 100 newest active properties in this tenant, newest first. Integration workspaces are excluded; retrieve those by ID or import reference. This version has no pagination.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["properties"],"properties":{"properties":{"type":"array","items":{"$ref":"#/components/schemas/Property"}}}},"example":{"properties":[{"id":"9af3cdb6-2bb6-421d-8b0a-96a57bc84e61","address":"Calle Mayor 1, Madrid, España","place_id":"ChIJ_example123","latitude":40.4168,"longitude":-3.7038,"city":"Madrid","neighborhood":"Centro","country":"España","description":null,"created_at":"2026-09-19T12:00:00.000Z","updated_at":"2026-09-19T12:00:00.000Z"}]}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}},"429":{"description":"Per-tenant API request limit exceeded. Retry-After is the number of seconds until the next one-minute window.","headers":{"Retry-After":{"schema":{"type":"integer","minimum":1}},"X-RateLimit-Limit":{"schema":{"type":"integer"}},"X-RateLimit-Remaining":{"schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp in seconds when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}}}},"post":{"tags":["Properties"],"summary":"Create property","operationId":"createProperty","description":"Looks up the Google Place ID and stores its verified address and coordinates.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["placeId"],"properties":{"placeId":{"type":"string","pattern":"^[-\\w]+$","description":"Google Place ID. The service verifies its address and coordinates."}}},"example":{"placeId":"ChIJ_example123"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PropertyMutation"},"example":{"id":"9af3cdb6-2bb6-421d-8b0a-96a57bc84e61","address":"Calle Mayor 1, Madrid, España"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}},"429":{"description":"Per-tenant API request limit exceeded. Retry-After is the number of seconds until the next one-minute window.","headers":{"Retry-After":{"schema":{"type":"integer","minimum":1}},"X-RateLimit-Limit":{"schema":{"type":"integer"}},"X-RateLimit-Remaining":{"schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp in seconds when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}}}}},"/api/v1/properties/{id}":{"get":{"tags":["Properties"],"summary":"Get property and photos","operationId":"getProperty","parameters":[{"name":"id","in":"path","required":true,"description":"Property UUID.","schema":{"type":"string","format":"uuid"}}],"description":"Returns property fields and active photo metadata. tags_json is a JSON encoded string. Image files are not included.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Property"},{"type":"object","required":["photos"],"properties":{"photos":{"type":"array","items":{"$ref":"#/components/schemas/Photo"}}}}]},"example":{"id":"9af3cdb6-2bb6-421d-8b0a-96a57bc84e61","address":"Calle Mayor 1, Madrid, España","place_id":"ChIJ_example123","latitude":40.4168,"longitude":-3.7038,"city":"Madrid","neighborhood":"Centro","country":"España","description":null,"created_at":"2026-09-19T12:00:00.000Z","updated_at":"2026-09-19T12:00:00.000Z","photos":[{"id":"364ae7fa-2bf8-470f-a256-e749e10c6821","filename":"salon.jpg","status":"approved","qa_decision":"OK","qa_reason":"Foto válida.","room":"Salón","tags_json":"[\"luminoso\",\"ventana\"]","position":0,"created_at":"2026-09-19T12:01:00.000Z","updated_at":"2026-09-19T12:02:00.000Z"}]}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}},"404":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}},"429":{"description":"Per-tenant API request limit exceeded. Retry-After is the number of seconds until the next one-minute window.","headers":{"Retry-After":{"schema":{"type":"integer","minimum":1}},"X-RateLimit-Limit":{"schema":{"type":"integer"}},"X-RateLimit-Remaining":{"schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp in seconds when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}}}},"patch":{"tags":["Properties"],"summary":"Update property address","operationId":"updateProperty","parameters":[{"name":"id","in":"path","required":true,"description":"Property UUID.","schema":{"type":"string","format":"uuid"}}],"description":"Replaces address and location fields using a verified Google Place ID.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["placeId"],"properties":{"placeId":{"type":"string","pattern":"^[-\\w]+$","description":"Google Place ID. The service verifies its address and coordinates."}}},"example":{"placeId":"ChIJ_example123"}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PropertyMutation"},"example":{"id":"9af3cdb6-2bb6-421d-8b0a-96a57bc84e61","address":"Gran Vía 10, Madrid, España"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}},"404":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}},"429":{"description":"Per-tenant API request limit exceeded. Retry-After is the number of seconds until the next one-minute window.","headers":{"Retry-After":{"schema":{"type":"integer","minimum":1}},"X-RateLimit-Limit":{"schema":{"type":"integer"}},"X-RateLimit-Remaining":{"schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp in seconds when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}}}}},"/api/v1/properties/{id}/photos":{"post":{"tags":["Photos"],"summary":"Upload photo","operationId":"uploadPhoto","parameters":[{"name":"id","in":"path","required":true,"description":"Property UUID.","schema":{"type":"string","format":"uuid"}}],"description":"Upload a JPEG, PNG, or WebP up to 20 MB. A property can have at most 20 active photos. Uploading does not start QA.","requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["photo"],"properties":{"photo":{"type":"string","format":"binary","description":"Image file, up to 20 MB."}}}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"type":"object","required":["id"],"properties":{"id":{"type":"string","format":"uuid"}}},"example":{"id":"364ae7fa-2bf8-470f-a256-e749e10c6821"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}},"404":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}},"429":{"description":"Per-tenant API request limit exceeded. Retry-After is the number of seconds until the next one-minute window.","headers":{"Retry-After":{"schema":{"type":"integer","minimum":1}},"X-RateLimit-Limit":{"schema":{"type":"integer"}},"X-RateLimit-Remaining":{"schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp in seconds when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}}}}},"/api/v1/photos/{id}/qa":{"post":{"tags":["Photos"],"summary":"Review and optionally retouch photo","operationId":"reviewPhoto","parameters":[{"name":"id","in":"path","required":true,"description":"Photo UUID.","schema":{"type":"string","format":"uuid"}}],"description":"Queues quality review. If the photo is suitable and needs retouching, the same job performs the automatic edit. The returned ID is a job ID, not a photo ID. Poll statusUrl, then fetch the property to read the photo result.","responses":{"202":{"description":"Queued","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueuedJob"},"example":{"id":"932c956b-4472-4dfa-bfcb-0702e1b73122","status":"queued","statusUrl":"/api/v1/jobs/932c956b-4472-4dfa-bfcb-0702e1b73122"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}},"404":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}},"429":{"description":"Per-tenant API request limit exceeded. Retry-After is the number of seconds until the next one-minute window.","headers":{"Retry-After":{"schema":{"type":"integer","minimum":1}},"X-RateLimit-Limit":{"schema":{"type":"integer"}},"X-RateLimit-Remaining":{"schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp in seconds when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}}}}},"/api/v1/properties/{id}/order":{"post":{"tags":["Processing"],"summary":"Order final photos","operationId":"orderPhotos","parameters":[{"name":"id","in":"path","required":true,"description":"Property UUID.","schema":{"type":"string","format":"uuid"}}],"description":"Queues photo ordering. The job fails unless at least five suitable final photos exist. Once complete, each photo position is available from GET /api/v1/properties/{id}.","responses":{"202":{"description":"Queued","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueuedJob"},"example":{"id":"932c956b-4472-4dfa-bfcb-0702e1b73122","status":"queued","statusUrl":"/api/v1/jobs/932c956b-4472-4dfa-bfcb-0702e1b73122"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}},"404":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}},"429":{"description":"Per-tenant API request limit exceeded. Retry-After is the number of seconds until the next one-minute window.","headers":{"Retry-After":{"schema":{"type":"integer","minimum":1}},"X-RateLimit-Limit":{"schema":{"type":"integer"}},"X-RateLimit-Remaining":{"schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp in seconds when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}}}}},"/api/v1/properties/{id}/description":{"post":{"tags":["Processing"],"summary":"Generate property description","operationId":"generateDescription","parameters":[{"name":"id","in":"path","required":true,"description":"Property UUID.","schema":{"type":"string","format":"uuid"}}],"description":"Queues description generation from at least five suitable final photos. Once complete, description is available from GET /api/v1/properties/{id}.","responses":{"202":{"description":"Queued","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueuedJob"},"example":{"id":"932c956b-4472-4dfa-bfcb-0702e1b73122","status":"queued","statusUrl":"/api/v1/jobs/932c956b-4472-4dfa-bfcb-0702e1b73122"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}},"404":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}},"429":{"description":"Per-tenant API request limit exceeded. Retry-After is the number of seconds until the next one-minute window.","headers":{"Retry-After":{"schema":{"type":"integer","minimum":1}},"X-RateLimit-Limit":{"schema":{"type":"integer"}},"X-RateLimit-Remaining":{"schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp in seconds when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}}}}},"/api/v1/jobs/{id}":{"get":{"tags":["Jobs"],"summary":"Get job status","operationId":"getJob","parameters":[{"name":"id","in":"path","required":true,"description":"Job UUID.","schema":{"type":"string","format":"uuid"}}],"description":"Poll every few seconds until status is complete or failed. error is populated when the job fails. photo_id is null for property-wide jobs.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Job"},"example":{"id":"932c956b-4472-4dfa-bfcb-0702e1b73122","property_id":"9af3cdb6-2bb6-421d-8b0a-96a57bc84e61","photo_id":"364ae7fa-2bf8-470f-a256-e749e10c6821","step":"qa","status":"complete","error":null,"created_at":"2026-09-19T12:01:00.000Z","updated_at":"2026-09-19T12:02:00.000Z"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}},"404":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}},"429":{"description":"Per-tenant API request limit exceeded. Retry-After is the number of seconds until the next one-minute window.","headers":{"Retry-After":{"schema":{"type":"integer","minimum":1}},"X-RateLimit-Limit":{"schema":{"type":"integer"}},"X-RateLimit-Remaining":{"schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp in seconds when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}}}}},"/api/v1/properties/{id}/jobs":{"get":{"tags":["Jobs"],"summary":"List property jobs","operationId":"listPropertyJobs","parameters":[{"name":"id","in":"path","required":true,"description":"Property UUID.","schema":{"type":"string","format":"uuid"}}],"description":"Returns up to 100 most recent jobs for this property. No pagination in this version.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["jobs"],"properties":{"jobs":{"type":"array","items":{"$ref":"#/components/schemas/PropertyJob"}}}},"example":{"jobs":[{"id":"932c956b-4472-4dfa-bfcb-0702e1b73122","photo_id":"364ae7fa-2bf8-470f-a256-e749e10c6821","step":"qa","status":"complete","error":null,"created_at":"2026-09-19T12:01:00.000Z","updated_at":"2026-09-19T12:02:00.000Z"}]}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}},"404":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}},"429":{"description":"Per-tenant API request limit exceeded. Retry-After is the number of seconds until the next one-minute window.","headers":{"Retry-After":{"schema":{"type":"integer","minimum":1}},"X-RateLimit-Limit":{"schema":{"type":"integer"}},"X-RateLimit-Remaining":{"schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp in seconds when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}}}}},"/api/v1/properties/{id}/progress":{"get":{"tags":["Jobs"],"summary":"Get progress events","operationId":"getPropertyProgress","parameters":[{"name":"id","in":"path","required":true,"description":"Property UUID.","schema":{"type":"string","format":"uuid"}}],"description":"Returns up to 100 recent events. seq increases within each property. photoId and message are optional.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ProgressEvent"}},"example":[{"step":"qa","status":"complete","photoId":"364ae7fa-2bf8-470f-a256-e749e10c6821","message":"Foto válida.","at":"2026-09-19T12:02:00.000Z","seq":4}]}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}},"404":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}},"429":{"description":"Per-tenant API request limit exceeded. Retry-After is the number of seconds until the next one-minute window.","headers":{"Retry-After":{"schema":{"type":"integer","minimum":1}},"X-RateLimit-Limit":{"schema":{"type":"integer"}},"X-RateLimit-Remaining":{"schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp in seconds when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}}}}},"/api/v1/properties/{id}/video/status":{"get":{"tags":["Jobs"],"summary":"Get video status","operationId":"getVideoStatus","parameters":[{"name":"id","in":"path","required":true,"description":"Property UUID.","schema":{"type":"string","format":"uuid"}}],"description":"Reads video work started in the web app. Video generation can be started through POST /api/v1/properties/{id}/video. job is null if no video was started.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["job"],"properties":{"job":{"anyOf":[{"$ref":"#/components/schemas/VideoJob"},{"type":"null"}]}}},"example":{"job":{"status":"running","startedAt":"2026-09-19T12:05:00.000Z","format":"horizontal","photoIds":["364ae7fa-2bf8-470f-a256-e749e10c6821"]}}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}},"404":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}},"429":{"description":"Per-tenant API request limit exceeded. Retry-After is the number of seconds until the next one-minute window.","headers":{"Retry-After":{"schema":{"type":"integer","minimum":1}},"X-RateLimit-Limit":{"schema":{"type":"integer"}},"X-RateLimit-Remaining":{"schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp in seconds when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}}}}},"/api/billing":{"get":{"tags":["Account"],"summary":"Get billing account","operationId":"getBillingAccount","security":[{"session":[]}],"description":"Returns the plan, credit balance in USD microdollars (1,000,000 = $1), subscription status, and 50 recent ledger entries.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["mode","currency","creditBalanceMicros","subscriptionStatus","stripeConfigured","entries"],"properties":{"mode":{"type":"string","enum":["legacy","credits","metered"]},"currency":{"type":"string","const":"usd"},"creditBalanceMicros":{"type":"integer"},"subscriptionStatus":{"type":["string","null"]},"stripeConfigured":{"type":"boolean"},"entries":{"type":"array","items":{"type":"object"}}}},"example":{"mode":"credits","currency":"usd","creditBalanceMicros":25000000,"subscriptionStatus":null,"stripeConfigured":true,"entries":[]}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}}}}},"/api/billing/topup":{"post":{"tags":["Account"],"summary":"Start credit purchase","operationId":"startCreditPurchase","security":[{"session":[]}],"description":"Creates a Stripe Checkout payment session. Credits appear only after a verified paid webhook. Available only to credit tenants.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["amountCents"],"properties":{"amountCents":{"type":"integer","minimum":500,"maximum":1000000,"description":"Credit amount in USD cents, $5–$10,000."}}},"example":{"amountCents":2500}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri"}}},"example":{"url":"https://checkout.stripe.com/..."}}}},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}},"503":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}}}}},"/api/billing/subscribe":{"post":{"tags":["Account"],"summary":"Start pay-as-you-go subscription","operationId":"startPayAsYouGo","security":[{"session":[]}],"description":"Creates a Stripe Checkout subscription session for the metered price. Requires zero credit balance. The subscription becomes active through Stripe webhooks.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri"}}},"example":{"url":"https://checkout.stripe.com/..."}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}},"503":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}}}}},"/api/billing/portal":{"post":{"tags":["Account"],"summary":"Open Stripe billing portal","operationId":"openBillingPortal","security":[{"session":[]}],"description":"Creates a Stripe-hosted session for payment methods and invoices.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri"}}},"example":{"url":"https://billing.stripe.com/..."}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}},"404":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}}}}},"/api/tenant":{"get":{"tags":["Account"],"summary":"Get current tenant","operationId":"getTenant","security":[{"session":[]}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Tenant"},"example":{"id":"f5041d10-bc6d-4eb1-9532-a59949853cf2","name":"Agencia Centro","created_at":"2026-09-19T11:00:00.000Z"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}}}},"patch":{"tags":["Account"],"summary":"Rename current tenant","operationId":"renameTenant","security":[{"session":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":100}}},"example":{"name":"Agencia Centro"}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["id","name"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"}}},"example":{"id":"f5041d10-bc6d-4eb1-9532-a59949853cf2","name":"Agencia Centro"}}}},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}}}}},"/api/keys":{"get":{"tags":["Account"],"summary":"List API keys","operationId":"listApiKeys","security":[{"session":[]}],"description":"Includes revoked keys. Never returns the full token.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ApiKey"}},"example":[{"id":"14e713af-2b0c-4407-b5ed-9917a76f49a5","name":"Production","prefix":"pil_ab12cd34","created_at":"2026-09-19T11:10:00.000Z","last_used_at":null,"revoked_at":null}]}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}},"429":{"description":"Per-tenant API request limit exceeded. Retry-After is the number of seconds until the next one-minute window.","headers":{"Retry-After":{"schema":{"type":"integer","minimum":1}},"X-RateLimit-Limit":{"schema":{"type":"integer"}},"X-RateLimit-Remaining":{"schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp in seconds when the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}}}},"post":{"tags":["Account"],"summary":"Create API key","operationId":"createApiKey","security":[{"session":[]}],"description":"The full token is returned once, at creation. Store it on your server.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":100}}},"example":{"name":"Production"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"type":"object","required":["id","name","prefix","token","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"prefix":{"type":"string"},"token":{"type":"string","description":"pil_ followed by 64 hexadecimal characters."},"created_at":{"type":"string","format":"date-time"}}},"example":{"id":"14e713af-2b0c-4407-b5ed-9917a76f49a5","name":"Production","prefix":"pil_ab12cd34","token":"pil_ab12cd34...","created_at":"2026-09-19T11:10:00.000Z"}}}},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}}}}},"/api/keys/{id}":{"delete":{"tags":["Account"],"summary":"Revoke API key","operationId":"revokeApiKey","security":[{"session":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"API key UUID.","schema":{"type":"string","format":"uuid"}}],"description":"Revocation takes effect immediately. Revoking an already revoked key returns 404.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean"}}},"example":{"ok":true}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}},"404":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}}}}}},"components":{"securitySchemes":{"apiKey":{"type":"http","scheme":"bearer","bearerFormat":"API key","description":"Tenant API key beginning with pil_. Create one at /developers."},"session":{"type":"apiKey","in":"cookie","name":"session","description":"Browser session cookie; used only for tenant and API key management."}},"schemas":{"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"string"}}},"Property":{"type":"object","required":["id","address","place_id","latitude","longitude","city","neighborhood","country","description","created_at","updated_at"],"properties":{"id":{"type":"string","format":"uuid"},"address":{"type":"string"},"place_id":{"type":"string"},"latitude":{"type":"number"},"longitude":{"type":"number"},"city":{"type":["string","null"]},"neighborhood":{"type":["string","null"]},"country":{"type":["string","null"]},"description":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"PropertyMutation":{"type":"object","required":["id","address"],"properties":{"id":{"type":"string","format":"uuid"},"address":{"type":"string"}}},"Photo":{"type":"object","required":["id","filename","status","qa_decision","qa_reason","room","tags_json","position","created_at","updated_at"],"properties":{"id":{"type":"string","format":"uuid"},"filename":{"type":"string"},"status":{"type":"string","description":"Current photo state, such as uploaded, approved, rejected, or edited."},"qa_decision":{"type":["string","null"],"description":"OK, KO, or a pending value."},"qa_reason":{"type":["string","null"]},"room":{"type":["string","null"]},"tags_json":{"type":"string","description":"JSON encoded array of tags."},"position":{"type":["integer","null"]},"integration_identifier":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"QueuedJob":{"type":"object","required":["id","status","statusUrl"],"properties":{"id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["queued","running","complete","failed"]},"statusUrl":{"type":"string","description":"Relative URL to poll with the same bearer key."}}},"Job":{"type":"object","required":["id","property_id","photo_id","step","status","error","created_at","updated_at"],"properties":{"id":{"type":"string","format":"uuid"},"property_id":{"type":"string","format":"uuid"},"photo_id":{"type":["string","null"],"format":"uuid"},"step":{"type":"string","enum":["qa","analyze","edit","order","description"]},"status":{"type":"string","enum":["queued","running","complete","failed"]},"error":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"PropertyJob":{"type":"object","required":["id","photo_id","step","status","error","created_at","updated_at"],"properties":{"id":{"type":"string","format":"uuid"},"photo_id":{"type":["string","null"],"format":"uuid"},"step":{"type":"string","enum":["qa","analyze","edit","order","description"]},"status":{"type":"string","enum":["queued","running","complete","failed"]},"error":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"ProgressEvent":{"type":"object","required":["step","status","at"],"properties":{"step":{"type":"string"},"status":{"type":"string"},"photoId":{"type":"string","format":"uuid"},"message":{"type":"string"},"at":{"type":"string","format":"date-time"},"seq":{"type":"integer"}}},"VideoJob":{"type":"object","required":["status","startedAt","format","photoIds"],"properties":{"status":{"type":"string","enum":["running","complete","failed"]},"startedAt":{"type":"string","format":"date-time"},"format":{"type":"string","enum":["horizontal","vertical","square"]},"photoIds":{"type":"array","items":{"type":"string","format":"uuid"}}}},"Tenant":{"type":"object","required":["id","name","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"created_at":{"type":"string","format":"date-time"}}},"ApiKey":{"type":"object","required":["id","name","prefix","created_at","last_used_at","revoked_at"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"prefix":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"last_used_at":{"type":["string","null"],"format":"date-time"},"revoked_at":{"type":["string","null"],"format":"date-time"}}}}}}