DoConure

Managing templates from a script

Everything the console's Templates page does is also a call on the API, apart from renaming, so an integration can onboard itself — upload a template, roll a new version forward, activate it — without anyone opening the console. A template is renamed in the console (the template's page, Name): only the name people read changes, the templateId your code calls stays the same. The file travels as base64 inside the JSON body, the same shape as every other endpoint here, rather than multipart: nicer for callers like Salesforce Apex that have no pleasant multipart story.

MethodPathDoes
POST/api/v1/compose/templatesCreate a template + version 1, active immediately.
GET/api/v1/compose/templatesList this tenant's templates.
GET/api/v1/compose/templates/{templateId}One template, with every version.
POST/api/v1/compose/templates/{templateId}/versionsAdd a version. Arrives inactive — same rule as the console.
POST/api/v1/compose/templates/{templateId}/versions/{n}/activateActivate version n. Others stay as they are.
POST/api/v1/compose/templates/{templateId}/versions/{n}/deactivateSwitch version n off; the rest keep working.
POST/api/v1/compose/templates/{templateId}/deactivateSwitch the whole template off, every version with it.
POST /api/v1/compose/templates
{
  "name": "Invoice",
  "templateType": "docx",
  "fileName": "invoice.docx",
  "fileBase64": "UEsDBBQAAAAIA...",
  "errorOnMissingFields": false
}

Every one of these answers with the template's full current state:

{
  "templateId": "invoice-a1b2c3",
  "name": "Invoice",
  "templateType": "docx",
  "active": true,
  "errorOnMissingFields": false,
  "payloadValidation": "warn",
  "activeVersions": [1],
  "versions": [
    { "versionNumber": 1, "active": true,
      "originalFilename": "invoice.docx",
      "checksum": "9f8c...", "createdAt": "2026-09-14T10:00:00+00:00",
      "hasJsonSchema": false }
  ]
}

Validated at upload, not at the first generate. These endpoints check the file immediately — the same check the console runs when a file is uploaded there — and answer TEMPLATE_FILE_REJECTED or TEMPLATE_FILE_INVALID on the upload call itself — an unattended integration has no page to notice the problem on later.

templateType is fixed at creation (docx, html or xlsx); every later version of that template must be the same format. Template ids are assigned by the service, the same slug-plus-suffix scheme the admin uses — do not try to choose one.

Checking the data: JSON Schema

A version can carry a jsonSchema (on the upload above, on a new version, or pasted in the console). Every compose call that uses that version then has its data measured against it. What a violation does is the template's payloadValidation:

ModeWhat happens
warn (default)The document is produced and charged as usual. output.warnings carries PAYLOAD_SCHEMA_VIOLATIONS (2106) listing the first 20 violations.
rejectThe call is refused with PAYLOAD_SCHEMA_VIOLATION (1403, 422) before any credit, test item or render slot is taken: it costs nothing and is never recorded as the result of an Idempotency-Key. In a bulk batch the item fails with that code and is not charged; the rest go on. In a flow, nothing is composed or sent.
offThe schema is ignored.

Set the mode with payloadValidation when you create the template, or on a new version together with its schema, so a CI job can ship both in one call and switch to reject:

POST /api/v1/compose/templates/{templateId}/versions
{
  "fileName": "invoice.docx",
  "fileBase64": "UEsDBBQAAAAIA...",
  "jsonSchema": {
    "type": "object",
    "required": ["cliente"],
    "properties": {
      "cliente": { "type": "string", "maxLength": 40 },
      "importo": { "type": "number", "minimum": 0 }
    },
    "additionalProperties": false
  },
  "payloadValidation": "reject"
}

A violation is {path, keyword, message}: path is a JSON pointer into your data (/righe/1/qta; empty means the whole payload), keyword the schema keyword that failed, message a sentence made of the schema's own words and the type found. The value you sent is never in it, so the warning is the same on a dataless organisation. A key name that came from your data (under additionalProperties) is hidden there too.

The schema is customer input and is treated like a template: it is code. The service enforces a bounded subset of draft 2020-12 and refuses the rest at upload with JSON_SCHEMA_INVALID (1303), so a schema either does what it says or is not accepted.

  • Supported: type, enum, const, minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf, minLength, maxLength, pattern, minItems, maxItems, uniqueItems, items (one schema), required, properties, additionalProperties, minProperties, maxProperties, allOf, anyOf, oneOf, not, and $ref to # or #/$defs/name.
  • Accepted and ignored: $schema, $id, $comment, title, description, default, examples, deprecated, readOnly, writeOnly and format (never asserted).
  • Refused: everything else (patternProperties, prefixItems, if/then/else, dependentRequired, unevaluated*), and any $ref that is not local: nothing is ever fetched.
  • Limits: 64 KB of schema, 24 levels, 1,000 sub-schemas, 500 enum values; a pattern of at most 128 characters, no back-references or look-arounds, no repeated group that itself repeats, at most two unbounded repeats, and a string over 2,048 characters is not matched against a pattern. Checking has a step and time budget; if it runs out the answer is a violation with keyword $budget, never "valid".

Retrying an upload

Every POST on this page accepts an Idempotency-Key header, and none of them charges you anything. That is not the reason to send one. A timed-out upload retried without a key leaves you with two templates of the same name and two ids, and your integration then points at whichever came back last while somebody in the console picks the other.

POST /api/v1/compose/templates
Authorization: Bearer lpdc_…
Idempotency-Key: invoice-template-v3

The rules are the ones on composing a document, unchanged: the same key with the same body replays the first answer with Idempotent-Replay: true, the same key with a different body is refused, and a failed attempt is never recorded so your next one runs for real.