Skip to main content

Metadata Templates

Metadata templates let you define custom fields that are attached to invoices during creation. Fields can be text (free-form input) or select (combobox with predefined options). Templates and fields are both scoped per user. Fields are reusable: the same MetadataField object can be assigned to multiple templates; each template controls only the display order of its fields.

There are two ways to attach metadata to an invoice via the API:

  • Template-based --- reference a pre-configured template by ID. The server snapshots the field definitions (including options) onto the invoice. Required-field enforcement applies.
  • Inline key-value --- pass key-value pairs directly. No field definitions, no enforcement. Simple data storage.

Metadata Template Endpoints

All metadata template endpoints require apiKeyAuth and the metadataTemplates feature.

EndpointMethodDescription
GET /metadata-templatesGETPaginated list of templates. Each item includes id, name, documentType, isActive, createdAt, updatedAt, and fieldCount (number of fields) — the fields array itself is omitted. Use the detail endpoint to fetch field definitions.
GET /metadata-template/{id}GETSingle template with full fields array including options.
POST /metadata-templatePOSTCreate a new template.
PATCH /metadata-template/{id}PATCHUpdate a template (partial update).
DELETE /metadata-template/{id}DELETEDelete a template. Only inactive templates can be deleted.
GET /metadata-template/{id}/options/{fieldKey}GETSearch options for a select field (substring match).

Query Parameters for GET /metadata-templates

ParameterTypeDescription
pagenumberPage number (1-based).
perPagenumberResults per page.
documentTypestringFilter by document type: invoice, offer, orderconfirmation, invoiceincoming.

Query Parameters for GET /metadata-template/{id}/options/{fieldKey}

ParameterTypeDescription
qstringSearch string. Case-insensitive substring match against option value and label.
limitnumberMaximum results (1--100, default 20).

Template Fields

FieldTypeRequiredDescription
namestringYesTemplate name (max 255 chars).
documentTypestringTarget document type (invoice, offer, orderconfirmation, invoiceincoming). Omit or null for all types.
isActivebooleanDefault false. Only one template can be active per document type --- activating a new one deactivates the existing one.
fieldsarrayYesArray of field definitions (max 50). See below.

Field Definition

FieldTypeRequiredDescription
keystringStable identifier. Auto-generated if omitted (format: f_ + 12 random alphanumeric characters). Must match ^[a-zA-Z0-9_]{1,64}$. Unique per user --- the same key cannot appear twice for the same user account, but the same field (by key) can be assigned to multiple templates. On PATCH the entire fields array is replaced; to preserve a field, resubmit it with its existing key. A field whose key is not resubmitted is dropped, and any documents that snapshotted its definition will no longer match the new template.
labelstringYesUser-facing name (max 255 chars).
inputTypestringYestext (free-form input) or select (searchable combobox with predefined options).
typestringYeshead (document-level) or position (line-item-level).
requiredbooleanWhether the field must be filled before the document can be saved. Default false.
defaultValuestringPre-filled value shown in the UI when the field is empty.
directInputbooleanFor inputType: "select" only: whether the user can type a free-text value not in the options list. Default false --- only listed options are accepted.
ordernumberYesDisplay order within this template. Order is per template: the same field can appear at different positions in different templates.
optionsarrayOnly for inputType: "select". Array of { value, label } pairs (max 1000). When directInput is false, only these values are accepted.

Sanity Limits

ConstraintLimit
Fields per template50
Options per select field1000
Field label length255
Option value / label length255
Template name length255
Templates per user100
Metadata value length2000
Inline head keys per document (no template)50

Value sanitization

All user-supplied strings are sanitized before storage: template name, field label, option value / label, and every metadata head and line-item value. Sanitization strips HTML tags (including tags expressed via HTML entities such as &lt;script&gt;), leaving only the inner text. No error is raised — a value of <b>VIP</b> is silently stored as VIP. Do not round-trip rich-text content through metadata fields.

Error responses

StatusWhen
400 BAD_REQUESTDuplicate field key within a template; inline metadata key not matching ^[a-zA-Z0-9_]{1,64}$; value exceeds 2000 characters; more than 50 inline head keys; more than 100 templates per user; attempting to DELETE an active template (deactivate first).
400 BAD_REQUESTOption search called on a field whose inputType is not select.
403 FORBIDDENCalling any metadata-template endpoint without the metadataTemplates feature.
404 NOT_FOUNDTemplate not found; fieldKey in option search not found on the template.

Invoice Metadata

When creating or updating an invoice, you can attach metadata using one of two paths.

Path A --- Template-based

Pass a templateId in the metadata object. The server resolves the template and snapshots field definitions (with options) onto the invoice. Required fields are enforced on save.

{
"metadata": {
"templateId": "550e8400-e29b-41d4-a716-446655440000",
"head": {
"f_abc123": "KS-001"
}
},
"invoiceItems": [
{
"metadata": {
"f_def456": "P-001"
}
}
]
}

Path B --- Inline key-value

Pass key-value pairs directly. No templateId, no field definitions, no required enforcement. Keys must match ^[a-zA-Z0-9_]{1,64}$, values max 2000 chars.

{
"metadata": {
"head": {
"Kostenstelle": "KS-001",
"Abteilung": "Marketing"
}
},
"invoiceItems": [
{
"metadata": {
"Projektnummer": "P-001"
}
}
]
}

GET Response

The invoice detail endpoint returns metadata in the response:

  • Template-based: includes templateId, fields (with options), and head.
  • Inline: includes head only (no fields, no templateId).

Line items include their metadata key-value map in the response.


Examples

Example: Create a metadata template
curl -X POST \
-H "X-API-KEY: your-api-token" \
-H "Content-Type: application/json" \
-d '{
"name": "Invoice Metadata",
"documentType": "invoice",
"isActive": true,
"fields": [
{
"label": "Kostenstelle",
"inputType": "select",
"type": "head",
"required": true,
"order": 1,
"options": [
{ "value": "KS-001", "label": "Marketing" },
{ "value": "KS-002", "label": "Engineering" },
{ "value": "KS-003", "label": "Sales" }
]
},
{
"label": "Projektnummer",
"inputType": "text",
"type": "position",
"required": false,
"order": 2
}
]
}' \
https://api.faktoora.com/api/v1/metadata-template

Response (200 OK) --- the created template with auto-generated field keys.

{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Invoice Metadata",
"documentType": "invoice",
"isActive": true,
"fields": [
{
"key": "f_a1b2c3d4e5f6",
"label": "Kostenstelle",
"inputType": "select",
"type": "head",
"required": true,
"order": 1,
"options": [
{ "value": "KS-001", "label": "Marketing" },
{ "value": "KS-002", "label": "Engineering" },
{ "value": "KS-003", "label": "Sales" }
]
},
{
"key": "f_g7h8i9j0k1l2",
"label": "Projektnummer",
"inputType": "text",
"type": "position",
"required": false,
"order": 2
}
],
"createdAt": "2026-04-15T10:00:00.000Z",
"updatedAt": "2026-04-15T10:00:00.000Z"
}
Example: List metadata templates
curl -H "X-API-KEY: your-api-token" \
"https://api.faktoora.com/api/v1/metadata-templates?documentType=invoice"

Response (200 OK) --- paginated list without the fields array.

{
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Invoice Metadata",
"documentType": "invoice",
"isActive": true,
"createdAt": "2026-04-15T10:00:00.000Z",
"updatedAt": "2026-04-15T10:00:00.000Z"
}
],
"meta": {
"page": 1,
"perPage": 10,
"totalItems": 1
}
}

Use GET /metadata-template/{id} to retrieve the full fields array with options.

Example: Search options for a select field
curl -H "X-API-KEY: your-api-token" \
"https://api.faktoora.com/api/v1/metadata-template/550e8400-e29b-41d4-a716-446655440000/options/f_a1b2c3d4e5f6?q=market&limit=10"

Response (200 OK)

{
"options": [
{ "value": "KS-001", "label": "Marketing" }
]
}
Example: Create invoice with template-based metadata
curl -X POST \
-H "X-API-KEY: your-api-token" \
-H "Content-Type: application/json" \
-d '{
"invoices": [
{
"format": "ZUGFeRD-2.1-extended",
"issueDate": "20260415",
"buyer": { "name": "Acme GmbH", "address": "Musterstr. 1", "postcode": "10115", "city": "Berlin", "country": "DE" },
"invoiceItems": [
{
"name": "Consulting",
"quantity": 10,
"unitCode": "HUR",
"unitPrice": 150.00,
"vatPercent": 19,
"metadata": {
"f_g7h8i9j0k1l2": "P-2026-042"
}
}
],
"metadata": {
"templateId": "550e8400-e29b-41d4-a716-446655440000",
"head": {
"f_a1b2c3d4e5f6": "KS-001"
}
}
}
]
}' \
https://api.faktoora.com/api/v1/invoices

Response (202 Accepted)

[
{
"faktooraId": "INV-20260415-001",
"detailsPage": "https://app.faktoora.com/invoice/INV-20260415-001"
}
]

The invoice's metadata will contain the snapshotted field definitions with options from the template, plus the provided head values.

Example: Create invoice with inline key-value metadata
curl -X POST \
-H "X-API-KEY: your-api-token" \
-H "Content-Type: application/json" \
-d '{
"invoices": [
{
"format": "ZUGFeRD-2.1-extended",
"issueDate": "20260415",
"buyer": { "name": "Acme GmbH", "address": "Musterstr. 1", "postcode": "10115", "city": "Berlin", "country": "DE" },
"invoiceItems": [
{
"name": "Consulting",
"quantity": 10,
"unitCode": "HUR",
"unitPrice": 150.00,
"vatPercent": 19,
"metadata": {
"Projektnummer": "P-2026-042"
}
}
],
"metadata": {
"head": {
"Kostenstelle": "KS-001",
"Abteilung": "Marketing"
}
}
}
]
}' \
https://api.faktoora.com/api/v1/invoices

No template reference needed. Keys and values are stored as-is on the invoice.

Example: Delete a metadata template

Only inactive templates can be deleted. Deactivate first if needed.

# Deactivate
curl -X PATCH \
-H "X-API-KEY: your-api-token" \
-H "Content-Type: application/json" \
-d '{ "isActive": false }' \
https://api.faktoora.com/api/v1/metadata-template/550e8400-e29b-41d4-a716-446655440000

# Delete
curl -X DELETE \
-H "X-API-KEY: your-api-token" \
https://api.faktoora.com/api/v1/metadata-template/550e8400-e29b-41d4-a716-446655440000

Response (200 OK)

{ "succeed": true }

Notes

  • HTML sanitization: All string inputs (labels, option values, field values) are sanitized to prevent stored XSS.
  • Fields are user-scoped: MetadataField objects belong to the user account, not to a single template. The same field can be added to multiple templates; each template records its own display order for that field.
  • Key uniqueness: A key must be unique per user account. The same key cannot be created twice for the same user, regardless of which template it belongs to.
  • Key immutability: Field keys are immutable after creation. Updating a template preserves existing keys --- only new fields get auto-generated keys.
  • Active template enforcement: Only one template can be active per document type. Setting isActive: true on a new template automatically deactivates the existing one for that document type.
  • Template deletion: Deleting a template removes the template-field associations (MetadataTemplateField rows) but does not delete the underlying MetadataField objects. Invoices snapshotted before deletion are unaffected.
  • Select fields: The select input type renders as a searchable combobox. When directInput: true, users may enter any free-text value. When directInput: false (default), only values from the options list are accepted.