spool2pdf.

Quick start

The body is the document. Options go in the query string.

curl --data-binary @report.txt \
  'https://api.spool2pdf.app/api/render?paper=A4&pointSize=9' \
  -H 'X-Api-Key: YOUR_KEY' \
  -o report.pdf

To add a letterhead, switch to multipart so you can attach it:

curl -X POST https://api.spool2pdf.app/api/render \
  -H 'X-Api-Key: YOUR_KEY' \
  -F text=@report.txt -F canvas=@letterhead.pdf \
  -F encoding=cp850 -F pointSize=9 \
  -o report.pdf

Endpoints

MethodPathAuthReturns
POST/api/renderAPI key The PDF, or a JSON envelope on request
GET/api/healthNone {"status":"ok","version":"..."}
GET/api/optionsNone Every option and its default, generated from the running code

The render response carries X-Pdf-Pages and X-Pdf-Bytes. Add ?response=json, or send Accept: application/json, to get {pdfBase64, pages, bytes, width, height} instead. A query parameter beats the header if both are present.

Sending a document

Raw body

Any content type other than JSON or multipart. The body is the document and every option comes from the query string. This is the mode to use from a shell, because it makes the service a pipe.

acctrpt -m 08 | curl --data-binary @- '.../render?pointSize=9' -o out.pdf

multipart/form-data

File parts: text (also accepted as document or file), canvas (or background) and font. Options are ordinary form fields. text may also be sent as a form field rather than a file.

application/json

For platforms that cannot post raw bytes. Options may sit at the top level or nested under options; both work.

{
  "text": "already-decoded string",
  // or, for raw bytes in a legacy code page:
  "textBase64": "<base64 of the file>",
  "canvasBase64": "<base64 of the letterhead PDF>",
  "fontBase64": "<base64 of a monospace TTF>",
  "options": { "paper": "A4", "encoding": "cp850" }
}

Send raw text in text, or base64 of the raw bytes in textBase64, never base64 of base64. Some connector platforms re-encode anything placed in a byte field, so pass file content straight through rather than encoding it yourself first. Doubly encoded input is detected and rejected rather than rendered as one unreadable line.

All options

Every option is optional. Names resolve case-insensitively, so pointSize, pointsize and POINTSIZE are the same option. An unrecognised name is an error rather than something quietly ignored. GET /api/options returns this table generated from the running code.

Page

OptionDefaultMeaning
paperA4A3, A4, A5, LETTER, LEGAL, TABLOID, LEDGER
landscape0Swap width and height
linesautoLines per page. Omitted, it fits to the page height
leftMargin25Left origin, in points
topMargin40Top margin, in points

Type

OptionDefaultMeaning
fontCourierCourier, Helvetica or Times. Ignored if a font file is supplied
bold0Bold weight of the chosen base font
pointSize10Type size, in points
vertSpace12Leading, baseline to baseline, in points
tab8Columns per tab stop

Text handling

OptionDefaultMeaning
encodinglatin-1Code page used to decode uploaded bytes
stripControl1Remove embedded printer control codes
skipLines0Blank the first N lines of the document, in place
skipEachPage0Blank the first N lines of every page, in place
trimTrailingBlanks1Drop trailing whitespace-only lines

Pagination

OptionDefaultMeaning
formFeeds1Treat 0x0c as a page break
splitOnStart a new page at every line carrying this marker
splitRegex0Treat splitOn as a regular expression
trimBlankPages1Drop wholly blank pages from each end of the document
pagesallall, first, last, 3, 2-4, 3-, -2, 1,3,5-7

Canvas, properties and security

OptionDefaultMeaning
canvasModeautoauto or cycle, see below
title author subject keywords creatorDocument properties, written to both DocInfo and XMP
userPasswordRequired to open the document
ownerPasswordRequired to change permissions
allowPrint allowCopy allowEdit allowAnnotate1Set any to 0 to withhold that permission

Geometry

Text is placed on a fixed grid rather than flowed. The first baseline sits at

y = pageHeight - topMargin - vertSpace

measured in PDF points, with the left origin at leftMargin, a monospace advance across the line and vertSpace between baselines. Every value is in points, so a report tuned once keeps its registration on any paper size that shares the same origin.

To match an existing document, start from its point size and leading, then adjust the margins until an overlay of the two lines up. Rendering the new output into one colour channel and the original into another makes any drift obvious: where they agree the result is neutral, where they do not you get a coloured fringe.

The canvas

The canvas is the letterhead or pre-printed form the text is laid over. Supply it as a PDF. It is placed at its own size, never rescaled to fit, because scaling a letterhead by a fraction of a percent is exactly the drift that shows up as misregistration against a pre-printed design.

A multi-page canvas maps onto the output according to canvasMode:

ModeBehaviour
auto Page N takes canvas page N, and the last canvas page repeats for everything beyond it. A one-page canvas therefore backs the whole document, and a two-page canvas gives letterhead then continuation stationery with nothing to configure.
cycle Canvas pages repeat in a loop, for multi-part stationery where the sequence itself matters.

The canvas is stored once and referenced from each page, so output size stays flat however many pages the run produces.

Page selection uses original page numbers rather than positions in the output, so rendering a document in slices and merging the slices downstream produces the same result as rendering it in one call.

Encoding

Legacy print streams are single-byte. cp850 is usual in Europe and cp437 in the US; both carry box-drawing characters and currency symbols at byte values that mean something else in Latin-1 and are invalid in UTF-8. Set encoding to the code page the system actually emits. Any codec Python knows is accepted.

Decoding is strict. A wrong code page produces an error naming the offending byte and its offset, rather than scattering replacement characters through an invoice where nobody notices until a customer does.

The option applies to bytes. If you send text as a JSON string it has already been decoded by your client, and the option has nothing left to do. To hand over raw code-page bytes, use textBase64 or a multipart file part.

Pagination

Pages break on form feeds, on a content marker, or on line count, and all three can apply at once.

Form feeds. 0x0c starts a new page. An eject at the very start or end of the file is treated as an instruction to move the paper rather than as a page, so a file that opens by ejecting to top of form does not gain a blank sheet in front of it. Blank pages inside the document are kept, because those are usually deliberate. Set trimBlankPages=0 to keep every one.

Markers. splitOn starts a new page at every line containing the marker, and that line becomes the first line of the new page, because that is what these markers are. Matching is a substring test by default, since a marker rarely sits alone at column 0. Set splitRegex=1 for a regular expression, which is searched rather than anchored, so anchor it with ^ yourself if you mean to.

Blanking without moving. skipLines and skipEachPage blank lines in place rather than deleting them, so everything below keeps its row on the grid. That distinction matters on a positional form, and it means hiding a marker line does not change where pages begin.

Fonts

The built-in fonts are Courier, Helvetica and Times, which cover Latin-1 and nothing else. Attach a monospace TTF or OTF as the font part to go further: it is embedded and subsetted, which adds roughly 25KB once, and unlocks box-drawing characters and full Unicode.

It must be monospace, or columns will not align. Its weight is whatever the file carries, so bold is ignored when a font file is supplied.

Security

Encryption is AES-256 only. The 40-bit and 128-bit RC4 modes older tools still offer are broken, and PDF 2.0 removed both, so there is no option to select them.

Setting a password or withholding any permission engages encryption. A userPassword is required to open the document; an ownerPassword is required to change its permissions.

If you restrict a document without supplying an ownerPassword, one is generated and discarded. An empty owner password lets any viewer lift every restriction with a single click, which is how this feature usually ships. Supply your own if you need to unlock the document later.

Extraction by assistive technology is always permitted and is not exposed as an option. Withholding it helps nobody who can already read the page, and PDF 2.0 deprecates the bit.

Errors

Errors are JSON, with a stable machine-readable code.

{
  "error": {
    "code": "bad_encoding",
    "message": "document is not valid utf-8: byte 0x9c at offset 6. Legacy spool files are usually cp850 or cp437 rather than utf-8; set the 'encoding' option to match."
  }
}
CodeStatusMeaning
empty_body400No document was sent
no_document400The request had options but no text
bad_json400The body is not a JSON object
bad_type400text was not a string
bad_base64400A base64 field would not decode
double_encoded400The document is base64 that was encoded twice
bad_encoding400Unknown code page, or the text is not valid in it
unknown_option400An option name that does not exist. The reply lists the valid ones
bad_option400A valid option with an unusable value
too_large413Document, canvas or font over the size limit
render_failed500Something unexpected. Nothing useful is leaked; the detail is logged