{"openapi":"3.1.0","jsonSchemaDialect":"https://json-schema.org/draft/2020-12/schema","info":{"title":"Dynjandi management API","version":"0.4.1","description":"The management API of the Dynjandi dashboard, as a workspace's API key reaches it. Every route here accepts `Authorization: Bearer dyn_...`; a key is created from the API keys page of the dashboard and acts as a member of exactly one workspace.\n\n- Every response, errors included, is `Cache-Control: no-store`. Errors use `{ \"error\": { \"code\", \"message\", \"details?\" } }`.\n- A key is limited to 120 requests per 60 seconds; past that the answer is `429 RATE_LIMITED` with `Retry-After`.\n- A request that names a route the key may not reach, such as `/api/users` or `/api/billing`, is `404`.\n- `HEAD` is accepted wherever `GET` is and answers the same headers with no body; it is left out of this document, as is usual.\n- There is no CORS: keys are for servers, not browsers."},"servers":[{"url":"https://app.dynjandi.dev"}],"security":[{"bearerAuth":[]}],"paths":{"/api/workspace":{"get":{"operationId":"getWorkspace","summary":"Read the key's workspace","description":"The workspace the key is bound to: its plan, status, billing status, caps, billing period and usage for the period (the variant count is per UTC month on a yearly plan). It has no delivery bandwidth: delivery is not metered per workspace. It answers with the same body as `GET /api/workspace/usage`.","responses":{"200":{"description":"The workspace's plan, caps and usage.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"status":{"type":"string"},"plan":{"type":"string"},"billingStatus":{"type":"string","enum":["pending","active","past_due","lapsed"]},"cancelAtPeriodEnd":{"type":"boolean"},"periodStart":{"type":["string","null"]},"periodEnd":{"type":["string","null"]},"caps":{"type":"object","properties":{"storageBytes":{"anyOf":[{"type":"integer","minimum":0,"maximum":9007199254740991},{"type":"null"}]},"variants":{"anyOf":[{"type":"integer","minimum":0,"maximum":9007199254740991},{"type":"null"}]},"projects":{"anyOf":[{"type":"integer","minimum":0,"maximum":9007199254740991},{"type":"null"}]}},"required":["storageBytes","variants","projects"],"additionalProperties":false},"usage":{"type":"object","properties":{"storageBytes":{"type":"integer","minimum":0,"maximum":9007199254740991},"newVariants":{"type":"integer","minimum":0,"maximum":9007199254740991},"projects":{"type":"integer","minimum":0,"maximum":9007199254740991}},"required":["storageBytes","newVariants","projects"],"additionalProperties":false}},"required":["id","name","status","plan","billingStatus","cancelAtPeriodEnd","periodStart","periodEnd","caps","usage"],"additionalProperties":false}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/workspace/usage":{"get":{"operationId":"getWorkspaceUsage","summary":"Read the workspace's plan, caps and usage","description":"The plan, status, billing status, caps, billing period and usage for the period (the variant count is per UTC month on a yearly plan). It has no delivery bandwidth: delivery is not metered per workspace.","responses":{"200":{"description":"The workspace's plan, caps and usage.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"status":{"type":"string"},"plan":{"type":"string"},"billingStatus":{"type":"string","enum":["pending","active","past_due","lapsed"]},"cancelAtPeriodEnd":{"type":"boolean"},"periodStart":{"type":["string","null"]},"periodEnd":{"type":["string","null"]},"caps":{"type":"object","properties":{"storageBytes":{"anyOf":[{"type":"integer","minimum":0,"maximum":9007199254740991},{"type":"null"}]},"variants":{"anyOf":[{"type":"integer","minimum":0,"maximum":9007199254740991},{"type":"null"}]},"projects":{"anyOf":[{"type":"integer","minimum":0,"maximum":9007199254740991},{"type":"null"}]}},"required":["storageBytes","variants","projects"],"additionalProperties":false},"usage":{"type":"object","properties":{"storageBytes":{"type":"integer","minimum":0,"maximum":9007199254740991},"newVariants":{"type":"integer","minimum":0,"maximum":9007199254740991},"projects":{"type":"integer","minimum":0,"maximum":9007199254740991}},"required":["storageBytes","newVariants","projects"],"additionalProperties":false}},"required":["id","name","status","plan","billingStatus","cancelAtPeriodEnd","periodStart","periodEnd","caps","usage"],"additionalProperties":false}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/projects":{"get":{"operationId":"listProjects","summary":"List the workspace's projects","responses":{"200":{"description":"The projects.","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"storageQuota":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"originPullEnabled":{"type":"boolean"},"id":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{0,62}$"},"tenantId":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{0,62}$"},"publicKey":{"type":"string"},"customHostname":{"type":["string","null"]},"maxFileSize":{"type":"number"},"createdAt":{"type":"string"}},"required":["name","storageQuota","originPullEnabled","id","tenantId","publicKey","customHostname","maxFileSize","createdAt"],"additionalProperties":false}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"operationId":"createProject","summary":"Create a project","description":"A key creates a project in its own workspace. `storageQuota` and `originPullEnabled` are a superadmin's to set, so a key that sends either gets `403 FORBIDDEN`; omit them. The response carries the project's secret key once, and it appears nowhere else. A workspace whose plan is not active gets `403 BILLING_INACTIVE`, and one at its project cap gets `403 PROJECT_LIMIT_REACHED` with `details.maxProjects`.","requestBody":{"required":true,"description":"The new project.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"storageQuota":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"originPullEnabled":{"type":"boolean"},"tenantId":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{0,62}$"}},"required":["name","tenantId"],"additionalProperties":false}}}},"responses":{"201":{"description":"The project, and its secret key.","content":{"application/json":{"schema":{"type":"object","properties":{"project":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"storageQuota":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"originPullEnabled":{"type":"boolean"},"id":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{0,62}$"},"tenantId":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{0,62}$"},"publicKey":{"type":"string"},"customHostname":{"type":["string","null"]},"maxFileSize":{"type":"number"},"createdAt":{"type":"string"}},"required":["name","storageQuota","originPullEnabled","id","tenantId","publicKey","customHostname","maxFileSize","createdAt"],"additionalProperties":false},"secret":{"type":"string","pattern":"^[0-9a-f]{64}$"}},"required":["project","secret"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/InvalidRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/projects/{id}":{"get":{"operationId":"getProject","summary":"Read a project","parameters":[{"name":"id","in":"path","required":true,"description":"The project's id.","schema":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{0,62}$"}}],"responses":{"200":{"description":"The project, with its file count and bytes used.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"storageQuota":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"originPullEnabled":{"type":"boolean"},"id":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{0,62}$"},"tenantId":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{0,62}$"},"publicKey":{"type":"string"},"customHostname":{"type":["string","null"]},"maxFileSize":{"type":"number"},"createdAt":{"type":"string"},"usage":{"type":"object","properties":{"fileCount":{"type":"number"},"bytesUsed":{"type":"number"}},"required":["fileCount","bytesUsed"],"additionalProperties":false}},"required":["name","storageQuota","originPullEnabled","id","tenantId","publicKey","customHostname","maxFileSize","createdAt","usage"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/InvalidRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"patch":{"operationId":"updateProject","summary":"Edit a project","description":"At least one field. `storageQuota` and `originPullEnabled` are a superadmin's to set, so a key that sends either gets `403 FORBIDDEN`, which leaves `name`.","parameters":[{"name":"id","in":"path","required":true,"description":"The project's id.","schema":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{0,62}$"}}],"requestBody":{"required":true,"description":"The fields to change.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"storageQuota":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"originPullEnabled":{"type":"boolean"}},"additionalProperties":false}}}},"responses":{"200":{"description":"The project after the edit.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"storageQuota":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"originPullEnabled":{"type":"boolean"},"id":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{0,62}$"},"tenantId":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{0,62}$"},"publicKey":{"type":"string"},"customHostname":{"type":["string","null"]},"maxFileSize":{"type":"number"},"createdAt":{"type":"string"}},"required":["name","storageQuota","originPullEnabled","id","tenantId","publicKey","customHostname","maxFileSize","createdAt"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/InvalidRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/projects/{id}/files":{"get":{"operationId":"listProjectFiles","summary":"List a project's files","parameters":[{"name":"id","in":"path","required":true,"description":"The project's id.","schema":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{0,62}$"}},{"name":"page","in":"query","required":false,"description":"The page to read; a page holds 24 files, newest first.","schema":{"default":1,"type":"integer","exclusiveMinimum":0,"maximum":1000000}}],"responses":{"200":{"description":"One page of files, newest first.","content":{"application/json":{"schema":{"type":"object","properties":{"files":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{0,62}$"},"tenantId":{"type":"string"},"projectId":{"type":"string"},"contentType":{"type":"string"},"bytes":{"type":"number"},"sha256":{"type":"string"},"originalFilename":{"type":["string","null"]},"source":{"type":"string"},"focalX":{"type":["number","null"]},"focalY":{"type":["number","null"]},"createdAt":{"type":"string"}},"required":["id","tenantId","projectId","contentType","bytes","sha256","originalFilename","source","focalX","focalY","createdAt"],"additionalProperties":false}},"page":{"type":"number"},"pageSize":{"type":"number"},"total":{"type":"number"}},"required":["files","page","pageSize","total"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/InvalidRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"operationId":"uploadFile","summary":"Upload a file","description":"A `multipart/form-data` body with the file in a part named `file`. The dashboard streams it to the image service unchanged, which enforces the size limit, the project's remaining quota and the plan's storage cap. A request with another content type is `400 VALIDATION_FAILED`.","parameters":[{"name":"id","in":"path","required":true,"description":"The project's id.","schema":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{0,62}$"}}],"requestBody":{"required":true,"description":"The file, and optionally its focal point.","content":{"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","description":"The file's bytes. Its content type is taken from the part's own `Content-Type`, and its name from the part's filename. Not empty, and no larger than the project's `maxFileSize` or its remaining storage quota."},"focalX":{"type":"number","minimum":0,"maximum":1,"description":"Optional focal point, left to right. Send with `focalY` or not at all."},"focalY":{"type":"number","minimum":0,"maximum":1,"description":"Optional focal point, top to bottom. Send with `focalX` or not at all."}}}}}},"responses":{"201":{"description":"The new file's id.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{0,62}$"}},"required":["id"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/InvalidRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"description":"`PAYMENT_REQUIRED`: uploads are not available on the plan, or its storage cap is reached.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"413":{"description":"`PAYLOAD_TOO_LARGE`: the file exceeds the project's maximum file size or its remaining storage quota.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"502":{"$ref":"#/components/responses/UpstreamError"}}}},"/api/files/{id}":{"get":{"operationId":"getFile","summary":"Read a file's record","parameters":[{"name":"id","in":"path","required":true,"description":"The file's id.","schema":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{0,62}$"}}],"responses":{"200":{"description":"The file.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{0,62}$"},"tenantId":{"type":"string"},"projectId":{"type":"string"},"contentType":{"type":"string"},"bytes":{"type":"number"},"sha256":{"type":"string"},"originalFilename":{"type":["string","null"]},"source":{"type":"string"},"focalX":{"type":["number","null"]},"focalY":{"type":["number","null"]},"createdAt":{"type":"string"}},"required":["id","tenantId","projectId","contentType","bytes","sha256","originalFilename","source","focalX","focalY","createdAt"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/InvalidRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"patch":{"operationId":"updateFile","summary":"Set or clear a file's focal point","description":"Send both coordinates, or send both as `null` to clear them.","parameters":[{"name":"id","in":"path","required":true,"description":"The file's id.","schema":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{0,62}$"}}],"requestBody":{"required":true,"description":"The focal point.","content":{"application/json":{"schema":{"type":"object","properties":{"focalX":{"anyOf":[{"type":"number","minimum":0,"maximum":1},{"type":"null"}]},"focalY":{"anyOf":[{"type":"number","minimum":0,"maximum":1},{"type":"null"}]}},"required":["focalX","focalY"],"additionalProperties":false}}}},"responses":{"200":{"description":"The file after the edit.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{0,62}$"},"tenantId":{"type":"string"},"projectId":{"type":"string"},"contentType":{"type":"string"},"bytes":{"type":"number"},"sha256":{"type":"string"},"originalFilename":{"type":["string","null"]},"source":{"type":"string"},"focalX":{"type":["number","null"]},"focalY":{"type":["number","null"]},"createdAt":{"type":"string"}},"required":["id","tenantId","projectId","contentType","bytes","sha256","originalFilename","source","focalX","focalY","createdAt"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/InvalidRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"operationId":"deleteFile","summary":"Delete a file","description":"Removes the file's row, its stored original and its variants, then purges their cached copies. A delete that stops part-way answers `502 DELETE_INCOMPLETE`; run it again with `?project=` and the hashes in `details.pendingPurge` as `purge`. A re-run, or any request carrying `purge` hashes, takes a slot from a separate limit and can answer `429`. `503 PURGE_NOT_CONFIGURED` means the file is deleted but its cache purge is not set up here; `details` has the body a `200` would.","parameters":[{"name":"id","in":"path","required":true,"description":"The file's id.","schema":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{0,62}$"}},{"name":"project","in":"query","required":false,"description":"Names the project when re-running a delete whose first run removed the file's row. Ignored while the file still exists.","schema":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{0,62}$"}}],"requestBody":{"required":false,"description":"Hashes left to purge by an earlier run. Omit on a first delete.","content":{"application/json":{"schema":{"type":"object","properties":{"purge":{"maxItems":1800,"type":"array","items":{"type":"string","pattern":"^[0-9a-f]{32}$"}}},"required":["purge"],"additionalProperties":false}}}},"responses":{"200":{"description":"The file is deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{0,62}$"},"projectId":{"type":"string"},"variants":{"type":"number"},"purge":{"type":"object","properties":{"status":{"type":"string","enum":["complete","incomplete","not-configured"]},"pending":{"type":"array","items":{"type":"string"}}},"required":["status","pending"],"additionalProperties":false}},"required":["id","projectId","variants","purge"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/InvalidRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"502":{"description":"`DELETE_INCOMPLETE`: the delete stopped at a step; `details.failedStep`, `completed`, `projectId` and `pendingPurge` say where. Run it again.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"`PURGE_NOT_CONFIGURED`: the file is deleted, but its cached copies are not purged yet.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/files/{id}/focal-suggestion":{"get":{"operationId":"suggestFocalPoint","summary":"Ask where a file's subject is","description":"Finds a face, or else the most salient area. Nothing is stored: confirm it and save it with `PATCH /api/files/{id}`. Limited to 10 requests per 60 seconds per project; past that, `429` with `Retry-After`. A workspace whose plan is not active gets `403 BILLING_INACTIVE`.","parameters":[{"name":"id","in":"path","required":true,"description":"The file's id.","schema":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{0,62}$"}}],"responses":{"200":{"description":"The suggested focal point.","content":{"application/json":{"schema":{"type":"object","properties":{"focalX":{"type":"number","minimum":0,"maximum":1},"focalY":{"type":"number","minimum":0,"maximum":1},"source":{"type":"string","enum":["face","attention"]},"detector":{"type":"string","const":"yunet-2026may"}},"required":["focalX","focalY","source","detector"],"additionalProperties":false}}}},"400":{"$ref":"#/components/responses/InvalidRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"502":{"$ref":"#/components/responses/UpstreamError"}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"API key (dyn_...)","description":"A workspace API key, sent as `Authorization: Bearer dyn_...`. When the header is present the session cookie is ignored, so a bad key is `401` and never falls back to a session."}},"schemas":{"Error":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}},"responses":{"InvalidRequest":{"description":"`VALIDATION_FAILED` (a body, query or content type) or `INVALID_PATH_PARAMETER`. `details` is `{ formErrors, fieldErrors }`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Unauthorized":{"description":"`UNAUTHORIZED`: the key is missing, invalid, expired, revoked or refused. The response does not say which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Forbidden":{"description":"`FORBIDDEN` (a field a key may not set, or a write from another origin), `BILLING_INACTIVE` or `PROJECT_LIMIT_REACHED`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotFound":{"description":"`NOT_FOUND`: no such resource in the key's workspace. Another workspace's resource answers the same.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"RateLimited":{"description":"`RATE_LIMITED`: past the key's limit, or past the failed-verification limit for the caller's address.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"UpstreamError":{"description":"`UPSTREAM_ERROR`: the image service could not be reached or answered unexpectedly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}