v1 · async render API

md2gost.

A quiet little REST API that turns GOST-style Markdown (*.g.md) into polished PDF or DOCX files — upload a ZIP, poll, download.

↓ demo_client.py dependency-free demo client — run with python3 demo_client.py

01Basics

Every job request needs an API key. Send it as a bearer token:

Authorization: Bearer 

Keys are scoped: a job can only be read, downloaded, or cancelled by the key that created it. Requests are rate-limited per key (X-RateLimit-Limit / X-RateLimit-Remaining headers).

Endpoints

GET/v1/jobs/:id

Job status & progress

GET/v1/jobs/:id/result

Download the rendered file

DELETE/v1/jobs/:id

Cancel a pending job

Job lifecycle

pending queued processing completed failed cancelled

Poll GET /v1/jobs/:id until a terminal state (completed, failed, cancelled). Results are retained for a limited time (default 7 days).

02The archive

Upload your project as a ZIP. It must contain a root document — either a file named main.g.md, or exactly one *.g.md file. Other files (images, fragments, fonts) are assets referenced by that document and are bundled inside the archive.

project.zip
├── main.g.md      ← root document
├── img/logo.png
└── sections/part.g.md

03Create a job

POST/v1/jobs?format=pdf
  • format — pdf (default) or docx
  • Idempotency-Key header — required, unique, 1–255 chars of A-Za-z0-9_-
  • Body — multipart/form-data with a file field containing the ZIP
curl -X POST \
  -H "Authorization: Bearer $KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -F "file=@project.zip" \
  "http://localhost:3000/v1/jobs?format=pdf"
{
  "id": "3f2a9e4c-...",
  "status": "queued",
  "statusUrl": "/v1/jobs/3f2a9e4c-...",
  "resultUrl": "/v1/jobs/3f2a9e4c-.../result"
}

Reusing the same Idempotency-Key returns the existing job instead of duplicating it. Reusing it with different data returns 409.

04Check status

GET/v1/jobs/:id
{
  "id": "3f2a9e4c-...",
  "status": "processing",
  "format": "pdf",
  "progress": { "percent": 44, "stage": "rendering", "message": "..." },
  "warnings": null,
  "createdAt": "2026-09-04T18:00:00.000Z",
  "finishedAt": null,
  "errorCode": null,
  "error": null
}

While processing, progress reports percent, stage, and a message. On failure, errorCode is machine-readable and error is a human-friendly explanation.

05Download the result

GET/v1/jobs/:id/result
curl -OJ \
  -H "Authorization: Bearer $KEY" \
  "http://localhost:3000/v1/jobs/$JOB/result"
StatusMeaning
200File bytes (PDF or DOCX)
409Not ready yet
422Job failed — no result
404Unknown job

06Cancel a job

DELETE/v1/jobs/:id

Cancels a pending or queued job. Once processing has started, cancellation is rejected with 409 job_already_processing.

07Errors

Errors are JSON with an error code, and often a message. Below are the most common ones.

CodeStatusMeaning
missing_api_key401No bearer token
invalid_api_key401Unknown or revoked key
rate_limit_exceeded429Per-key request limit
invalid_idempotency_key400Header missing or malformed
idempotency_key_reused409Key reused with different data
invalid_format400Use pdf or docx
invalid_file_type400Must be a ZIP archive
file_too_large413ZIP exceeds max size
job_quota_exceeded429Your job limit is full
global_job_limit_exceeded429Service queue is full
job_not_found404No such job for this key
result_not_ready409Job hasn’t finished
render_failed422Job failed, no result
job_already_processing409Cannot cancel a running job

Async errors

Reported in the errorCode of a failed job:

CodeMeaning
archiveCorrupt, protected, or unsupported archive
archive:no_mainNo root main.g.md found
renderUnexpected rendering error
render:bad_inputSyntax errors or unsupported features
render:timeoutRendering timed out
output_too_largeOutput exceeds max size
timeoutOverall job timeout