{"openapi":"3.1.1","info":{"title":"Origami workspace API","summary":"Validate, repair and enrich publications from your own systems.","description":"Origami is the production toolchain for the publishing industry: validate EPUBs with\nepubcheck, check accessibility with Ace by DAISY, extract and enrich metadata into\nONIX for Books 3, convert formats, and generate publication copy. This document\ndescribes the **workspace plane** — the API a customer's own key opens.\n\n## Authentication\n\nEvery request carries a workspace API key as a bearer token:\n`Authorization: Bearer <key>`. A key is minted in **Settings → API keys** (workspace\nowners and admins) or with `php artisan api:token`. The key names its own workspace:\nthere is no account parameter anywhere in this API, and a key can never read or write\nanother workspace's publications.\n\n## Abilities\n\nA key carries abilities (library:read, library:write, tools:run), and each operation names the ones it needs in\nthe `x-required-abilities` extension. A key without the ability is answered `403`\nbefore the operation runs. The default grant for a workspace key is all three.\n\nOAuth is **not** accepted here. It authorizes the MCP server only (`POST /mcp`, scope\n`mcp:use`); this plane takes an API key.\n\n## Errors\n\nEvery failure answers the same envelope: `{\"error\": \"...\", \"reason\": \"...\",\n\"detail\": \"...\"}`. `error` is the human summary and `detail` is optional context;\n`reason` is a stable machine string and is what an integration should branch on. The\nreasons each operation can answer are listed on its error responses.\n\n## Conventions\n\n- **Asynchronous runs.** Starting a tool answers `202` with a run to poll. A run is\n  terminal at `succeeded`, `failed` or `rejected`; the report is readable once it has\n  succeeded.\n- **Idempotency.** `POST /api/v1/publications/{publication}/runs` honours an\n  `Idempotency-Key` header: a retry replays the original run (`200`) instead of\n  spending a second one.\n- **Pagination.** Lists answer `{data, next_cursor, has_more}`, newest first, 50 rows\n  a page. Pass `next_cursor` back as `?cursor=` until `has_more` is false.\n- **File bytes never route through the API.** An upload is a presigned `PUT` straight\n  to storage and a download is a `302` to a presigned `GET`.\n- **Rate limits.** 60 requests a minute per key. Metered tools additionally answer to\n  the workspace's daily fair-use limits, and adding a publication spends the plan's\n  publication allowance.\n\n## The same capabilities over MCP\n\nEvery operation here has a matching tool on Origami's MCP server at `POST /mcp`\n(Streamable HTTP) — same services, same tenancy, same quotas, same `reason` strings.\nSee /.well-known/mcp/server-card.json and /auth.md.","version":"1.0.0","termsOfService":"https://origami.publica.la/en/legals/terms","contact":{"name":"Origami by publica.la","url":"https://origami.publica.la/en/developers"},"x-mcp-endpoint":"https://origami.publica.la/mcp","x-documentation":"https://origami.publica.la/en/api"},"servers":[{"url":"https://origami.publica.la","description":"Origami"}],"security":[{"apiKey":[]}],"tags":[{"name":"Workspace","description":"The workspace behind the key, and what it may still spend."},{"name":"Publications","description":"Books and audiobooks in the library, and the two-step upload that adds one."},{"name":"Files","description":"A publication's version history and its download links."},{"name":"Runs","description":"Starting publishing tools against a publication, and reading what they produced."},{"name":"Reader","description":"The publication's mirrors on the publica.la reader store."}],"paths":{"/api/v1/tools":{"get":{"operationId":"listTools","tags":["Workspace"],"summary":"List the publishing tools this API exposes","description":"The tool catalog as the API exposes it: each tool's slug, display name, category, description, whether it is metered (metered tools count against the workspace's daily fair-use limits) and whether a run replaces the publication's working file. Tools held back from the public and tools excluded from the API never appear. The slug is what `POST /api/v1/publications/{publication}/runs` takes as `tool`.","x-required-abilities":["tools:run"],"responses":{"200":{"description":"The tool catalog.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ToolList"}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/account":{"get":{"operationId":"getAccount","tags":["Workspace"],"summary":"Get the workspace behind the key","description":"The first call an integration makes: the key names its own workspace, so this is how it discovers which one it landed in, what plan that workspace is on, whether it is entitled to run paid work at all, and how much of its publication allowance is left. A plan that cannot be resolved (a Stripe hiccup) is reported as `null` rather than as zero, which a client would read as \"out of allowance\".","x-required-abilities":["library:read"],"responses":{"200":{"description":"The workspace, its plan and its publication allowance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/account/usage":{"get":{"operationId":"getAccountUsage","tags":["Workspace"],"summary":"Get what the workspace has spent against its limits","description":"The daily fair-use ceiling on metered tool runs: the workspace-wide limit, how many metered runs the last rolling day used, and the narrower per-tool per-publication limit (which has no single \"used\" figure, because it is counted per publication). A run past the workspace ceiling is refused with `daily_limit_reached`. The publication allowance is repeated here so a client can read both ceilings in one call.","x-required-abilities":["library:read"],"responses":{"200":{"description":"Daily metered-run usage and the publication allowance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UsageEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/uploads":{"post":{"operationId":"createUpload","tags":["Publications"],"summary":"Reserve a publication id and mint a presigned upload URL","description":"Step one of the two-step upload. Reserves the publication id and returns a presigned `PUT` valid for 30 minutes; the client uploads the file bytes straight to `upload_url` with the returned headers, then calls `POST /api/v1/publications` with the same `publication_id`, `key`, `original_filename` and `size_bytes` to register the landed object. The allowance is checked here as well as at registration, so a client never uploads bytes that cannot land.","x-required-abilities":["library:write"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadRequest"}}}},"responses":{"201":{"description":"The reserved publication id and the presigned PUT.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadEnvelope"}}}},"422":{"description":"The file is not a supported format or exceeds the size limit for its format, or the workspace has spent its publication allowance for the cycle.","x-error-reasons":["publication_quota_exceeded"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"publication_quota_exceeded":{"value":{"error":"The request could not be honoured.","reason":"publication_quota_exceeded"}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/publications":{"get":{"operationId":"listPublications","tags":["Publications"],"summary":"List the workspace's publications","description":"Every book and audiobook in the workspace, newest first, 50 to a page. Keyset-paginated: pass the previous page's `next_cursor` back as `?cursor=` until `has_more` is false, so a publication added while a client pages cannot shift the window under it.","x-required-abilities":["library:read"],"parameters":[{"name":"cursor","in":"query","required":false,"description":"The previous page's `next_cursor`. Omit for the first page.","schema":{"type":"string"}}],"responses":{"200":{"description":"One page of publications.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicationList"}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"operationId":"createPublication","tags":["Publications"],"summary":"Register an uploaded file as a publication","description":"Step two of the upload: the bytes have landed on the presigned URL, and this registers them as a publication in the workspace. The allowance is re-checked here, so parallel registrations cannot slip past the limit.","x-required-abilities":["library:write"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicationRequest"}}}},"responses":{"201":{"description":"The registered publication.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicationEnvelope"}}}},"422":{"description":"The object never landed, its stored size does not match the declaration, the key is outside this workspace's namespace, the format is not supported, or the allowance has since been spent.","x-error-reasons":["unsupported_format","invalid_upload_key","upload_not_found","upload_incomplete","publication_quota_exceeded"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"unsupported_format":{"value":{"error":"The request could not be honoured.","reason":"unsupported_format"}},"invalid_upload_key":{"value":{"error":"The request could not be honoured.","reason":"invalid_upload_key"}},"upload_not_found":{"value":{"error":"The request could not be honoured.","reason":"upload_not_found"}},"upload_incomplete":{"value":{"error":"The request could not be honoured.","reason":"upload_incomplete"}},"publication_quota_exceeded":{"value":{"error":"The request could not be honoured.","reason":"publication_quota_exceeded"}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/publications/{publication}":{"get":{"operationId":"getPublication","tags":["Publications"],"summary":"Get one publication","description":"One publication of the workspace the key belongs to: its type, status, title, the original's stored size, the working file's id and size, and its creation time. A publication in another workspace is answered `publication_not_found` rather than `403` — a key cannot learn that it exists.","x-required-abilities":["library:read"],"parameters":[{"name":"publication","in":"path","required":true,"description":"The publication id, as returned by the upload flow or by `GET /api/v1/publications`.","schema":{"type":"string"}}],"responses":{"200":{"description":"The publication.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicationEnvelope"}}}},"404":{"description":"No publication with that id exists in this workspace.","x-error-reasons":["publication_not_found"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"publication_not_found":{"value":{"error":"The request could not be honoured.","reason":"publication_not_found"}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/publications/{publication}/files":{"get":{"operationId":"listPublicationFiles","tags":["Files"],"summary":"List a publication's file versions","description":"The publication's version history: the immutable original plus every file a tool derived from it. The working version — what the next tool run acts on — is flagged `current` on its row and repeated as `current_file_id`.","x-required-abilities":["library:read"],"parameters":[{"name":"publication","in":"path","required":true,"description":"The publication id, as returned by the upload flow or by `GET /api/v1/publications`.","schema":{"type":"string"}}],"responses":{"200":{"description":"The version history.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FileList"}}}},"404":{"description":"No publication with that id exists in this workspace.","x-error-reasons":["publication_not_found"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"publication_not_found":{"value":{"error":"The request could not be honoured.","reason":"publication_not_found"}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/publications/{publication}/files/current":{"get":{"operationId":"getCurrentPublicationFile","tags":["Files"],"summary":"Get the publication's working version","description":"The one file the next tool run acts on, and the one a client downloads after a repair.","x-required-abilities":["library:read"],"parameters":[{"name":"publication","in":"path","required":true,"description":"The publication id, as returned by the upload flow or by `GET /api/v1/publications`.","schema":{"type":"string"}}],"responses":{"200":{"description":"The working file version.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FileEnvelope"}}}},"404":{"description":"No publication with that id exists in this workspace, or the publication has no file yet.","x-error-reasons":["publication_not_found","file_not_found"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"publication_not_found":{"value":{"error":"The request could not be honoured.","reason":"publication_not_found"}},"file_not_found":{"value":{"error":"The request could not be honoured.","reason":"file_not_found"}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/files/{file}/download":{"get":{"operationId":"downloadFile","tags":["Files"],"summary":"Download one file version","description":"File bytes never route through the API. This answers `302` to a presigned `GET` on the library bucket, valid for 10 minutes; follow the redirect to fetch the file.","x-required-abilities":["library:read"],"parameters":[{"name":"file","in":"path","required":true,"description":"The file version id, as listed by `GET /api/v1/publications/{publication}/files`.","schema":{"type":"string"}}],"responses":{"302":{"description":"A redirect to the short-lived presigned download URL.","headers":{"Location":{"description":"The presigned storage URL.","schema":{"type":"string","format":"uri"}}}},"404":{"description":"No file with that id belongs to a publication in this workspace.","x-error-reasons":["file_not_found"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"file_not_found":{"value":{"error":"The request could not be honoured.","reason":"file_not_found"}}}}}},"409":{"description":"This deployment's storage backend cannot mint download links.","x-error-reasons":["downloads_unavailable"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"downloads_unavailable":{"value":{"error":"The request could not be honoured.","reason":"downloads_unavailable"}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/publications/{publication}/reader":{"get":{"operationId":"getPublicationReader","tags":["Reader"],"summary":"Get a publication's reader-store mirrors","description":"Where the publication stands on the publica.la reader store: whether the integration is configured for this deployment at all, and one row per mirror — the immutable `original` and the `modified` working version — with its conversion status, whether a push is in flight, and the last sync error. A publication that has never been pushed reports no mirrors. The store's own URLs are absent by design: reaching them needs a per-viewer SSO handoff, so a raw URL here would be a link nobody holding this key can open.","x-required-abilities":["library:read"],"parameters":[{"name":"publication","in":"path","required":true,"description":"The publication id, as returned by the upload flow or by `GET /api/v1/publications`.","schema":{"type":"string"}}],"responses":{"200":{"description":"The publication's mirrors.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReaderEnvelope"}}}},"404":{"description":"No publication with that id exists in this workspace.","x-error-reasons":["publication_not_found"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"publication_not_found":{"value":{"error":"The request could not be honoured.","reason":"publication_not_found"}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/publications/{publication}/reader/sync":{"post":{"operationId":"syncPublicationReader","tags":["Reader"],"summary":"Re-push a publication to the reader store","description":"Mirrors are pushed automatically on upload and on every change, so this is the rescue beside them: a first publish for a book whose initial push died, and a retry for a mirror the store failed to convert. Answers `202` with the mirror state as the push starts; the push itself is asynchronous, so the mirror reports as processing until the store finishes converting. Writes to the store rather than running a tool, which is why it needs `library:write`.","x-required-abilities":["library:write"],"parameters":[{"name":"publication","in":"path","required":true,"description":"The publication id, as returned by the upload flow or by `GET /api/v1/publications`.","schema":{"type":"string"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReaderSyncRequest"}}}},"responses":{"202":{"description":"The mirror state as the push starts.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReaderEnvelope"}}}},"404":{"description":"No publication with that id exists in this workspace.","x-error-reasons":["publication_not_found"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"publication_not_found":{"value":{"error":"The request could not be honoured.","reason":"publication_not_found"}}}}}},"409":{"description":"This deployment has no reader integration configured, or a push of the same variant is already in flight.","x-error-reasons":["reader_not_configured","reader_sync_in_progress"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"reader_not_configured":{"value":{"error":"The request could not be honoured.","reason":"reader_not_configured"}},"reader_sync_in_progress":{"value":{"error":"The request could not be honoured.","reason":"reader_sync_in_progress"}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/publications/{publication}/runs":{"get":{"operationId":"listPublicationRuns","tags":["Runs"],"summary":"List a publication's tool runs","description":"The runs recorded against one publication, newest first, 50 to a page, optionally narrowed to a single tool slug.","x-required-abilities":["library:read"],"parameters":[{"name":"publication","in":"path","required":true,"description":"The publication id, as returned by the upload flow or by `GET /api/v1/publications`.","schema":{"type":"string"}},{"name":"cursor","in":"query","required":false,"description":"The previous page's `next_cursor`. Omit for the first page.","schema":{"type":"string"}},{"name":"tool","in":"query","required":false,"description":"Narrow the list to one tool slug, as listed by `GET /api/v1/tools`.","schema":{"type":"string"}}],"responses":{"200":{"description":"One page of runs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunList"}}}},"404":{"description":"No publication with that id exists in this workspace.","x-error-reasons":["publication_not_found"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"publication_not_found":{"value":{"error":"The request could not be honoured.","reason":"publication_not_found"}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"operationId":"createToolRun","tags":["Runs"],"summary":"Start a tool run against a publication","description":"Runs one Origami publishing tool against the publication's working version — the same file, the same limits and the same settlement as the web app. Answers `202` with a run to poll through `GET /api/v1/runs/{run}`; a synchronous tool simply reaches a terminal status on the first poll. Send an `Idempotency-Key` header to make a retry safe: the original run is replayed with `200` instead of a second one being spent.","x-required-abilities":["tools:run"],"parameters":[{"name":"publication","in":"path","required":true,"description":"The publication id, as returned by the upload flow or by `GET /api/v1/publications`.","schema":{"type":"string"}},{"name":"Idempotency-Key","in":"header","required":false,"description":"An opaque client-chosen string. A retry carrying the same key, method and path replays the original run instead of starting another.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunRequest"}}}},"responses":{"200":{"description":"The original run, replayed for a retried Idempotency-Key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunEnvelope"}}}},"202":{"description":"The run that was started.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunEnvelope"}}}},"402":{"description":"The workspace has no active plan, so it may not run paid work. Read endpoints keep working, which is why this is not a 403.","x-error-reasons":["plan_required"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"plan_required":{"value":{"error":"The request could not be honoured.","reason":"plan_required"}}}}}},"404":{"description":"No publication with that id exists in this workspace, or the tool slug is not one this API exposes.","x-error-reasons":["publication_not_found","unknown_tool"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"publication_not_found":{"value":{"error":"The request could not be honoured.","reason":"publication_not_found"}},"unknown_tool":{"value":{"error":"The request could not be honoured.","reason":"unknown_tool"}}}}}},"409":{"description":"The tool is not available yet, the publication has no file to run against, another run currently holds the publication's working file, or a request with the same Idempotency-Key is still in flight.","x-error-reasons":["tool_unavailable","file_not_found","working_file_busy","idempotency_in_flight"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"tool_unavailable":{"value":{"error":"The request could not be honoured.","reason":"tool_unavailable"}},"file_not_found":{"value":{"error":"The request could not be honoured.","reason":"file_not_found"}},"working_file_busy":{"value":{"error":"The request could not be honoured.","reason":"working_file_busy"}},"idempotency_in_flight":{"value":{"error":"The request could not be honoured.","reason":"idempotency_in_flight"}}}}}},"422":{"description":"The options are not valid for this tool.","x-error-reasons":["invalid_options"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"invalid_options":{"value":{"error":"The request could not be honoured.","reason":"invalid_options"}}}}}},"429":{"description":"A metered tool has reached the workspace's daily fair-use limit. Carries `Retry-After`.","x-error-reasons":["daily_limit_reached"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"daily_limit_reached":{"value":{"error":"The request could not be honoured.","reason":"daily_limit_reached"}}}}},"headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/api/v1/runs/{run}":{"get":{"operationId":"getToolRun","tags":["Runs"],"summary":"Get a tool run","description":"The state of one run: which tool it ran, which publication it acted on, its status (`pending` or `processing` while it is still going; `succeeded`, `failed` or `rejected` once it has settled), the failure reason and detail when it did not succeed, and its timings.","x-required-abilities":["library:read"],"parameters":[{"name":"run","in":"path","required":true,"description":"The tool run id.","schema":{"type":"string"}}],"responses":{"200":{"description":"The run.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunEnvelope"}}}},"404":{"description":"No run with that id exists in this workspace.","x-error-reasons":["run_not_found"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"run_not_found":{"value":{"error":"The request could not be honoured.","reason":"run_not_found"}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/runs/{run}/report":{"get":{"operationId":"getToolRunReport","tags":["Runs"],"summary":"Get a finished run's report","description":"The tool's own JSON result — an epubcheck report, an accessibility report, extracted metadata — for a run that succeeded. One narrowing applies: `epub-description` returns only the publication-page copy (the `back_cover` style), with a `long` version for the page body and a `short` one for cards and metadata.","x-required-abilities":["library:read"],"parameters":[{"name":"run","in":"path","required":true,"description":"The tool run id.","schema":{"type":"string"}}],"responses":{"200":{"description":"The run's report.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunReportEnvelope"}}}},"404":{"description":"No run with that id exists in this workspace.","x-error-reasons":["run_not_found"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"run_not_found":{"value":{"error":"The request could not be honoured.","reason":"run_not_found"}}}}}},"409":{"description":"The run has not settled yet, or it did not succeed and so has no report (`detail` carries the failure detail).","x-error-reasons":["run_not_finished","run_not_succeeded"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"run_not_finished":{"value":{"error":"The request could not be honoured.","reason":"run_not_finished"}},"run_not_succeeded":{"value":{"error":"The request could not be honoured.","reason":"run_not_succeeded"}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}}},"components":{"securitySchemes":{"apiKey":{"type":"http","scheme":"bearer","description":"A workspace API key, sent as `Authorization: Bearer <key>`. Minted in Settings → API keys (workspace owners and admins) or with `php artisan api:token`. The key names its own workspace and carries the abilities each operation lists in `x-required-abilities`."}},"responses":{"Unauthenticated":{"description":"No bearer token, or a token that is not a valid workspace API key.","x-error-reasons":["invalid_token"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"invalid_token":{"value":{"error":"The request could not be honoured.","reason":"invalid_token"}}}}}},"Forbidden":{"description":"The key lacks the ability this operation needs, is not bound to a workspace, or its owner no longer belongs to that workspace.","x-error-reasons":["token_without_workspace","membership_revoked"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"token_without_workspace":{"value":{"error":"The request could not be honoured.","reason":"token_without_workspace"}},"membership_revoked":{"value":{"error":"The request could not be honoured.","reason":"membership_revoked"}}}}}},"RateLimited":{"description":"Too many requests: the plane allows 60 a minute per key.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}}}},"schemas":{"Error":{"type":"object","description":"The one failure envelope every endpoint on this plane uses.","required":["error","reason"],"properties":{"error":{"type":"string","description":"The human summary."},"reason":{"type":"string","description":"The stable machine string to branch on."},"detail":{"type":"string","description":"Optional extra context."}}},"Tool":{"type":"object","properties":{"slug":{"type":"string","description":"What `POST /runs` takes as `tool`."},"name":{"type":"string"},"category":{"type":["string","null"]},"metered":{"type":"boolean","description":"Counts against the workspace's daily fair-use limits."},"writes_working_file":{"type":"boolean","description":"Two such runs cannot overlap on one publication."},"description":{"type":["string","null"]}}},"ToolList":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Tool"}}},"required":["data"]},"Account":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"type":{"type":"string"},"plan":{"type":"object","properties":{"slug":{"type":["string","null"],"enum":["starter","pro","max",null]},"name":{"type":["string","null"]}}},"subscribed":{"type":"boolean"},"on_trial":{"type":"boolean"},"entitled":{"type":"boolean","description":"Wider than `subscribed`, and the flag that decides whether a tool run may start."},"unlimited":{"type":"boolean","description":"True for a publica.la operator's credential, which no allowance, plan requirement or daily ceiling binds. The workspace's figures are still reported as they stand; nothing is refused against them for that credential."},"publications":{"$ref":"#/components/schemas/PublicationAllowance"}}},"AccountEnvelope":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Account"}},"required":["data"]},"PublicationAllowance":{"type":["object","null"],"description":"Null when the plan could not be resolved. `remaining` is the sum of all three buckets and is the only figure that answers \"can I add another one\".","properties":{"used":{"type":"integer"},"limit":{"type":"integer"},"remaining":{"type":"integer"},"resets_at":{"type":"string","format":"date-time"},"granted_remaining":{"type":"integer","description":"Slots the Origami team granted; spendable once, not once a cycle."},"promotional_remaining":{"type":"integer"}}},"UsageEnvelope":{"type":"object","properties":{"data":{"type":"object","properties":{"daily":{"type":"object","properties":{"account_limit":{"type":"integer"},"account_used":{"type":"integer"},"tool_limit":{"type":"integer","description":"Per tool per publication per rolling day."}}},"publications":{"$ref":"#/components/schemas/PublicationAllowance"},"unlimited":{"type":"boolean","description":"The same flag the account resource carries: neither ceiling above binds this credential."}}}},"required":["data"]},"UploadRequest":{"type":"object","required":["original_filename","size_bytes"],"properties":{"original_filename":{"type":"string","maxLength":255,"description":"The extension decides the format: a book (EPUB, PDF), a source document (docx, doc, odt, rtf, md, html, htm, txt) or an audiobook."},"size_bytes":{"type":"integer","minimum":1,"description":"Checked against the ceiling for that format."}}},"UploadEnvelope":{"type":"object","properties":{"data":{"type":"object","properties":{"publication_id":{"type":"string","description":"Reserved here; pass it back when registering."},"key":{"type":"string"},"upload_url":{"type":"string","format":"uri"},"headers":{"type":"object","additionalProperties":{"type":"string"},"description":"Send these with the PUT."},"expires_at":{"type":"string","format":"date-time"}}}},"required":["data"]},"PublicationRequest":{"type":"object","required":["publication_id","key","original_filename","size_bytes"],"properties":{"publication_id":{"type":"string","maxLength":64},"key":{"type":"string","maxLength":1024},"original_filename":{"type":"string","maxLength":255},"size_bytes":{"type":"integer","minimum":1,"description":"Must match what actually landed, or the call is refused with `upload_incomplete`."},"mime_type":{"type":["string","null"],"maxLength":255}}},"Publication":{"type":"object","properties":{"id":{"type":"string"},"type":{"type":"string"},"status":{"type":"string"},"title":{"type":["string","null"]},"storage_bytes":{"type":["integer","null"],"description":"The immutable original as uploaded, in bytes. Written once at ingest and never revised, so it does not follow what a tool run ships — for the version a download returns, read `current_file_bytes`."},"current_file_id":{"type":["string","null"],"description":"The working version the next run acts on."},"current_file_bytes":{"type":["integer","null"],"description":"The working version's size in bytes — what a download of this publication returns. Equal to `storage_bytes` until a run ships a new version; smaller after an optimization. Null while the publication has no file yet."},"created_at":{"type":["string","null"],"format":"date-time"}}},"PublicationEnvelope":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Publication"}},"required":["data"]},"PublicationList":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Publication"}},"next_cursor":{"type":["string","null"],"description":"Pass back as `?cursor=` for the next page."},"has_more":{"type":"boolean"}},"required":["data","next_cursor","has_more"]},"File":{"type":"object","properties":{"id":{"type":"string"},"publication_id":{"type":"string"},"role":{"type":"string"},"derived_kind":{"type":["string","null"]},"original_filename":{"type":["string","null"]},"mime_type":{"type":["string","null"]},"size_bytes":{"type":["integer","null"]},"reverted":{"type":"boolean"},"current":{"type":"boolean"},"created_at":{"type":["string","null"],"format":"date-time"}}},"FileEnvelope":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/File"}},"required":["data"]},"FileList":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/File"}},"current_file_id":{"type":["string","null"]}}},"ReaderSyncRequest":{"type":"object","properties":{"variant":{"type":"string","enum":["original","modified"],"description":"Which mirror to push. Defaults to `original`."}}},"ReaderEnvelope":{"type":"object","properties":{"data":{"type":"object","properties":{"publication_id":{"type":"string"},"reader_configured":{"type":"boolean"},"mirrors":{"type":"array","items":{"type":"object","properties":{"variant":{"type":"string","enum":["original","modified"]},"status":{"type":"string"},"created":{"type":"boolean"},"file_type":{"type":["string","null"]},"sync_pending":{"type":"boolean"},"sync_error":{"type":["string","null"]},"synced_at":{"type":["string","null"],"format":"date-time"},"created_at":{"type":["string","null"],"format":"date-time"},"updated_at":{"type":["string","null"],"format":"date-time"}}}}}}},"required":["data"]},"RunRequest":{"type":"object","required":["tool"],"properties":{"tool":{"type":"string","maxLength":64,"description":"A slug from `GET /api/v1/tools`."},"options":{"type":"object","description":"Tool-specific options. Rejected with `invalid_options` when they are not valid for the tool."}}},"Run":{"type":"object","properties":{"id":{"type":"string"},"publication_id":{"type":["string","null"]},"tool":{"type":"string"},"status":{"type":"string","enum":["pending","held","processing","succeeded","failed","rejected","refunded"],"description":"Terminal at `succeeded`, `failed` or `rejected`."},"error_reason":{"type":["string","null"]},"error_detail":{"type":["string","null"]},"started_at":{"type":["string","null"],"format":"date-time"},"finished_at":{"type":["string","null"],"format":"date-time"},"duration_ms":{"type":["integer","null"]},"progress":{"type":["object","null"],"description":"While the run is processing, the tier's latest phase and counters: `phase` (a snake_case step), `sequence`, and the optional `done`, `total`, `unit`, `current`, `bytes_saved` and `elapsed_ms`. Null until the tier reports, and for tools that do not."},"progress_at":{"type":["string","null"],"format":"date-time","description":"When that progress was received. A `progress` far in the past is the signal that a run has gone quiet."},"created_at":{"type":["string","null"],"format":"date-time"}}},"RunEnvelope":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Run"}},"required":["data"]},"RunList":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Run"}},"next_cursor":{"type":["string","null"],"description":"Pass back as `?cursor=` for the next page."},"has_more":{"type":"boolean"}},"required":["data","next_cursor","has_more"]},"RunReportEnvelope":{"type":"object","properties":{"data":{"type":"object","properties":{"run_id":{"type":"string"},"tool":{"type":"string"},"report":{"type":["object","null"],"description":"The tool's own JSON result; its shape depends on the tool."}}}},"required":["data"]}}}}