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
apiKeyAuthand themetadataTemplatesfeature.
| Endpoint | Method | Description |
|---|---|---|
GET /metadata-templates | GET | Paginated 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} | GET | Single template with full fields array including options. |
POST /metadata-template | POST | Create a new template. |
PATCH /metadata-template/{id} | PATCH | Update a template (partial update). |
DELETE /metadata-template/{id} | DELETE | Delete a template. Only inactive templates can be deleted. |
GET /metadata-template/{id}/options/{fieldKey} | GET | Search options for a select field (substring match). |
Query Parameters for GET /metadata-templates
| Parameter | Type | Description |
|---|---|---|
page | number | Page number (1-based). |
perPage | number | Results per page. |
documentType | string | Filter by document type: invoice, offer, orderconfirmation, invoiceincoming. |
Query Parameters for GET /metadata-template/{id}/options/{fieldKey}
| Parameter | Type | Description |
|---|---|---|
q | string | Search string. Case-insensitive substring match against option value and label. |
limit | number | Maximum results (1--100, default 20). |
Template Fields
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Template name (max 255 chars). |
documentType | string | Target document type (invoice, offer, orderconfirmation, invoiceincoming). Omit or null for all types. | |
isActive | boolean | Default false. Only one template can be active per document type --- activating a new one deactivates the existing one. | |
fields | array | Yes | Array of field definitions (max 50). See below. |
Field Definition
| Field | Type | Required | Description |
|---|---|---|---|
key | string | Stable 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. | |
label | string | Yes | User-facing name (max 255 chars). |
inputType | string | Yes | text (free-form input) or select (searchable combobox with predefined options). |
type | string | Yes | head (document-level) or position (line-item-level). |
required | boolean | Whether the field must be filled before the document can be saved. Default false. | |
defaultValue | string | Pre-filled value shown in the UI when the field is empty. | |
directInput | boolean | For inputType: "select" only: whether the user can type a free-text value not in the options list. Default false --- only listed options are accepted. | |
order | number | Yes | Display order within this template. Order is per template: the same field can appear at different positions in different templates. |
options | array | Only for inputType: "select". Array of { value, label } pairs (max 1000). When directInput is false, only these values are accepted. |
Sanity Limits
| Constraint | Limit |
|---|---|
| Fields per template | 50 |
| Options per select field | 1000 |
| Field label length | 255 |
| Option value / label length | 255 |
| Template name length | 255 |
| Templates per user | 100 |
| Metadata value length | 2000 |
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 <script>), 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
| Status | When |
|---|---|
400 BAD_REQUEST | Duplicate 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_REQUEST | Option search called on a field whose inputType is not select. |
403 FORBIDDEN | Calling any metadata-template endpoint without the metadataTemplates feature. |
404 NOT_FOUND | Template 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), andhead. - Inline: includes
headonly (nofields, notemplateId).
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:
MetadataFieldobjects 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
keymust 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: trueon a new template automatically deactivates the existing one for that document type. - Template deletion: Deleting a template removes the template-field associations (
MetadataTemplateFieldrows) but does not delete the underlyingMetadataFieldobjects. Invoices snapshotted before deletion are unaffected. - Select fields: The
selectinput type renders as a searchable combobox. WhendirectInput: true, users may enter any free-text value. WhendirectInput: false(default), only values from theoptionslist are accepted.