sugarcloud.ch · HTTP API · for consumers of the service
Converting documents over HTTP
A PDF goes in. An EPUB, an HTML page, Markdown, a PDF or the OCD working format comes out. Server to server, one key, seven routes — and only two of them do the work.
Service sugarcloud.chEngine jexter 1.0.0 Authority GET /api/targetsContact contact [at] sugarcube.ch
01What it is
One HTTP front over the jexter document engine. Everything the engine can produce, it produces here — a target added to the engine appears on this API the same day, with nothing to wire on either side.
Seven routes, set out as plates below. Each plate has the same anatomy: the signature, one sentence of purpose, then what you can pass and what comes back. Learn it once, read any of them at a glance.
| route | what it does |
|---|---|
GET /api/health |
is it up — the only route with no key |
GET /api/targets |
what this deployment can produce |
GET /api/options |
the engine's option registry, with defaults |
POST /api/convert |
the work, synchronously — one call, the bytes back |
POST /api/jobs |
the work, asynchronously — submit and get an identifier |
GET /api/jobs |
where a job stands, by uid or by src |
GET /api/jobs/fetch |
the finished artifact |
The difference between the two that matter is only how long you are willing to hold a connection open. Below a few dozen pages, convert. Above, submit a job: a 1000-page book takes about 40 seconds, which no proxy or load balancer will hold.
02Your key
Every route needs it except /api/health, which stays open so monitoring can
reach it. Two header forms, both accepted:
Authorization: Bearer <key>
X-API-Key: <key>
The key is shown once, when it is minted, and only its hash is stored here — we cannot read it back to you. Lose it and we mint another; it takes a second, and the old one is revoked. This API is not meant to be called from a browser, and a key in a page is a key given away.
03Five minutes
Is it up?
curl https://sugarcloud.ch/api/health
ok
Turn a PDF into an EPUB and save it:
curl -H "Authorization: Bearer $KEY" \
-F file=@book.pdf \
"https://sugarcloud.ch/api/convert?to=epub" -o book.epub
The same book, as Markdown for an index or a search engine — change one word:
"https://sugarcloud.ch/api/convert?to=md" -o book.md
That is the whole API for a small document. The rest of this page is for big ones, for documents that already live in a bucket, and for knowing what went wrong.
04Convert, now
The document in, the artifact out, in one call.
| you send | form |
|---|---|
| the bytes | raw body with Content-Type: application/pdf |
| or a form | multipart/form-data, field named file |
| the target | ?to=epub — see plate 8 |
| options | any key of the registry, &selectable=true — see plate 9 |
HTTP/1.1 200 OK
Content-Type: application/epub+zip
Content-Disposition: attachment; filename="book.epub"
Both upload forms give byte-identical results. You may also send an
.ocd.epub instead of a PDF: it is then re-exported rather than re-imported,
which is faster and lossless. The input is read, never declared — we look at the first
bytes (%PDF or PK), so the file name and the content type do not
have to be right.
Nothing caps a document at a page count, but a conversion that runs longer than your client, your proxy or your load balancer will hold is a conversion you never receive.
05Convert, as a job
The same call, when the conversion outlives the request. Answers at once with an identifier.
UID=$(curl -s -H "X-API-Key: $KEY" -H "Content-Type: application/pdf" \
--data-binary @book.pdf "https://sugarcloud.ch/api/jobs?to=epub" | jq -r .uid)
202 {"uid":"ffc0…","state":"QUEUED"}
Where it stands. Poll every second or two; there is no rate limit on it.
{"uid":"ffc0…","state":"RUNNING","percent":42,"failed":false}
{"uid":"ffc0…","state":"DONE","percent":100,"failed":false,"artifact":"book.epub"}
| state | meaning |
|---|---|
QUEUED | accepted, waiting for a worker |
RUNNING | converting; percent is moving |
DONE | the artifact is ready |
FAILED | the conversion threw; error says what |
STALE | it stopped writing and was given up on |
The artifact itself — for jobs whose bytes you posted. A job that named a store
gets 409 here, and the message says where the file went.
If this service is redeployed mid-conversion, the job goes back in the queue at boot with the options you asked for. You keep polling the same uid and nothing else changes. Artifacts and records are kept for a week, then swept.
06Working from S3
If the PDFs already live in a bucket, do not move them through us twice. Name the source and
the artifact goes back beside it — dir/doc.pdf becomes
dir/doc.epub, which is what keeps a bucket readable months later.
We hold the credentials on your bucket.
| parameter | what it does |
|---|---|
src | the source object; no request body |
out | where to write it; omit and it lands beside the source |
meta | a metadata sidecar to apply first |
notify | called when the job ends — see plate 7 |
You keep the credentials. We speak plain HTTP and hold nothing at all.
S3Presigner p = S3Presigner.create();
String src = p.presignGetObject(r -> r.signatureDuration(Duration.ofHours(2))
.getObjectRequest(o -> o.bucket("my-bucket").key("dir/doc.pdf"))).url().toString();
String out = p.presignPutObject(r -> r.signatureDuration(Duration.ofHours(2))
.putObjectRequest(o -> o.bucket("my-bucket").key("dir/doc.epub"))).url().toString();
Three lines with the SDK you already use to put the PDF there — no bucket policy, no role to create. And because a signed URL is no longer S3, the same call works against Azure, GCS, MinIO or a plain web server.
outbecomes mandatory: a signature covers one method on one exact key, so no destination can be derived from a source. A signedsrcwithoutoutis refused at once with a400, not twenty seconds later with a 403.- Sign for an hour, not five minutes — your job may wait in the queue, and a stale
signature comes back as
PUT refused with HTTP 403. - The run's log needs its own signed PUT (
&log=…), for the same reason.
The metadata sidecar
Point &meta= at a small file beside the PDF — key=value lines
or a flat JSON object — and its fields land on the document before it is projected, so they
reach the EPUB's Dublin Core:
title=The Alpine Guide
creator=Jean Dupont, Sugarcube
language=fr-CH
keywords=alpes, randonnee, guide
publisher=MyLiveBook
Known keys are title, subject, language,
created, modified, producer, and the multi-valued
creator and keywords. Anything else is kept rather than
dropped. The sidecar overrides what the PDF declared: it is your deliberate statement
about the document.
07Callbacks and the log
What we call when a job ends, if you passed ¬ify= or
¬ify_error=.
Best effort by design: if your endpoint is down, a finished conversion does not become
a failed one — the outcome goes to the job's log instead. For a job with a store source that
log is uploaded beside the artifact, doc.epub next to doc.log.
It is the only way to find out why a conversion came out the way it did without asking
us.
08Targets
GET /api/targets is the
authority — it lists what this deployment can produce today. At the time of writing:
| target | what it is |
|---|---|
epub | EPUB 3 fixed-layout — the page as it looks, with real selectable text. The Replica. |
epub-reflow | EPUB 3 reflowable — built from the document's structure, the reader chooses the typography. |
pdf | a normalized PDF: repaired fonts, regular structure. Add
&selectable=true for a real text layer. |
html | semantic HTML5 from the structure tree. |
md | clean Markdown — for indexing, search, LLM ingestion. |
doctags | Docling DocTags, with position boxes. For grounded LLM/RAG use. |
svg | one page as SVG; pick it with &page=0. |
ocd | the working format: a valid EPUB that is also the lossless model. Send it back later and it is re-exported, not re-imported. |
audit | JSON: is this PDF really redacted? What is still under the black boxes. |
stages | JSON: the geometry of each analysis stage. A diagnostic. |
zones | JSON: where a regular expression matches, as
rectangles. Pair it with &match= — it is how you find what to redact
before you redact it. |
patch | a zip of only the members a redaction changed. For a
consumer holding the .ocd already: apply the patch instead of moving the
whole document twice. |
09Options
GET /api/options returns the registry itself — key, label, help, type and
default — so a UI can build itself and a client never guesses. Every query parameter other
than the ones named on this page is passed straight to the engine.
| key | effect |
|---|---|
selectable | PDF target: embed fonts and place real text. Off = a pixel-exact facsimile. |
page | SVG target: which page, 0-based. |
navPerPage | EPUB: a navigation entry for every page, even one with no heading. |
pages | the page set as a result: 1,4-6,2
keeps those, in that order — omission deletes, repetition duplicates. |
redact | audit, or an explicit
page:x,y,w,h;… list. What is under a zone is removed, not covered,
and the engine refuses to write unless its own audit of the result is clean. |
grid | DocTags: the integer grid the position boxes quantize onto. |
Nothing refuses it, and nothing happens. Spell it exactly as /api/options
gives it — renderAnnotations, not annotations.
10Errors
One shape, on every route:
{"error":"unknown target 'epub3'"}
| code | when |
|---|---|
400 | an unknown target, a bad option, no document
in the request, a signed src with no out |
401 | missing or invalid key |
404 | no such route, or no such job |
405 | right route, wrong method
(/api/convert is a POST) |
409 | the job is not ready, or its artifact went to your store — the message says where |
413 | the body is over the limit |
500 | the conversion threw; the message names the exception |
A failed job carries the same text in its error field, and its log holds the
rest.
11Migrating from ReplicaCloudServlet
The old service did the same work with a different vocabulary. Everything it did, this does.
| before | now |
|---|---|
action=login |
Gone. One API key per client, in a header. No session, nothing to expire. |
action=convert |
POST /api/jobs?to=epub&src=s3://bucket/pdf — the bucket is part of
the URI. (The old bucket parameter was read and never used.) |
&meta ¬ify |
Same names, same meaning, same GET callback. |
action=get_status |
GET /api/jobs?src=s3://bucket/pdf — poll by the source you uploaded, as
before. The answer also carries the uid. |
action=get_percent |
The same call: percent is a field of the answer. |
| XML answers | JSON, on every route. |
| numeric statuses | QUEUED / RUNNING / DONE / FAILED / STALE plus an error
message. Branch on state, or on failed. |
the .log beside the EPUB | Unchanged. |
| the artifact beside the PDF | Unchanged — same name, same folder. |
A minimal migration: drop the login call, keep your key in a header, replace one URL, and
read state instead of an integer. Polling by file name keeps working.
The conversion engine is a new one — same model, rebuilt — so the EPUB is not merely the old Replica: fonts are repaired, text is real, and the structure is recovered rather than guessed. And a job survives a redeploy instead of being lost.