Skip to content
Origami API

The production line, programmable

Everything the workspace does — upload, evaluate, improve, download — driven from your own systems over a simple JSON API.

The loop

The API mirrors the workspace: four steps, repeated per book.

01

Upload

Declare the file, PUT the bytes to a presigned URL, register the publication. Files never route through the API itself.

02

Run

Start any tool against the publication's working version. The response is a run you poll — sync tools finish on the first poll.

03

Read

Fetch the run's JSON report: findings, scores, metadata, or the publication-page description.

04

Download

Repairs and edits append new file versions. Download the working version via a short-lived link.

Base URL & authentication

Every request carries a workspace API key as a Bearer token. The key is bound to one workspace: everything you create and read through it lives there, exactly as if you were working in the app. There is no workspace parameter anywhere in this API — the key is what names the tenant, so a key can only ever reach its own library.

Base URL

https://origami.publica.la/api/v1

Every path on this page is relative to that. Requests and responses are JSON; send Accept: application/json and, on a POST, Content-Type: application/json.

A first call

curl https://origami.publica.la/api/v1/publications \
  -H "Authorization: Bearer ${ORIGAMI_API_KEY}" \
  -H "Accept: application/json"

Getting a key

A workspace owner or admin mints keys in Settings → API keys. The value is shown once and never again — only its hash is stored — so put it straight into your secret manager. The same screen revokes a key, and revocation takes effect on the next request.

A key belongs to the person who minted it as well as to the workspace. If that person leaves the workspace, their keys stop working with membership_revoked — so mint integration keys from an account that will outlive the integration.

Abilities

A key carries only the abilities it was minted with, and every route checks exactly one. A request that presents a key without the ability its route needs is refused with 403.

library:read

Read the workspace, its publications, file versions, tool runs, reports and reader mirrors.

library:write

Mint upload slots, register publications, and re-push a publication to the reader store.

tools:run

List the tool catalog and start tool runs.

Quickstart

Six requests take a book from your disk to a validation report and back. Replace the token and file name; everything else is copy-paste.

1 · Ask for an upload slot

curl -X POST https://origami.publica.la/api/v1/uploads \
  -H "Authorization: Bearer ${ORIGAMI_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{"original_filename": "book.epub", "size_bytes": 4180294}'

Answers 201 with a publication_id, the storage key, the upload_url to PUT to, the headers that URL was signed with, and its expires_at — 30 minutes out.

2 · PUT the bytes to the presigned URL

curl -X PUT "UPLOAD_URL_FROM_STEP_1" \
  -H "Content-Type: application/epub+zip" \
  --data-binary @book.epub

Send every header the previous step returned, verbatim. The upload goes straight to storage: no file bytes ever pass through the API, so a 2 GB book uploads at the speed of the bucket rather than the speed of a PHP request.

3 · Register the publication

curl -X POST https://origami.publica.la/api/v1/publications \
  -H "Authorization: Bearer ${ORIGAMI_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{"publication_id": "ID_FROM_STEP_1", "key": "KEY_FROM_STEP_1", "original_filename": "book.epub", "size_bytes": 4180294}'

Pass back the same publication_id, key, original_filename and size_bytes. The size is compared against what actually landed, so a truncated upload is refused here rather than discovered by the first tool run.

4 · Run a tool against it

curl -X POST https://origami.publica.la/api/v1/publications/PUBLICATION_ID/runs \
  -H "Authorization: Bearer ${ORIGAMI_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{"tool": "epub-check"}'

Answers 202 with a run. A run always acts on the publication's working version — the newest file a tool produced, or the original if none has.

5 · Poll the run, then read the report

curl https://origami.publica.la/api/v1/runs/RUN_ID \
  -H "Authorization: Bearer ${ORIGAMI_API_KEY}"

curl https://origami.publica.la/api/v1/runs/RUN_ID/report \
  -H "Authorization: Bearer ${ORIGAMI_API_KEY}"

Poll until status is terminal. The report endpoint answers 409 run_not_finished while the run is still going, so polling the run first is the cheaper loop.

6 · Download the working version

curl https://origami.publica.la/api/v1/publications/PUBLICATION_ID/files/current \
  -H "Authorization: Bearer ${ORIGAMI_API_KEY}"

curl -L -o book-improved.epub \
  https://origami.publica.la/api/v1/files/FILE_ID/download \
  -H "Authorization: Bearer ${ORIGAMI_API_KEY}"

The download endpoint answers a 302 to a presigned GET valid for 10 minutes. Follow redirects (curl -L) and do not cache the redirect target.

Endpoints

Every endpoint lives under /api/v1, speaks JSON, and answers an error as {error, reason, detail} with a stable machine reason. The ability column is the one each route checks.

Workspace

GET /api/v1/account library:read

The workspace behind the key: plan, entitlement and publication allowance.

GET /api/v1/account/usage library:read

Today's metered-tool usage against the daily limit, plus what's left of the publication allowance.

Request & response

Request

curl "https://origami.publica.la/api/v1/account" \
  -H "Authorization: Bearer ${ORIGAMI_API_KEY}"

Response

{
  "data": {
    "id": "01JQ8ZW4M3S9F0AC2KQ7X6HV1B",
    "name": "Ediciones Ejemplo",
    "type": "standalone",
    "plan": { "slug": "pro", "name": "Pro" },
    "subscribed": true,
    "on_trial": false,
    "entitled": true,
    "unlimited": false,
    "publications": {
      "used": 2,
      "limit": 5,
      "remaining": 3,
      "resets_at": "2026-10-08T00:00:00+00:00",
      "granted_remaining": 0,
      "promotional_remaining": 0
    }
  }
}

Publications & uploads

GET /api/v1/tools tools:run

The tool catalog as the API exposes it, with metering flags.

POST /api/v1/uploads library:write

Mint a presigned PUT for a new file (30 minutes).

POST /api/v1/publications library:write

Register an uploaded object as a publication.

GET /api/v1/publications library:read

List the workspace's publications (cursor pagination).

GET /api/v1/publications/{id} library:read

One publication: type, status, title, working file.

Request & response

Request

curl -X POST "https://origami.publica.la/api/v1/uploads" \
  -H "Authorization: Bearer ${ORIGAMI_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{"original_filename": "book.epub", "size_bytes": 4180294}'

Response

{
  "data": {
    "publication_id": "01JQ90R2VTA6E4M8YN5D3PKC7F",
    "key": "accounts/01JQ8ZW4.../uploads/01JQ90R2....epub",
    "upload_url": "https://storage.example/...&X-Amz-Signature=...",
    "headers": { "Content-Type": "application/epub+zip" },
    "expires_at": "2026-09-04T13:21:00+00:00"
  }
}

File versions

GET /api/v1/publications/{id}/files library:read

The full version history: the immutable original plus every improved copy.

GET /api/v1/publications/{id}/files/current library:read

The working version — what the next tool run acts on.

GET /api/v1/files/{id}/download library:read

302 to a short-lived download link for any version.

GET /api/v1/publications/{id}/reader library:read

The publication's mirrors on the publica.la reader store, and whether the integration is configured.

POST /api/v1/publications/{id}/reader/sync library:write

Re-push a mirror to the publica.la reader (202) — rescues a failed first publish or retries a stuck conversion.

Request & response

Request

curl "https://origami.publica.la/api/v1/publications/PUBLICATION_ID/files/current" \
  -H "Authorization: Bearer ${ORIGAMI_API_KEY}"

Response

{
  "data": {
    "id": "01JQ93B8KDW1H7RZ4T0NQXME62",
    "publication_id": "01JQ90R2VTA6E4M8YN5D3PKC7F",
    "role": "derived",
    "derived_kind": "epub-repair",
    "original_filename": "book.epub",
    "mime_type": "application/epub+zip",
    "size_bytes": 4194512,
    "reverted": false,
    "current": true,
    "created_at": "2026-09-04T12:58:41+00:00"
  }
}

Tool runs

POST /api/v1/publications/{id}/runs tools:run

Start a tool run (202). Accepts an options object and an Idempotency-Key header.

GET /api/v1/publications/{id}/runs library:read

List a publication's runs, filterable by tool.

GET /api/v1/runs/{id} library:read

Run status, timing and error detail.

GET /api/v1/runs/{id}/report library:read

The finished run's JSON report.

Request & response

Request

curl -X POST "https://origami.publica.la/api/v1/publications/PUBLICATION_ID/runs" \
  -H "Authorization: Bearer ${ORIGAMI_API_KEY}" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 5f3a-nightly-sweep-0042" \
  -d '{"tool": "epub-accessibility"}'

Response

{
  "data": {
    "id": "01JQ95T1YB0PJ6VF8C3WKR2HQD",
    "publication_id": "01JQ90R2VTA6E4M8YN5D3PKC7F",
    "tool": "epub-accessibility",
    "status": "queued",
    "error_reason": null,
    "error_detail": null,
    "started_at": null,
    "finished_at": null,
    "duration_ms": null,
    "created_at": "2026-09-04T13:02:10+00:00"
  }
}

The four resources

Four shapes cover the whole API, and every endpoint that returns one returns it identically.

Workspace

Which workspace the key landed in, whether it may run paid work at all (entitled — wider than subscribed, because a publica.la store carries a tier with no Stripe subscription behind it), and the publication allowance. remaining sums the plan's cycle bucket, the slots the team granted and anything a promotion still covers — it is the only figure that answers "can I add another book". When Stripe cannot be reached, plan and publications come back null rather than zero: unknown, not exhausted.

Publication

A book, audiobook or source document in the workspace. current_file_id points at the working version, which is what the next tool run reads, and current_file_bytes is what that version weighs today — storage_bytes stays the size of the original as it was uploaded, so an optimized book reports two different numbers.

File version

One version of a publication. The original is immutable and never replaced; every tool that writes appends a new version, so the history is complete. current marks the working one and reverted marks a version rolled back in the app.

Tool run

One execution of one tool against one publication. status reaches succeeded, failed or rejected and then never changes; error_reason and error_detail say why on the other two.

Tool catalog

The slugs POST /publications/{id}/runs accepts, read live from this deployment — GET /api/v1/tools returns the same list as JSON. Two flags matter when you chain calls.

Slug Tool Category Flags
epub-metadata EPUB Metadata Extractor metadata metered
epub-description EPUB Description Writer description metered
epub-metadata-write EPUB Metadata Editor metadata writes working file
epub-toc EPUB Table of Contents toc metered
epub-toc-generate EPUB Table of Contents Generator toc metered
epub-toc-write EPUB Table of Contents Editor toc writes working file
epub-check EPUB Checker validation
epub-accessibility EPUB Accessibility Checker accessibility
epub-repair EPUB Repair repair metered writes working file
epub-qualebook Qualebook Quality Checklist quality
epub-qualebook-review Qualebook AI Review quality metered
cover-replace Cover Change cover writes working file
onix-export ONIX File Creator metadata
social-copy Social Campaign Writer marketing metered
social-image Social Image Generator marketing metered
social-carousel Social Carousel Generator marketing metered
social-video Social Video Generator marketing metered
pdf-check PDF Diagnosis validation
pdf-accessibility PDF Accessibility Checker accessibility
pdf-accessibility-fix PDF Accessibility Fix accessibility writes working file
pdf-autotag PDF Auto-Tag accessibility metered writes working file
pdf-alt-text PDF Alt Text accessibility metered writes working file
pdf-repair PDF Repair repair metered writes working file
asset-optimize Asset Optimizer repair metered writes working file
pdf-epub-analyze PDF to EPUB Analyzer conversion
pdf-epub-convert PDF to EPUB Converter conversion metered
document-epub-convert Document to EPUB Converter conversion metered

A slug this deployment does not expose answers 404 unknown_tool; one that exists but is not released yet answers 409 tool_unavailable. Options are tool-specific and passed as an options object — an option a tool does not recognize is ignored rather than refused, so check the report to confirm what actually ran. An option that would change what the run is allowed to do is the exception: an option that would make a run wait on a person, sent to a run that applies by itself, is refused with 422 invalid_options rather than quietly widening it.

Conventions

Async by default

Runs answer 202 with a resource you poll. A run is terminal at succeeded, failed or rejected. There are no webhooks yet, so poll — a second or two apart is plenty; nothing here finishes faster than the queue picks it up.

Idempotency

Send an Idempotency-Key header on POST /runs; a retry returns the original run with 200 instead of spending twice, and a retry that arrives while the first is still being recorded answers 409 idempotency_in_flight.

Pagination

Lists answer {data, next_cursor, has_more} with 50 rows a page, newest first. Pass next_cursor back as ?cursor= until has_more is false. The cursor is an id, not an offset, so a book added while you page cannot shift the window under you.

Rate limits

Requests are throttled at 60 a minute per key — per key, so two integrations on one workspace do not share a bucket. Over the ceiling is a 429; back off and retry.

Two spending ceilings

Uploads consume the plan's publication allowance (422 publication_quota_exceeded), and metered tools consume the workspace's daily fair-use allowance (429 daily_limit_reached with Retry-After). GET /account and GET /account/usage report both before you spend either.

Files move directly

Uploads are presigned PUTs and downloads presigned GETs against storage — the API never proxies file bytes, so transfers are fast at any size. Upload links live 30 minutes, download links 10.

Descriptions

The description tool returns the publication-page copy: one style (back_cover) with a long version for the page body and a short one for cards and metadata. The catalogue and bookseller styles the tool also writes stay inside the app.

Dates and ids

Every timestamp is ISO 8601 with an offset. Every id is a ULID — sortable, so ordering by id is ordering by creation.

Errors

Every failure answers the same envelope. reason is the stable machine string to branch on — the HTTP status tells you the class of problem, the reason tells you which one.

The envelope

{
  "error": "Publication not found.",
  "reason": "publication_not_found",
  "detail": null
}

A malformed request body is Laravel's own 422 validation response ({message, errors}), not this envelope — the envelope is for failures the API decided on, the validation shape for a body it could not read.

Status Reason What it means
401 invalid_token No key, an unknown key, or one that has been revoked.
403 token_without_workspace The key is not bound to a workspace — an operator key on a customer route.
403 membership_revoked The key's owner no longer belongs to this workspace. Mint a new key from a current member.
404 publication_not_found No publication with that id in this workspace. Ids are never shared across workspaces.
404 file_not_found No such file version, or the publication has no file yet.
404 run_not_found No run with that id in this workspace.
404 unknown_tool That slug is not a tool this API exposes. GET /tools is the authoritative list.
409 tool_unavailable The tool exists but is not released yet.
409 working_file_busy Another run that writes the working file holds this publication. It clears when that run finishes — no Retry-After can name when, so poll the run.
409 run_not_finished The report was asked for before the run reached a terminal status.
409 run_not_succeeded The run is terminal but did not succeed, so there is no report. The run resource says why.
409 approval_required This run's result needs a person to compare before/after examples, which only the Origami app can show. Waiting or subscribing does not clear it.
409 idempotency_in_flight A request with the same Idempotency-Key is still being recorded. Retry in a moment.
409 reader_not_configured This deployment has no publica.la reader integration.
409 reader_sync_in_progress A push of the same variant to the reader store is already in flight.
409 downloads_unavailable The storage backend cannot mint presigned links. Only ever seen on local development storage.
422 publication_quota_exceeded The workspace has spent its publication allowance for the cycle. The detail says when it renews.
422 unsupported_format Not a book, source document or audiobook format Origami accepts.
422 invalid_options The options object is not valid for that tool.
402 plan_required The workspace has no active plan, so no tool run may start. Read endpoints keep working — which is why this is not a 403.
429 daily_limit_reached The workspace hit its daily fair-use limit on metered runs. Carries Retry-After.

Also over MCP

Origami speaks MCP (Model Context Protocol) at https://origami.publica.la/mcp over Streamable HTTP, so an agent can browse your workspace and run tools the same way this API does.

Connect with a workspace API key as a Bearer token, or via OAuth from clients that support it. Every tool mirrors one endpoint above — same service, same tenancy, same quotas — so an agent and an HTTP client see one product.

What you can do over MCP

Connect from Claude Code

claude mcp add --transport http origami https://origami.publica.la/mcp \
  --header "Authorization: Bearer ${ORIGAMI_API_KEY}"

Building an integration?

Tell us what you're automating — bulk ingestion, a DAM hook, a preflight pipeline — and we'll help you wire it.