Overview
PrysM Tools generates finished, print-ready PDFs from JSON. One endpoint covers all four business documents — the type field picks the template. Everything else about the payload is shared, so adding a second document type to your integration is a one-word change.
invoiceInvoice
Priced lines, tax, totals, payment details.
quoteQuote
Same as an invoice, with a validity date.
credit_noteCredit note
A refund. Amounts represent money going back.
delivery_noteDelivery note
Goods received. No prices, no payment.
https://prysm.tools/api/v1/documents/generateThe PDF comes back as base64 by default, or stored in your PrysM Cloud with a signed link. Text is vector — searchable and selectable, not a picture of a document.
Authentication
Every request carries a secret key as a bearer token. Keys are created in your API dashboard and shown once; only a SHA-256 digest is stored, so a lost key is replaced rather than recovered.
Authorization: Bearer prysm_sk_live_8f3a2b1c94ed07bb215af6390c4d81e7
Content-Type: application/jsonTreat a key like a password
Anyone holding it can generate documents billed to your account. Keep it server-side — never in a browser bundle, a mobile app, or a public repository. Revoke a leaked key from the dashboard; it stops working on the next request.
Quickstart
A complete, working invoice. Replace the key and run it — the response contains a base64 PDF you can write straight to disk.
curl -X POST https://prysm.tools/api/v1/documents/generate \
-H "Authorization: Bearer prysm_sk_live_…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"type": "invoice",
"document": {
"number": "INV-2026-0042",
"issued_on": "2026-09-19",
"due_on": "2026-10-19",
"currency": "EUR",
"language": "en",
"tax_label": "VAT"
},
"sender": {
"name": "Meridian Studio Ltd",
"address": "18 Fontaine Street",
"postal_code": "75011",
"city": "Paris",
"country": "France",
"tax_id": "FR62902471338",
"legal_form": "SASU au capital de 1 000 EUR"
},
"recipient": {
"name": "Northwind Labs Inc.",
"address": "4 Opera Square",
"postal_code": "69001",
"city": "Lyon",
"country": "France"
},
"line_items": [
{
"name": "Brand identity redesign",
"description": "Logotype, palette and usage guidelines.",
"quantity": 1,
"unit_price": 4800,
"tax_rate": 20
},
{
"name": "Product interface design",
"quantity": 12,
"unit_price": 620,
"tax_rate": 20
}
],
"payment": {
"iban": "FR76 3000 4008 2800 0123 4567 890",
"bic": "BNPAFRPPXXX"
},
"notes": {
"terms": "Payment due within 30 days - bank transfer."
}
}'Request body
The payload mirrors the seven blocks of the editor, so a field on screen and a field in JSON are the same field. Unknown keys are rejected rather than ignored — a typo produces a 422 naming it, not a silently incomplete document.
typerequiredWhich template to render.typeenumrequiredinvoice,quote,credit_noteordelivery_note.
documentrequiredNumbering, dates, currency, presentation.numberstringrequired- Your own reference, e.g.
INV-2026-0042. issued_ondaterequiredYYYY-MM-DD. Real calendar dates only — 31 February is refused.due_ondate- Required when
payment_timingisdue_by. Cannot precedeissued_on. valid_untildate- How long a quote's price holds.
payment_timingenumdefault due_bypaidprints “Paid on”,due_byprints “To be paid by” with the date,dueprints “To be paid” with no date.statusenumdefault pendingpaid,pendingoroverdue.currencyenumdefault EUREUR,USD,GBP,CHF. Symbol placement follows the document language.languageenumdefault enen,fr,es,de,it. Translates the printed sheet — labels, dates, number formatting.themeenumdefault minimalistminimalist,modernorbold.title_overridestring- Replaces the printed title. Australian invoices must read “Tax Invoice”.
tax_labelstring- Renames the tax column:
VAT,TVA,GST,Sales Tax. logostring- A
data:image/…URL or anhttps://one. Nothing else is accepted. referencestring- Purchase order or internal reference.
shipping_addressstring- Printed as a separate block when present.
watermarkbooleandefault false- Off by default. The API starts at Cloud Pro, and every tier that reaches it has the watermark removed.
sender · recipientrequiredBoth sides. Same shape; only name is required.namestringrequired- Trading name.
legal_namestring- Registered name, when it differs.
address · postal_code · city · countrystring- Printed as the party block.
email · phonestring- Contact details.
tax_idstring- VAT or sales-tax number, e.g.
FR62902471338. registration_idstring- SIRET, company number, EIN.
legal_formstring- Legal form and share capital, e.g.
SASU au capital de 1 000 €. registration_citystring- Registry and city, e.g.
RCS Paris 902 471 338.
line_itemsrequired1 to 200 entries. The priced body of the document.namestringrequired- What is being charged for.
descriptionstring- A second line under the name.
quantitynumberdefault 1unit_pricenumberdefault 0- In the document's currency, as a decimal:
1499.90, not minor units. tax_ratenumberdefault 0- A percentage.
20means 20%. discountnumberdefault 0- Per-line discount, percentage.
photostring- Thumbnail beside the line. Same rules as
logo.
adjustmentsUp to 20. Document-level discounts and surcharges.labelstringrequired- Printed in the totals, e.g.
Shipping. kindenumrequireddiscountorsurcharge.modeenumrequiredpercentorfixed.valuenumberrequired- A percentage or an amount, per
mode.
paymentBank details. Not allowed on a delivery note.iban · bic · bank_namestringpayment_linkstring- An
https://URL to a hosted payment page. instructionsstring- Free text under the bank block.
notesTerms and statutory mentions.termsstring- The short line under the totals, e.g.
Payment due within 30 days. legalstring- The free-text legal footer: statutory mentions, an Impressum, the French €40 recovery-fee clause. Printed above the watermark.
delivery_instructionsstring- Delivery notes only. Handling instructions, never payment language.
outputHow you want the result back.formatenumdefault base64base64returns the bytes inline.urlstores the PDF in your PrysM Cloud and returns a signed link.filenamestring- Without the extension;
.pdfis appended. folder_iduuidurlonly. The cloud folder to file it under.
Business rules
A few constraints are enforced because breaking them produces a document that is wrong rather than malformed. These return 422 with the offending field named.
A delivery note is not a payment request
Sending payment with type: "delivery_note" is refused, and so is any payment_timing other than due. Printing an IBAN and a deadline on a proof of receipt invites a customer to pay against a document that is not an invoice — a real accounting problem, not a cosmetic one. Put handling notes in notes.delivery_instructions.
due_onis required whenpayment_timingisdue_by, and cannot precedeissued_on.valid_untilcannot precedeissued_on.folder_idonly applies whenoutput.formatisurl.- Images must be
data:image/…orhttps://. Plainhttp,file:and protocol-relative URLs are refused, and remote fetches are blocked for private and loopback addresses. - The whole request body is capped at 4 MB. Reference large images by URL rather than inlining them.
Responses
200 The document was generated. Totals are included so you never have to re-derive VAT to reconcile against your own ledger.
Inline — output.format: base64
{
"type": "invoice",
"created_at": "2026-09-19T10:24:31.882Z",
"document": {
"number": "INV-2026-0042",
"currency": "EUR",
"page_count": 1,
"byte_size": 148203,
"totals": {
"subtotal": 12240.00,
"total_excl_tax": 12240.00,
"tax": 2448.00,
"total": 14688.00,
"tax_breakdown": [
{ "rate": 20, "base": 12240.00, "tax": 2448.00 }
]
}
},
"output": {
"filename": "INV-2026-0042.pdf",
"format": "base64",
"content": "JVBERi0xLjQKJeLjz9MKMyAwIG9iago8PC9UeXBl…"
},
"usage": {
"used": 7413,
"quota": 10000,
"remaining": 2587,
"resets_at": 1790812800
}
}Stored — output.format: url
The PDF is written to your PrysM Cloud, consuming one storage slot, and comes back as a signed link valid for one hour. The document also appears in your cloud and can be reopened in the editor.
{
"type": "invoice",
"document": { "number": "INV-2026-0042", "page_count": 1 },
"output": {
"filename": "INV-2026-0042.pdf",
"format": "url",
"url": "https://…supabase.co/storage/v1/object/sign/prysm_cloud/…",
"expires_at": "2026-09-19T11:24:31.882Z",
"document_id": "8f3a2b1c-94ed-07bb-215a-f6390c4d81e7"
},
"usage": { "used": 7414, "quota": 10000, "remaining": 2586 }
}Idempotency
Send an Idempotency-Key header on every request that costs money. Use a fresh, unique value per logical operation — a UUID is ideal.
Idempotency-Key: 3f1a9c2e-7b4d-4f10-9a52-0c8e1d6b7a34Why this matters
A network timeout does not tell you whether the work happened. Without a key, your retry renders a second document and consumes a second request — the customer is billed twice for one intended operation. With a key, the retry returns the original response, byte for byte, and nothing is charged again.
- A repeat of a completed request replays the stored response with
Idempotent-Replay: true. - A repeat while the first is still running returns 409 — wait and retry.
- The same key with a different body returns 422. That is a bug on your side, and replaying the first response would hide it.
- A failed request releases its key, so you can retry with the same one and actually get a document.
- Keys are remembered for 24 hours.
Errors
Every failure returns the same envelope. code is the contract and is safe to branch on; message is for humans and may be reworded.
{
"error": {
"code": "invalid_api_key",
"message": "This API key is unknown, revoked, or no longer valid."
}
}| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_json | The body is not valid JSON. |
| 401 | unauthorized | No credentials, or an expired access token. |
| 401 | invalid_api_key | The key is unknown, revoked or no longer valid. |
| 402 | payment_required | Your plan includes no API requests. Upgrade to Cloud Pro or above. |
| 409 | conflict | A request with this Idempotency-Key is still running. |
| 413 | payload_too_large | The body exceeds 4 MB. |
| 415 | unsupported_media_type | Content-Type is not application/json. |
| 422 | invalid_request | The payload failed validation. See `details`. |
| 429 | quota_exceeded | Monthly allowance spent, or the rate limit hit. See `Retry-After`. |
| 500 | internal_error | Generation failed. The request was not charged. |
| 503 | not_configured | Limits are temporarily unreadable. Retry shortly. |
Validation errors name the field
A 422 carries a details array with one entry per problem — all of them, so you fix your payload in one pass rather than discovering the next error on the next call.
{
"error": {
"code": "invalid_request",
"message": "The request payload is not valid. See `details`.",
"details": [
{
"field": "document.due_on",
"message": "`due_on` is required when `payment_timing` is \"due_by\"."
},
{
"field": "line_items.2.tax_rate",
"message": "Too big: expected number to be <=100"
},
{
"field": "line_item",
"message": "Unknown field: line_item. Check the spelling, or remove it."
}
]
}
}Rate limits & quotas
Two separate ceilings. The monthly quota is how many documents your plan includes. The rate limit is how fast you may spend it, and exists to protect the service.
| Plan | Requests / month | Cloud storage |
|---|---|---|
| Free · Premium | — | — |
| Cloud Pro | 10,000 | 10,000 PDFs |
| Business API | 50,000 | 50,000 PDFs |
| Scale | 200,000 | 250,000 PDFs |
The rate limit is 30 requests every 10 seconds per account. Exceeding it returns 429 with a Retry-After header in seconds.
Every response carries your remaining monthly allowance, so you can back off before you hit the wall rather than after:
X-RateLimit-Limit: 10000
X-RateLimit-Remaining: 2588
X-RateLimit-Reset: 1790812800A failed render is never charged
The request is reserved before the work and settled after it. If generation fails for any reason, the reservation is released — so what you are billed for is exactly the documents you received.