The address your program calls, and its answers
Your program posts the description of one document to one address, under the API key of its account, with the files that description names beside it, and the finished PDF comes back in the answer.
The Call
One address answers, and it answers to a POST. The body of that POST is the description of the document you want. The key of your account travels in the Authorization header, as a bearer token.
What may be in a description — the page, the items drawn on it, the fonts, the standard it is written to — is the subject of the rest of the API section, which this page does not repeat.
| The call that creates a PDF | POST https://hqf-pdf.com/api/v1/render |
|---|---|
| The shape a description on its own is sent in | application/json |
| The shape a description naming files is sent in | multipart/form-data |
| The part the description travels in | json_data |
| Every other part | one file, under the name the description gives it |
| The largest body this address reads, files and all | 536 870 912 (512 MiB) |
| The font the service holds, which a description names and sends nothing for | DejaVuSans |
| How the API key travels | Authorization: Bearer <key> |
| What a success carries | 200 application/pdf |
| The name the file is offered under | Content-Disposition: attachment; filename="document.pdf" |
| How many pages the finished document has | x-hqf-pages: 4 |
| How many things the engine remarked on | x-hqf-warnings: 2 |
| The remarks themselves, comma-separated under one header name | x-hqf-warning: <value>,<value> |
| How many findings separate the document from the standards asked for | x-hqf-findings: 1 |
| The findings themselves, comma-separated under one header name | x-hqf-finding: <value>,<value> |
| What a refusal carries | application/json {"error": "…", "kind": "…"} |
| What a month with nothing left carries beside it | Retry-After: Sat, 05 Sep 2026 00:00:00 GMT |
What Travels Beside the Description
A description that stamps a template, draws a picture, calls for an electronic invoice or states a colour through a profile gives the name of a file, never its bytes. Those bytes travel beside the description, in the same call.
Such a call is sent as multipart/form-data: the description in the part named json_data, and one part per file. You name your files as you like; what ties a part to what it is for is the description, which says which name plays which role. Every part but the description's must carry a filename of its own: a part sent as plain text counts as text, not as a file.
A description naming a file the body carries none of is refused before anything is forwarded, and the refusal names the file. A description naming no file at all is posted as JSON, on its own.
The 536 870 912 bytes this address reads cover the whole body: the description and every file it names, weighed together.
The Same Call, Three Times Over
The three are answered on an account paying nothing. Their text is set in DejaVuSans, the font the service holds, and that is what lets them through: a description naming one of the fourteen fonts all reading software already carries is answered 402 under needs_a_licence until the account pays for a plan. The section under this one says what else such an account gets.
From a Shell
The body is the description itself. The answer is the PDF, so write it straight to a file.
curl -X POST https://hqf-pdf.com/api/v1/render \
-H "Authorization: Bearer $HQF_PDF_KEY" \
-H "Content-Type: application/json" \
--data-binary @invoice.json \
-o invoice.pdf
From Python
A refusal arrives as JSON carrying one sentence, and the status says whether the request is worth sending again.
import os
import pathlib
import requests
KEY = os.environ["HQF_PDF_KEY"]
description = {
"items": [
{
"type": "text",
"rect": {"llx": 56, "lly": 700, "urx": "{page_width} - 56", "ury": 780},
"content": ["Invoice 2026-014"],
"font": "DejaVuSans",
"font_size": 24,
}
],
}
answer = requests.post(
"https://hqf-pdf.com/api/v1/render",
headers={"Authorization": f"Bearer {KEY}"},
json=description,
timeout=120,
)
if answer.status_code != 200:
raise SystemExit(answer.json()["error"])
pathlib.Path("invoice.pdf").write_bytes(answer.content)
With a File Beside the Description
The description names its files; each one travels in a part of the same body, under exactly the name the description gives it. Here an electronic invoice names its XML `invoice_xml`, so the part carrying that XML is named `invoice_xml` too.
import json
import os
import pathlib
import requests
KEY = os.environ["HQF_PDF_KEY"]
description = {
"invoice": {
"xml": "invoice_xml",
"profile": "EN 16931",
"relationship": "alternative",
"issued": "2026-08-23T09:30:00+02:00",
},
"archive": "pdfa3b",
"items": [
{
"type": "text",
"rect": {"llx": 56, "lly": 700, "urx": "{page_width} - 56", "ury": 780},
"content": ["Invoice 2026-014"],
"font": "DejaVuSans",
"font_size": 24,
}
],
}
answer = requests.post(
"https://hqf-pdf.com/api/v1/render",
headers={"Authorization": f"Bearer {KEY}"},
files={
"json_data": ("description.json", json.dumps(description), "application/json"),
"invoice_xml": ("factur-x.xml", pathlib.Path("factur-x.xml").read_bytes()),
},
timeout=120,
)
if answer.status_code != 200:
raise SystemExit(answer.json()["error"])
pathlib.Path("invoice.pdf").write_bytes(answer.content)
What an Account Paying Nothing Gets
Every page comes out with a watermark: wording written across it. Under the watermark the pages are laid out exactly as a paid plan lays them out, with one difference: every letter is traced as a shape instead of being written as text. No text on the page can be selected, searched or copied; the file carries no font, and it is heavier than the same page written as text. Tracing a letter needs its glyph, so a page comes out only when every font its text is set in is either sent with your request or already held by the service. The editor on this site and the examples on the API page are set that way. Anything else needs a paid licence: the service creates no document, and its answer names what was asked for and says a licence key covers it.
These are the requests it answers 402 rather than writing:
- Text set in one of the fourteen fonts reading software already holds, in a font whose letters are glyphs the request itself describes, or in a type 1 program: none of the three gives a glyph to trace.
- A page taken from a document of your own that shows more than ten thousand words, or one whose words cannot be counted: such a page is carried in as it stands, and its text is text.
- A claim that the document meets the accessibility rules, or the archival level A, which rests on each mark on the page saying what it stands for: a page of traced shapes says nothing about its marks.
- A link or a bookmark leading to a piece of the document's own structure.
Every Answer, and What to Do about It
A refusal is JSON carrying one sentence and one word. The sentence says what is wrong, in the language of whoever reads it; the word, under kind, is what a program reads. It comes from the closed list below and is never translated.
The status alone does not say what to do: two refusals under one status can call for opposite things, and kind is what tells them apart. A word this page does not list comes from a server newer than this page, and is to be treated as the status says.
The one answer worth sending the same request again for at once is the one that says the server was never reached. A month with nothing left is worth sending again too, once the moment Retry-After names has come.
| Answer | The word under kind | What It Means | What to Do |
|---|---|---|---|
200 |
The PDF is the body, handed over as it is created, under the file name the header above names. Beside it stand the page count, what the engine remarked on, and what stands against the standards check asked for. |
Nothing to do. | |
400 |
not_json · no_description · no_such_part · bad_part_name · description · drawing · form
|
Either what was posted is not JSON at all, or the body carries no description, or the description names a file the body carries none of, or a part of the body is named something this address will not carry, or the server read the description and refused it. The word says which, and the sentence says what it found. | Correct the description. The same one posted twice gives the same answer. |
400 |
client_name · call_number · render_key
|
The server would not take the name, the number or the key this site writes on the call. None of the three is yours: the name is the one your API key carries, the number is made here, one per call, and the key is the one this site presents to the server it forwards to. | Nothing on your side, and sending the same request again changes nothing. Write to contact@hqf-pdf.com, quoting the moment of the call. |
401 |
no_key
|
The call carried no API key, or an API key nobody holds any more. | Present a live API key of the account. |
402 |
needs_a_licence
|
The request asked for something a licence key covers and the account has none. The sentence names what was asked for. | Change the request, or take out a licence. Sending the same one again changes nothing, whenever it is sent. |
405 |
wrong_method
|
The address was called with a method other than POST. | Post the description. The same address opened in a browser shows a page. |
411 |
no_body_length
|
The body states no length. A body sent in chunks, or with the header left off, reaches this address empty, so it is refused rather than read as an empty description. | State Content-Length, and send the body in one piece. |
413 |
body_too_large · too_many_pages
|
The body of the request runs past the 536 870 912 bytes this address reads, or the document it asks for runs past a ceiling of the server's. The word says which, and the sentence names the size. | Ask for less in one call. |
429 |
no_documents_left
|
The documents the month includes are all used. Retry-After carries the moment the month turns, as a date: the month is the one the account is billed on, running from the day it subscribed to the same day of the next month, rather than the calendar's. |
Wait for the moment Retry-After names and send the same request again, or move up a plan, which takes effect on the month already running. |
500 |
server
|
Writing the PDF did not come off, on the server's side. | Worth one more call. Tell us if it keeps happening. |
503 |
unreachable
|
The server could not be reached at all, or this site does not know where it is. Nothing was read of the description. | The one answer worth sending the same request again for. |
One more word can arrive under any of the numbers above: `other`. It means the server refused and said nothing of which sort, which is what a server older than the word itself does. Read the number, and tell us.
What Comes Back Beside the File
A success carries three counts. x-hqf-pages states how many pages the finished document has, and it is written down beside the render; the call counts as one document whatever it says. x-hqf-warnings states how many things the engine remarked on while writing. x-hqf-findings states how many findings stand between the finished document and the standards the check key of your description named. All three stand on every success, 0 included; an answer carrying none of them comes from a server older than they are.
The remarks themselves come under x-hqf-warning and the findings under x-hqf-finding. Each name is stated once, its values separated by a comma: cut on the comma first, then decode each piece — in that order, never the other way round. A comma inside a value stands as %2C, so the cut never falls inside one.
Every value is written the way a header carries it: every byte but a printable ASCII one, and the per cent sign itself, stands as %XX. Ten values at most under each of the two names, running to a thousand bytes together, whichever of the two is reached first; the value that runs past what is left is cut short and ends on %E2%80%A6. The counts state the whole number either way, so a customer reading x-hqf-warnings: 50 beside ten values still has the true count.
No answer states how long the work took, here or on a server of your own: the two durations the service measures go to its own log, and no header carries them back.
What a Call Costs
- One document is taken off the month before the description is forwarded.
- A render that never arrives gives its document back: a call answered 503 costs nothing.
- A call refused before anything is forwarded — no API key, a body that is not JSON, a description naming a file the body carries none of, a month with nothing left — costs nothing either.
- The pages written down are the ones the server states; this site does not open the document to count them again.
Trying It by Hand
The same address opened in a browser shows a box: paste a description into it and the document comes back on the spot. It creates the document for whoever is signed in, from the browser session rather than from an API key, and what it creates is not taken off the month.
It is the same address, the same description and the same server, so what you try by hand is what your program will send. The box carries the description alone, though: there is nowhere on the page to put a file, so a description naming one is tried from your own program.
Where to Go Next
The format of a description Your account: its licence keys and its invoices What becomes of a document you send