DEKRA 1.0 — structure and rules
Normative companion to STANDARD.md. Everything here is enforced by
App\Services\Dekra\DekraValidatorService; every rule names the code the validator reports, so a
finding in the console can be traced straight back to a line in this document.
The machine-readable form of this document is dekra-1.0.schema.json,
generated from the same constants the validator enforces. It carries shape, presence and enumerated
values. It cannot carry the arithmetic cross-checks or the signatures — a file that satisfies the
schema is well-formed, and only the validator can say it is valid.
Conventions used below:
- required — the key must be present. A key whose value may be
nullis still required to be present: absent keys are an error,nullvalues are not. This is what keeps every document the same shape regardless of type, and what letsnullmean "the document does not say" instead of being indistinguishable from "nobody wrote this field". - decimal — an exact decimal string (
"12.10"), never a JSON number. See §10. - money — a decimal string with exactly as many places as the currency's ISO 4217 minor unit.
- percent — a decimal string, 2 places,
0.00…100.00. - Levels: E = error (file invalid), W = warning (file valid, data suspicious).
Closed value lists live in registry/ and are named per field below.
1. Container (root object)
| Key | Type | Req. | Rule | Codes |
|---|---|---|---|---|
format |
string | ✔ | Exactly DEKRA |
E key.missing, E key.type, E key.empty, E format.unknown |
version |
string | ✔ | ^\d+\.\d+$; a supported version (currently 1.0) |
E version.malformed, E version.unsupported |
profile |
string | ✔ | Conformance profile, §9. From registry/profiles.json |
E profile.unknown |
signature_profile |
string | ✔ | local or issuer, §8 |
E signature_profile.unknown |
signature_algorithm |
string | ✔ | Must be the algorithm the declared profile uses | E signature_algorithm.mismatch |
key_id |
string|null | ✔ | Which key signed. Absent under issuer → W key_id.missing |
E key.type |
issuer_key_url |
string|null | ✔ | Where the public key is published (§8) | E key.type |
generated_at |
string | ✔ | Parseable timestamp; SHOULD be ISO-8601 with offset | E generated_at.format, W generated_at.loose |
document_count |
integer | ✔ | Must equal documents.length |
E document_count.missing, E document_count.mismatch |
documents |
array | ✔ | 1…1000 objects (§11 for larger batches) | E documents.missing, E documents.empty, E documents.too_many, E document.not_object |
extensions |
object|null | ✔ | §12 | E extensions.type, E extensions.namespace |
signature |
string | ✔ | 64 or 128 lowercase hex; must verify (STANDARD §4) | E signature.missing, E signature.malformed, E signature.collection_invalid |
Any other top-level key: W key.unknown. Readers must ignore unknown keys, never fail on them.
Whole-file failures: E file.unreadable, E json.invalid, E json.not_object.
2. Document (documents[i])
| Key | Type | Req. | Rule | Codes |
|---|---|---|---|---|
document_type |
string | ✔ | From registry/document-types.json |
E document_type.missing, E document_type.unknown |
document_subtype |
string|null | ✔ | Sender-defined refinement, free text | E key.missing_nullable, E key.type |
document_number |
string|null | ✔ | May be null — a shop receipt often has no usable number |
E key.type, W document_number.absent |
issuer |
object | ✔ | Party block, §3. Who wrote the document | see §3 |
recipient |
object | ✔ | Party block, §3. Who it was written to | see §3 |
references |
array | ✔ | 0…n references to other documents, §4 | see §4 |
items |
array | ✔ | 0…n item objects, §5. May be empty | E items.missing, W items.empty |
allowances_charges |
array | ✔ | 0…n document-level allowances and charges, §6 | see §6 |
totals |
object | ✔ | §7 | see §7 |
dates |
object | ✔ | §7.3 | see §7.3 |
payment |
object | ✔ | §7.4 | see §7.4 |
fiscal |
object|null | ✔ | What an authority stamped on it, §7.5 | see §7.5 |
source_document |
object|null | ✔ | Fingerprint of the file it was read from, §7.6 | see §7.6 |
comment |
string|null | ✔ | A note about the document as a whole | E key.type |
extensions |
object|null | ✔ | §12 | see §12 |
signature |
string | ✔ | 64 or 128 lowercase hex; must verify | E signature.* |
Any other key inside a document: W key.unknown.
2.1 Document types
invoice, credit_note, debit_note, corrective_invoice, self_billed_invoice, proforma,
receipt, contract, waybill, other.
A credit note is a type, not a subtype. As free text no consumer could tell a refund from a charge, so every sum over an archive was too high by the value of its returns.
Amounts are always positive. Direction is carried by the type: a consumer summing an archive
negates credit_note (DekraRegistry::CREDIT_TYPES) and nothing else. A sign convention is
the fastest way to get two implementations disagreeing about a refund. The rule binds the producer;
the validator does not check the sign of an amount outside payment (§7.4), so a negative figure
elsewhere is a wrong file that still validates.
2.2 What a document never describes
No field describes the archive holding the document, or the software that wrote the file. One
invoice is incoming to the buyer and outgoing to the seller, and it arrives on a different day
in each of their archives: a field carrying either would make the same signed bytes assert two
different things depending on who opened the file. Which side of a transaction you are on is
answered by comparing issuer and recipient with your own identity — a question only the holder
can answer. Likewise a producer's own record number: it means something in exactly one database and
carries out how many records that database holds.
3. Party (issuer, recipient)
The same key set for both sides and both directions — this is the rule that lets a reader treat invoices and receipts identically.
| Key | Type | Req. | Rule |
|---|---|---|---|
name |
string|null | ✔ | Legal name. Empty → W party.name_missing |
entity_type |
string|null | ✔ | Legal form as printed (SIA, GmbH, Ltd, LLC, …). Free text: the world's legal forms are not a list anyone can close |
country |
string|null | ✔ | ISO 3166-1 alpha-2. Absent → W party.country_missing; malformed → E party.country_invalid |
identifiers |
array | ✔ | §3.1 |
is_vat_payer |
bool|null | ✔ | null = unknown, not "no" |
address |
string|null | ✔ | Address as printed, one line |
address_parts |
object|null | ✔ | { street, city, postal_code, subdivision }, each string|null, all four required to be present |
postal_address |
string|null | ✔ | Only when it differs from address |
email |
string|null | ✔ | Malformed → W party.email_invalid |
phone |
string|null | ✔ | As stored, not normalised |
endpoint_id |
string|null | ✔ | Electronic address for e-invoicing (PEPPOL EndpointID). Only when the document states one |
endpoint_scheme |
string|null | ✔ | Four-digit PEPPOL EAS code. Missing while endpoint_id is set → W party.endpoint_scheme_missing; not four digits → W party.endpoint_scheme_invalid |
bank |
object | ✔ | { name, account, swift }, each string|null, all three required to be present |
Codes: E party.missing, E party.key_missing, E party.type, E party.type_bool,
E party.country_invalid, E party.bank_missing, E party.bank_key_missing, E party.bank_type,
E address_parts.type, E address_parts.key_missing, E address_parts.type_field,
E identifiers.missing, W party.name_missing, W party.country_missing,
W party.email_invalid, W party.endpoint_scheme_missing, W party.endpoint_scheme_invalid.
country is never inferred. Not from the address text, not from the document's language, not
from the producer's own location. Without it a consumer cannot decide which tax rules the document
falls under, and a guessed country decides them wrongly while looking exactly as confident as a
read one. subdivision is not decoration either: in the United States, Canada, Brazil and India
the rate is decided by the subdivision rather than the country.
3.1 Identifiers (identifiers[k])
| Key | Type | Req. | Rule |
|---|---|---|---|
scheme |
string | ✔ | VAT, GLN, LEI, DUNS, or <ISO 3166 alpha-2>:<local abbreviation> (LV:REGN, US:EIN, IN:GSTIN, BR:CNPJ) |
value |
string | ✔ | As printed |
Codes: E identifiers.missing, E identifier.not_object, E identifier.key_missing,
E identifier.type, E identifier.scheme_malformed, W identifier.scheme_unknown.
Why a list and not two fields. registration_number plus vat_number is a European company's
paperwork written into a format that claims to be international: it holds no EIN, no TIN, no GSTIN,
no ABN, no CNPJ, no UEN, no CUIT, and no company that has three at once. And an unqualified string
says nothing about whose register it came from — short numbers collide across countries, and a
consumer matching one against its own database is matching two different things.
A scheme outside registry/identifier-schemes.json is a warning, not an error: there are more
registers in the world than in that list, and refusing a document because a country is missing from
it would be exactly the provincialism the qualifier exists to remove. The shape is still required.
Empty strings are not allowed as "unknown". A producer must emit null. The validator
enforces this on identifiers[].scheme and value (E identifier.type) and reports an empty
name (W party.name_missing); an empty string in any other party field passes the type check
and is simply a wrong file.
Neither party may be inferred. Both blocks describe what the source document says. A producer
that fills recipient from whoever happens to be filing the document is writing a claim the paper
does not make into a signed record — and a cash receipt names no buyer at all, so "not stated" has
to be representable. An all-null party block is the correct output in that case.
4. References (references[k])
| Key | Type | Req. | Rule |
|---|---|---|---|
type |
string | ✔ | From registry/reference-types.json |
document_number |
string | ✔ | The referenced document's number, as printed |
issue_date |
string|null | ✔ | YYYY-MM-DD |
signature |
string|null | ✔ | The referenced document's DEKRA signature, when the producer holds it |
Codes: E references.missing, E reference.not_object, E reference.key_missing,
E reference.number_required, E reference.type_unknown, E reference.date_format,
E reference.signature_malformed, W references.correction_unlinked.
A credit_note, debit_note or corrective_invoice with no reference of type corrects or
replaces gets W references.correction_unlinked: the receiver can book the amount and can tie it
to nothing.
signature is the strongest field in this block. A number can be reused, reissued or typed
wrongly; a signature pins the reference to one exact prior version. With it, a chain of corrections
is verifiable by anyone holding the documents, without access to the issuer's database — which is
what an archive needs and what a bare document number cannot give.
5. Item (items[j])
| Key | Type | Req. | Rule |
|---|---|---|---|
description |
string | ✔ | Empty → W item.description_empty |
quantity |
decimal | ✔ | 4 places |
unit |
string | ✔ | UN/ECE Recommendation 20 code, the list PEPPOL uses in unitCode (H87, KGM, LTR, HUR, …). 1–3 chars, [A-Z0-9] |
unit_name |
string|null | ✔ | The unit as a person reads it: the word the document printed (gab., vnt., tk, шт), or the producer's own short name for the code. Never the code repeated |
price |
decimal | ✔ | Unit price excluding tax, 4 places |
discount_percent |
percent | ✔ | Line-level discount |
taxes |
array | ✔ | 0…n tax entries, §5.1 |
total |
money | ✔ | Line net: quantity × price × (1 − discount_percent/100). Excludes tax. Excludes the document-level discount. |
comment |
string|null | ✔ | A note about this line |
Codes: E item.not_object, E item.key_missing, E item.type, E item.unit_code,
E key.missing_number, E key.not_decimal, E key.scale, E percent.range,
W item.description_empty, W item.total_mismatch, W item.unit_unknown, W key.unknown.
item.total_mismatch is a warning, tolerance ±0.02: sources round differently, and an incoming
document's lines come from a parser. The total semantics above are nevertheless normative — a
producer that puts a tax-inclusive figure there is producing a wrong file, and the warning is how it
shows up.
Why the unit is a code. A document prints gab., vnt., tk, шт or pcs and means the same
thing every time; stored as printed, that fact is available to a human reader and to nobody else —
quantities cannot be summed across two documents, and a billing message cannot be produced without
guessing. So the code is normative and a readable word is kept beside it: the document's own where
it printed one, because a producer does not get to overwrite what the paper said. A malformed unit
is an error; a well-formed code outside a reader's table is a warning, since Rec 20 has some
1800 codes and no implementation carries all of them. unit_name is checked for type only — a
producer that repeats the code there is not caught, only wrong.
comment is a note, not data. It is where a person writes what the paper does not say — why a
quantity looks wrong, which order it belongs to, that the price was agreed by phone. Free text, no
rule beyond its type, and a reader must not parse it. Like the document's own comment it is
outside the signature (STANDARD §4), so a filed document can be annotated without becoming
unverifiable.
5.1 Tax (items[j].taxes[t])
| Key | Type | Req. | Rule |
|---|---|---|---|
type |
string | ✔ | From registry/tax-types.json: VAT, GST, SALES, EXCISE, WITHHOLDING, OTHER |
category |
string | ✔ | UNTDID 5305, from registry/tax-categories.json |
rate |
percent | ✔ | The rate, not the amount |
amount |
money | ✔ | Tax on this line at this rate |
jurisdiction |
string|null | ✔ | CC or CC-SUB (ISO 3166-1 / 3166-2) — which authority it is owed to |
tax_type_name |
string|null | ✔ | What the document called it, when type is OTHER |
exemption_reason |
string|null | ✔ | Free text, in the document's own words |
exemption_code |
string|null | ✔ | The legal reference, when the document states one |
Codes: E taxes.missing, E tax.not_object, E tax.key_missing, E tax.type_unknown,
E tax.category_unknown, E tax.zero_category_rate, E tax.jurisdiction_invalid,
E tax.type_field, W tax.exemption_unexplained.
Why an array. One rate per line is a European assumption. A Canadian line carries GST and a
provincial tax at once; a Brazilian line carries ICMS, IPI, PIS and COFINS with different bases; a
United States line carries state, county and city rates that are not a VAT at all and are not
recoverable by the buyer. A single rate field cannot express any of them, and a format that
requires one is a European format wearing an international name.
A category meaning "no tax is due" (Z, E, AE, K, G, O) with a rate above zero asserts
two opposite things at once: E tax.zero_category_rate. A category of E, AE or O with neither
exemption_reason nor exemption_code gets W tax.exemption_unexplained — a tax authority asks
for the provision, and a receiver cannot invent it.
6. Allowances and charges (allowances_charges[c])
| Key | Type | Req. | Rule |
|---|---|---|---|
is_charge |
bool | ✔ | true = a charge, false = an allowance |
amount |
money | ✔ | The absolute amount |
base_amount |
decimal|null | ✔ | What percent was applied to |
percent |
decimal|null | ✔ | When the document expressed it as a percentage. Checked as a decimal only — neither the 2-place scale nor the 0…100 range of a percent is enforced here |
reason |
string|null | ✔ | The document's own wording |
reason_code |
string|null | ✔ | UNTDID 5189 / 7161, from registry/allowance-reasons.json |
taxes |
array | ✔ | Tax on the allowance or charge, §5.1. The key must be present (E charge.key_missing); a null value is not reported — the §5.1 checks run only when it is an array |
Codes: E allowances_charges.missing, E charge.not_object, E charge.key_missing,
E charge.type_bool, E charge.type_field, E key.not_decimal, E key.scale,
W charge.reason_unknown.
Delivery, packing and insurance are not goods. While they had to be written as line items they
distorted both the item list and the tax base, and an absolute discount had to be converted into a
percentage, losing what was printed. An unlisted reason is stated in reason with a null
reason_code rather than forced into the nearest match.
7. Totals, dates, payment, fiscal, source
7.1 Totals (totals)
| Key | Type | Req. | Rule |
|---|---|---|---|
currency |
string | ✔ | ISO 4217, three uppercase letters. Decides the scale of every money field in the document |
tax_currency |
string|null | ✔ | Only when the tax is owed in a different currency. ISO 4217 shape is checked (E totals.tax_currency); that it differs from currency is not |
exchange_rate |
decimal|null | ✔ | The rate actually applied |
exchange_rate_date |
string|null | ✔ | YYYY-MM-DD |
discount_percent |
percent | ✔ | Document-level discount, applied on top of line discounts |
total_without_tax |
money | ✔ | Sum of line nets after the document discount |
tax_breakdown |
array | ✔ | §7.2 |
total_tax |
money | ✔ | Total tax |
total_tax_in_tax_currency |
decimal|null | ✔ | The same figure in tax_currency. Checked as a decimal only — its scale would come from tax_currency, and the validator does not apply it |
total_amount |
money | ✔ | total_without_tax + total_tax |
Two cross-checks:
- E
totals.mismatch—|total_without_tax + total_tax − total_amount| > 0.01. Pure self-consistency, independent of any source convention, therefore an error. - W
totals.items_mismatch— the line nets after the document discount disagree withtotal_without_taxby more than 0.02. A warning: rounding and discount conventions legitimately differ between sources.
tax_currency set with no exchange_rate → W totals.exchange_rate_missing. An amount in another
currency with no rate is a claim nobody can check and nobody can reproduce.
Other codes: E totals.missing, E totals.key_missing, E totals.currency,
E totals.tax_currency, E totals.exchange_rate_date, E key.missing_number,
E key.not_decimal, E key.scale, E percent.range. The currency is read first: it sets the
scale every money field in the document is checked against, and a document with no readable
currency is checked at 2 places.
7.2 Tax breakdown (totals.tax_breakdown[b])
{ type, category, rate, jurisdiction, taxable_amount, tax_amount } — the same vocabulary as §5.1.
The rows must sum to total_tax within 0.01, or E tax_breakdown.mismatch. A document with tax and
an empty breakdown is E tax_breakdown.empty. Each row's type, category and rate are checked
with the §5.1 rules and codes; the block's own are E tax_breakdown.missing,
E tax_breakdown.not_object, E tax_breakdown.key_missing. The sum is only compared when every row
parsed — a row with an unreadable amount produces its own error, not a second one for the total.
This is what a tax return is filed from. A document with lines at 21 % and 12 % cannot state its base per rate with a single total: it cannot be filed, cannot be checked, and cannot be converted to a billing message without the reader re-deriving the split and hoping it matches.
Where a producer cannot honestly decompose the tax — no lines, or lines whose rates do not explain the printed total — it emits one row at the effective rate covering the whole base. That keeps the rate and the amount agreeing with each other and with the totals. Bending one group's amount to close the gap would leave a row whose rate contradicts its own figure.
7.3 Dates (dates)
| Key | Type | Req. | Rule |
|---|---|---|---|
invoice_date |
string | ✔ | YYYY-MM-DD, a real date, must not be null |
tax_point_date |
string|null | ✔ | When the tax became due. null = the same as invoice_date |
supply_date |
string|null | ✔ | When the goods or services were supplied |
due_date |
string|null | ✔ | Earlier than invoice_date → W dates.due_before_invoice |
period_start |
string|null | ✔ | For a periodic document |
period_end |
string|null | ✔ | Before period_start → E dates.period_reversed |
Dates are calendar dates without a time zone. Anything with a time in it is E dates.format.
Codes: E dates.missing, E dates.key_missing, E dates.format, E dates.invoice_date_required,
E dates.period_reversed, W dates.due_before_invoice.
tax_point_date is separate because in most tax regimes it is legally distinct from the date the
document was written, and it is the one that decides which period the document is filed in.
Both dates are dates printed on the document. When a copy arrived somewhere is not one: two archives holding the same invoice disagree about that and both are right.
7.4 Payment (payment)
Payment state is deliberately outside the signature — a document that gets paid must not become "corrupt".
{ method, status, paid_amount, remaining_amount }. method and status are free text as stored
by the source, not code lists. Amounts are money, ≥ 0 — the one place the validator checks a sign
(E payment.negative). paid_amount exceeding total_amount by more than 0.01 →
W payment.overpaid.
Codes: E payment.missing, E payment.key_missing, E payment.type, E payment.negative,
E key.missing_number, E key.not_decimal, E key.scale, W payment.overpaid.
7.5 Fiscal (fiscal)
null when the document is not fiscal. Otherwise:
| Key | Type | Req. | Rule |
|---|---|---|---|
scheme |
string | ✔ | From registry/fiscal-schemes.json |
id |
string|null | ✔ | The identifier the authority issued |
issued_at |
string|null | ✔ | When the authority registered it |
device_id |
string|null | ✔ | Fiscal register / cash register / fiscal memory serial |
sequence |
string|null | ✔ | Receipt or shift number |
qr |
string|null | ✔ | The QR payload exactly as printed |
verification_url |
string|null | ✔ | Where the authority verifies it |
authority_signature |
string|null | ✔ | The authority's own signature, verbatim |
previous_hash |
string|null | ✔ | For chained regimes (PT, ES) |
Codes: E fiscal.type, E fiscal.key_missing, E fiscal.type_field, E fiscal.scheme_unknown,
E fiscal.empty, E fiscal.issued_at_format.
A scheme naming neither an id nor a qr names nothing: E fiscal.empty.
This block is inside the signature, and nothing in it is normalised. It is the only part of a DEKRA body that is not the producer's reading of paper but another party's claim about the document — and it is the only thing that lets a holder prove the document against the authority that issued the identifier. A "tidied up" Portuguese QR payload or a reformatted Brazilian access key no longer verifies against the issuer, which destroys the one property that makes a stored document evidence rather than a transcript.
7.6 Source document (source_document)
null, or { hash_algorithm, hash, media_type, byte_size, filename } — every key present,
byte_size an integer or null, the rest a string or null. hash is lowercase hex, 32–128
characters.
Codes: E source_document.type, E source_document.key_missing, E source_document.type_field,
E source_document.algorithm_missing, E source_document.hash_malformed.
Not an attachment — DEKRA carries data, not scans. But without the fingerprint the .dkr record
and the file it was read from are two unrelated objects that happen to share a folder, and an
archive cannot show that one came from the other. A hash with no algorithm named is not verifiable:
E source_document.algorithm_missing.
8. Signature profiles
| Profile | Algorithm | What it proves |
|---|---|---|
local |
HMAC-SHA256 | The file has not been corrupted. Not origin: whoever holds the key to verify it holds the key to forge it |
issuer |
Ed25519 | Origin. A verifier needs only the public key, published as a JWK set at issuer_key_url |
The profile is written into the file rather than left for a reader to infer from the algorithm, because the difference between "this file is intact" and "this file came from them" is the whole question a signature is asked, and a reader that guesses wrong guesses in the dangerous direction.
key_id names the key so a compromised one can be retired without invalidating everything signed
before it. A retired public key stays published for as long as files signed with it must verify.
9. Conformance profiles
| Profile | Adds |
|---|---|
core |
Structurally valid. Any document type; no promise beyond the shape |
billing |
A billing type; both parties named and placed in a country; an issue date; a non-empty tax breakdown |
fiscal |
billing (or a receipt), plus a fiscal block with a scheme and an identifier or QR |
archive |
fiscal, plus a source_document hash |
A collection is only as strong as its weakest document. A batch claiming fiscal with one
document inside carrying nothing from an authority would be exactly the false assurance the profiles
exist to prevent.
10. Numbers
Money and quantities are decimal strings, never JSON numbers.
A JSON number is an IEEE-754 double: it cannot represent 0.10 exactly. A format whose entire
purpose is proving an amount did not change cannot store that amount in a type that changes it. The
danger is not theoretical — it appears the first time an implementation in another language parses
and re-serialises a file, and it appears as a signature that no longer verifies.
Scale comes from the currency, not from a constant. "Two decimal places" is a statement about
the euro: JPY and KRW have no minor unit, KWD, BHD, OMR and TND have three. A money field must carry
exactly the number of places ISO 4217 gives its currency — E key.scale otherwise, in both
directions. Quantities carry 4 places, unit prices 4, percentages 2.
Rounding is half-up, applied at the scale of the field being written. The rule is stated because without it two implementations differ by a cent and the signature stops matching for a reason neither can see.
11. Rules a producer must follow
- Emit every key, using
nullfor unknown — never omit and never use"". items[].totalis net, pre-document-discount. Getting this wrong is the single most common way to produce a file that validates as "warnings only" but means something else. The second is a negative amount: outsidepaymentthe validator does not check the sign, so a refund written as-100.00instead of as acredit_noteproduces a file that validates and sums wrongly.- Sign after building, never edit a signed field afterwards. The exceptions are
payment,generated_at, the comments andextensions, which are outside both signatures by design. - Make the breakdown add up. A file whose
tax_breakdowndoes not sum tototal_taxcannot be filed by its receiver. - Never infer a country, a recipient or an electronic address. All three look exactly as confident when guessed as when read.
- One file, one collection signature. Concatenating two
.dkrfiles is not a valid file. - Cap at 1000 documents in the container form; larger batches use the streaming form (STANDARD §6), which carries the same two signature levels without holding the batch in memory.
12. Extensions
extensions is an object whose keys are reverse-DNS namespaces
(^[a-zA-Z][a-zA-Z0-9]*(\.[a-zA-Z0-9-]+)+$ — pl.gov.ksef, br.gov.nfe). It exists at the
container level and per document.
Without namespaces, "a reader ignores unknown keys" means that sooner or later Poland and Brazil add their own field and name it the same thing. With them a collision is impossible.
Extensions are outside both signatures. An unknown key must never be a reason a file fails to verify — that is the whole point of the ignore-unknown-keys rule, and signing extension content would quietly reverse it.
Codes: E extensions.type, E extensions.namespace.
13. Rules a reader must follow
- Reject an unknown MAJOR version. Accept an unknown MINOR — it only adds keys that may be null.
- Ignore unknown keys. They are a warning at most, never a reason to refuse a file.
- Verify both signature levels before trusting anything, and check
signature_profile: alocalsignature is a corruption check and proves nothing about who produced the file. - Read the profile before the content. If you need
fiscaland the file saysbilling, refuse at the door. - Treat the codes as authoritative. The format carries no display text: translate the codes.
- Do not parse the comments. Free text written by a person, in whatever language they used.
- Do not infer accounting treatment. DEKRA carries no ledger account; book the document under your own rules.