Docs
API reference
DocRelay is preparing for launch. The specification may change before launch. The base URL is https://api.docrelay.jp.
Issuing documents
Issue documents with POST /v1/renders. If the document is ready within wait seconds (0-25, default 25), it is returned immediately (200). Otherwise you receive 202 with a Render, and a webhook on completion. Requests with wait set to 0 and batches of more than 50 documents are processed asynchronously.
curl https://api.docrelay.jp/v1/renders \
-H "Authorization: Bearer $DOCRELAY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: inv_88" \
-d '{
"template": "invoice",
"format": "pdf",
"data": { "customer": { "name": "Acme Inc." }, "total": 128000 },
"wait": 10
}' \
-o invoice.pdfconst res = await fetch('https://api.docrelay.jp/v1/renders', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.DOCRELAY_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': 'inv_88',
},
body: JSON.stringify({
template: 'invoice',
format: 'pdf',
data: { customer: { name: 'Acme Inc.' }, total: 128000 },
wait: 10,
}),
});
if (res.status === 200) {
const pdf = Buffer.from(await res.arrayBuffer()); // x-render-id and x-content-sha256 headers are included
} else if (res.status === 202) {
const render = await res.json(); // { id: 'rnd_…', status: 'queued', … } → render.succeeded webhook on completion
}| Method | Path | Description |
|---|---|---|
| POST | / | Issue a document. The document itself (200) if ready; a Render with Accept: application/ or link. 202 with a Render if not ready |
| GET | / | Issuance history (filter by template_id, status, since; newest first) |
| GET | / | One Render |
| GET | / | The document. 410 gone after the retention period, 409 if not yet complete |
| POST | / | Create an expiring URL (expires_in, max_downloads; defaults to 24 hours with no download limit) |
| GET | / | Download through an expiring URL (no API key). 404 once expired or over the limit, 410 after the document is deleted at the end of its retention period |
type RenderRequest = {
template: string; // template ID or name
version?: number; // default: published (production) / latest (test)
format?: 'pdf' | 'xlsx' | 'docx' | 'csv'; // default: the first format
data?: object; // one document
items?: object[]; // 1-1,000 documents, returned as a zip
merge?: boolean; // layouts only: merge into one PDF
wait?: number; // seconds, 0-25 (default 25)
keep_for?: number; // retention period in seconds (default 86,400)
link?: true | { expires_in?: number; max_downloads?: number }; // expiring URL
metadata?: Record<string, string>; // up to 20, also sent in webhooks
csv?: { // how to write CSV (format csv only)
newline?: 'crlf' | 'lf'; // default crlf
trailing_newline?: boolean; // newline after the last row (default true)
bom?: boolean; // UTF-8 BOM (default true)
encoding?: 'utf-8' | 'shift_jis'; // default utf-8
quote?: 'minimal' | 'all'; // minimal quotes only when needed, all quotes every value
};
};
type Render = {
id: string; // rnd_…
status: 'queued' | 'rendering' | 'succeeded' | 'failed' | 'expired';
template_id: string; version: number; format: string;
filename: string; size: number | null; sha256: string | null; item_count: number;
error: { code: string; message: string } | null;
ledger: { seq: number } | null;
link: { url: string; expires_at: number } | null;
metadata: Record<string, string>;
created_at: number; completed_at: number | null; expires_at: number | null;
};- Headers on a direct response:
content-type,content-disposition(non-ASCII file names supported),x-render-id,x-content-sha256. - With the optional
Idempotency-Keyheader, a retry with the same key within 24 hours returns the same result. - Request bodies are limited to 5MB.
- Each document issued in production counts toward your monthly volume (batches count each document). Test issuance and previews are not counted.
- Responses include
Server-Timingwith the milliseconds spent in each stage (authentication, template loading, fonts, rendering and more). SendX-DocRelay-Timing: cputo also receive the CPU time of rendering ascpu.
Templates
| Method | Path | Description |
|---|---|---|
| GET | / | List templates |
| POST | / | Create a layout template with JSON { name, layout? } (a blank A4 page if layout is omitted), or upload a multipart file (xlsx or docx) with name |
| GET / PATCH / DELETE | / | Get, update (name, file name, sample data) or delete. :id accepts an ID or template name. Issuance history and the tamper-evident log are kept after deletion |
| PUT | / | Save a layout. Overwrites the latest version if it is a draft; otherwise creates a new draft version |
| POST | / | Upload an xlsx or docx of the same type to create a new draft version |
| POST | / | Publish a version. Production API keys use it from then on |
| POST | / | Preview. Uses the sample data if data is omitted. Not recorded in issuance history and not counted toward your monthly volume. Missing merge fields are returned in the x-missing-fields header |
Layouts
Layouts designed in the template editor are JSON documents, saved with PUT /v1/templates/:id/layout. Lengths are in millimeters, with the origin at the top left of the page. The MCP tool get_layout_guide returns the same guide with samples.
| Element | Description |
|---|---|
text | Text with wrapping, shrinking or clipping, borders and fills, rotate and opacity |
line | Lines (width, color, dashes) |
rect | Rectangles (stroke, fill, rounded corners) |
image | Images (data: or https URLs, which can also come from merge fields) |
table | Line-item table. Adds a row per item in a data array and continues onto the next page, repeating the header |
flow | Body text. Starts in its frame and flows onto the following pages. Content is Markdown or data blocks (headings, paragraphs, lists, tables, rules and page breaks) |
Line-item tables and tables in body text
wrap: truewraps cell text to the column width and sizes each row to its tallest cell. A row that does not fit is split by lines within its cells.- Row data fields
_span(span columns),_alignand_boldchange the look of individual rows. - A column’s
richfield ([{ text, bold?, color?, underline? }]) makes part of the text bold, colored or underlined. Markdown body text supports**bold**and<u>underline</u>. - Set
headerBold,padding(cell padding) andlineHeight(line height within cells). - Cells of tables in body text can contain paragraphs, lists and tables, nested one level (data blocks only).
Other options
follow: { id, gap? }: places elements such as totals right after the actual end of a table or body text, moving them to the next page if they do not fit.repeat: draws elements other than tables and body text on the first page, every page or the last page.watermark: { text, size?, color?, opacity?, angle?, tile?, gap? }: overlays semi-transparent text on every page.tile: truerepeats it across the page.- Merge fields:
{{a.b}},{{x | number}},{{d | date:YYYY-MM-DD}},{{#list}}…{{/list}},{{^x}}…{{/x}},{{_page}}and{{_pages}}. - Invalid layouts return
422 invalid, with a description per field such aselements[3].winfields.
Completion webhooks
There are two events: render.succeeded and render.failed. The body is { type, data: Render }. How to register endpoints will be announced at launch.
{
"type": "render.succeeded",
"data": {
"id": "rnd_…", "status": "succeeded", "filename": "invoice_1024.pdf",
"sha256": "a3f9…", "item_count": 1, "link": null, …
}
}Retention period
Set the retention period in seconds with keep_for. The default is 86,400 seconds (24 hours), and the maximum is your plan’s retention period. After the retention period, the document is deleted and only the issuance record (hash, size and file name) and the tamper-evident log are kept.
Tamper-evident log
Tamper detection that applies blockchain technology. Each issued document’s SHA-256, render ID, template, version and time are chained to the previous entry; the data you merge is never recorded. A daily hash (per UTC day) is published.
| Method | Path | Description |
|---|---|---|
| GET | / | The log entry for a document and the chain up to that day’s daily hash (head is null until the day is closed) |
| POST | / | Send a document file or { sha256 } to verify whether it was issued in this environment |
| GET | / | Published daily hashes (no API key; the last 30 days if date is omitted) |
type LedgerEntry = {
seq: number; // sequence number
render_id: string; sha256: string; template_id: string; version: number;
at: number;
prev_hash: string; // entry_hash of the previous entry
// SHA-256 of prev_hash, seq, sha256, render_id, at joined by "\n"
entry_hash: string;
};
type DailyHead = { date: string; seq: number; entry_hash: string; published_at: number };API key scopes
| Scope | Allows |
|---|---|
templates:read | Get and preview templates |
templates:write | Create, update, delete and publish templates |
renders:create | Issue documents |
renders:read | Get issuance history, document files and expiring URLs; get and verify the tamper-evident log |
GET /v1/whoami can be called with any scope. It returns the API key’s environment, project and organization.
Rate limits and errors
- Rate limits apply per environment, and the per-minute limit depends on your plan (Free 60, Starter 300, Pro 1,200, Business 3,000). Every response includes
x-ratelimit-limit,x-ratelimit-remainingandx-ratelimit-reset. Over the limit, the API returns429 rate_limitedwithretry-after. - Errors are returned as
{ error, message, fields?, request_id }. Input errors return422 invalid, with a description per field infields. - Failed requests can be reviewed in the API logs in the dashboard for 30 days.