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.

Plate 0 the whole surface
routewhat 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>
Keep it server-side

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

Plate 1 POST/api/convert?to=<target>

The document in, the artifact out, in one call.

you sendform
the bytesraw body with Content-Type: application/pdf
or a formmultipart/form-data, field named file
the target?to=epub — see plate 8
optionsany 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.

The limit is the wait, not the size

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

Plate 2 POST/api/jobs?to=<target>

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"}
Plate 3 GET/api/jobs?uid=… or ?src=…

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"}
statemeaning
QUEUEDaccepted, waiting for a worker
RUNNINGconverting; percent is moving
DONEthe artifact is ready
FAILEDthe conversion threw; error says what
STALEit stopped writing and was given up on
Plate 4 GET/api/jobs/fetch?uid=…

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.

A restart does not lose your job

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 itdir/doc.pdf becomes dir/doc.epub, which is what keeps a bucket readable months later.

Plate 5 POST/api/jobs?to=…&src=s3://bucket/key

We hold the credentials on your bucket.

parameterwhat it does
srcthe source object; no request body
outwhere to write it; omit and it lands beside the source
metaa metadata sidecar to apply first
notifycalled when the job ends — see plate 7
Plate 6 POST/api/jobs?src=<signed GET>&out=<signed PUT>

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.

Three things to know before you sign
  • out becomes mandatory: a signature covers one method on one exact key, so no destination can be derived from a source. A signed src without out is refused at once with a 400, 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

Plate 7 GET<your url>?file=…&uid=…[&error=…]

What we call when a job ends, if you passed &notify= or &notify_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:

targetwhat it is
epubEPUB 3 fixed-layout — the page as it looks, with real selectable text. The Replica.
epub-reflowEPUB 3 reflowable — built from the document's structure, the reader chooses the typography.
pdfa normalized PDF: repaired fonts, regular structure. Add &selectable=true for a real text layer.
htmlsemantic HTML5 from the structure tree.
mdclean Markdown — for indexing, search, LLM ingestion.
doctagsDocling DocTags, with position boxes. For grounded LLM/RAG use.
svgone page as SVG; pick it with &page=0.
ocdthe working format: a valid EPUB that is also the lossless model. Send it back later and it is re-exported, not re-imported.
auditJSON: is this PDF really redacted? What is still under the black boxes.
stagesJSON: the geometry of each analysis stage. A diagnostic.
zonesJSON: where a regular expression matches, as rectangles. Pair it with &match= — it is how you find what to redact before you redact it.
patcha 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.

keyeffect
selectablePDF target: embed fonts and place real text. Off = a pixel-exact facsimile.
pageSVG target: which page, 0-based.
navPerPageEPUB: a navigation entry for every page, even one with no heading.
pagesthe page set as a result: 1,4-6,2 keeps those, in that order — omission deletes, repetition duplicates.
redactaudit, 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.
gridDocTags: the integer grid the position boxes quantize onto.
An unknown key is ignored in silence

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'"}
codewhen
400an unknown target, a bad option, no document in the request, a signed src with no out
401missing or invalid key
404no such route, or no such job
405right route, wrong method (/api/convert is a POST)
409the job is not ready, or its artifact went to your store — the message says where
413the body is over the limit
500the 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.

beforenow
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 &notify 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 answersJSON, on every route.
numeric statuses QUEUED / RUNNING / DONE / FAILED / STALE plus an error message. Branch on state, or on failed.
the .log beside the EPUBUnchanged.
the artifact beside the PDFUnchanged — 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.

Two things got better on the way

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.