DEKRA

DEKRA interchange format — the standard

Version 1.0. Status: stable, published at https://dekra.online/standard. Readers accept 1.0; producers emit 1.0.

  • STRUCTURE.md — the field-by-field rules a file must satisfy.
  • dekra-1.0.schema.json — the machine-readable shape.
  • registry/ — the closed value lists, generated from the same code the validator uses.
  • dkr.xml — the shared-mime-info definition for .dkr / .dkrs (§8).

1. What DEKRA is

A .dkr file is a UTF-8 JSON container that carries the economic content of one or more business documents, the identifiers a tax authority put on them, and tamper-evidence for both.

It is not a replacement for PEPPOL/UBL. UBL describes a single e-invoice for statutory delivery; DEKRA describes a batch of documents of mixed kinds, for storage and transfer — invoices, credit notes, receipts, waybills and contracts together, in one shape, between two parties who both understand it.

Design goals, in order:

  1. Self-describing. Every document says what it is (document_type) and what it promises (profile). A reader never infers the kind from a filename or the context.
  2. Homogeneous shape for heterogeneous content. An invoice and a receipt have exactly the same key set. Absent information is null, never an omitted key. One code path walks documents[].
  3. No jurisdiction is the default one. Every field that varies by country — the party's country, the tax model, the register a number comes from, the currency's number of decimal places — is carried explicitly and is never inferred from the producer's own location.
  4. Tamper-evident, not confidential. The file is plain readable JSON. Signatures prove nothing was changed; they do not hide anything.
  5. Verifiable in pieces. A single document lifted out of a collection is still verifiable on its own, and the collection separately proves nothing was added or removed.

2. Container

{
  "format": "DEKRA",
  "version": "1.0",
  "profile": "fiscal",
  "signature_profile": "issuer",
  "signature_algorithm": "Ed25519",
  "key_id": "acme-2026-08",
  "issuer_key_url": "https://acme.example/.well-known/dekra-keys.json",
  "generated_at": "2026-08-31T14:30:00+03:00",
  "document_count": 2,
  "documents": [ { … }, { … } ],
  "extensions": {},
  "signature": "…hex…"
}

A single document is a collection of one. There is no separate "one document" file shape, which means an importer has exactly one parser.

Field Rule
format Always the literal DEKRA.
version MAJOR.MINOR. MAJOR changes break readers; MINOR only adds keys that may be null.
profile The weakest promise every document in the file keeps — see §5.
signature_profile local or issuer — see §4.
signature_algorithm Determined by the profile. Stated so a future version can change it without guessing.
key_id Which key signed, so a compromised key can be retired without invalidating what came before.
issuer_key_url Where the public key is published as a JWK set.
generated_at ISO-8601 with offset. Informational — not covered by any signature.
document_count Must equal documents.length. Redundant on purpose: a truncated file fails loudly.
documents 1…1000 documents, any mix of types. Larger batches use the streaming form, §6.
extensions Namespaced room for what the core format does not carry — STRUCTURE §12.
signature Collection signature, §4.

3. Documents

Each element of documents[] is one business document, classified by document_typeinvoice, credit_note, debit_note, corrective_invoice, self_billed_invoice, proforma, receipt, contract, waybill, other. That is the kind of paper, not the accounting treatment.

issuer and recipient always mean the same thing: who wrote the document / who it was written to. On an invoice you issued, the issuer is your company; on a receipt you were given, the issuer is the shop. There is one shape and a reader never has to learn a second.

Amounts are always positive; direction is carried by the type. A credit note reduces what is owed because it is a credit note, not because its figures are negative. A sign convention is the fastest route to two implementations disagreeing about a refund.

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 — so a field carrying either would make the same signed bytes assert two different things depending on who opened it. Which side of a transaction you are on is answered by comparing issuer and recipient with your own identity, which only the holder can do. Likewise a producer's own record number: it means something in exactly one database and carries out how many records that database holds.

Full field list and per-field rules: STRUCTURE.md.

4. Signatures

Two signatures per file, both hex-encoded lowercase.

Document signature (documents[i].signature) covers the canonical JSON of exactly these fields, in this order:

document_type, document_subtype, document_number,
issuer, recipient, references, items, allowances_charges,
dates, totals, fiscal, source_document

document_type is inside deliberately: without it a receipt could be relabelled an invoice without breaking anything. fiscal is inside for a stronger reason still — it is the authority's own claim about the document, and a claim that can be swapped after signing is worth nothing.

Fields not covered:

Field Why it is outside
payment Mutable after issue — a document that gets paid must not become "corrupt".
comment, items[].comment Notes, not assertions. These are the fields somebody is most likely to add to after filing, and a record that reads as tampered with because a person wrote down why an amount looks odd is a record nobody will annotate.
extensions An unknown key must never be a reason a file fails to verify — which is exactly what signing extension content would reverse.

Collection signature (top-level signature) covers:

{"domain":"fiscal-document-container/1","version":"1.0",
 "document_count":N,"document_signatures":[ … ]}

i.e. the list of document signatures, not the documents themselves. This is what makes a removed or inserted document detectable even when every remaining document verifies correctly.

No name appears in either signature payload. domain is a frozen identifier describing what the file is; it is deliberately not the format's name, not the vendor's and not the media type. Domain separation is a real requirement — without it a signature could be replayed as one under another format — but it does not need a brand, and putting one there would mean that renaming the format invalidates every signature ever issued under the old name. The name lives in the format field, which is data and is outside both signatures, so it can change without a single stored signature ceasing to verify.

Canonical JSON for both: json_encode with JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES, no pretty-printing, and object keys sorted, recursively. Array element order is preserved — the sequence of line items is part of what the document says; the order its fields happen to be written in is not. The file body itself is pretty-printed; only the signing input is compact.

Sorting is not cosmetic. JSON says nothing about key order and neither do the stores a document passes through: PostgreSQL jsonb normalises key order on write, so a body signed in the producer's order and read back in the database's order would hash differently while saying exactly the same thing. A signature that only verifies until the document is stored is not tamper-evidence.

4.1 Signature profiles

Profile Algorithm Signature length What it proves
local HMAC-SHA256 64 hex The file is intact. Not who produced it
issuer Ed25519 128 hex Origin

A shared-key signature stops proving anything the moment it becomes verifiable: whoever holds the key to check it holds the key to produce it. Under local a signature is a corruption check between parties who already trust each other, and presenting one to a third party as authentication is a misuse the format names rather than permits.

Under issuer the producer signs with an Ed25519 secret key and publishes the public half as a JWK set at issuer_key_url:

{ "keys": [ { "kty": "OKP", "crv": "Ed25519", "use": "sig", "alg": "EdDSA",
             "kid": "acme-2026-08", "x": "…base64url…" } ] }

A verifier needs the public key and nothing else — no account, no secret, no request to the issuer beyond fetching a static file. Retired keys stay published for as long as files signed with them must still verify: a signature that stops verifying because a key was rotated is indistinguishable, to the reader, from a corrupted archive.

5. Conformance profiles

profile states what a file promises, so a consumer can refuse at the door rather than three fields into processing.

Profile Adds
core Structurally valid. Any document type
billing Both parties named and placed in a country; an issue date; a filable tax breakdown
fiscal Plus the authority's own identifiers
archive Plus an issuer signature and the hash of the file the data was read from

A collection is only as strong as its weakest document.

6. Large batches

The container form is capped at 1000 documents because it is built and verified in memory. An annual archive is larger than that, and "split it yourself" moves the problem to the holder.

The streaming form (.dkrs, application/vnd.dkr+jsonl) is the same data as JSON Lines:

{"format":"DEKRA","version":"1.0","signature_profile":…,"signature_algorithm":…,"key_id":…,"issuer_key_url":…,"generated_at":…}
{"document_type":"invoice", … ,"signature":"…"}
{"document_type":"receipt", … ,"signature":"…"}
{"profile":…,"document_count":2,"signature":"…"}

One header line, one line per document, one trailer carrying the profile, the count and the collection signature over the same payload as §4. profile and document_count are in the trailer rather than the header because neither is knowable before the last document has been read — the header goes out before the first one has — and a producer that guessed them up front would put two contradictory claims in one file. The trailer is recognised by carrying document_count; a reader must not rely on it being the last non-empty line only.

Nothing about the two signature levels changes — which is the point: a document lifted out of a stream verifies exactly as it would out of a container, and the trailer still proves the set. A producer writes it without holding the batch in memory; a reader validates document by document (each one checked as a container of one, so there is exactly one implementation of the document rules) and checks the set at the end. Findings on a stream point at a line, not at /documents/i; a stream has three codes of its own — E stream.line_invalid, E stream.header_missing, E stream.trailer_missing.

7. Conformance

A file is valid when it has no error-level issues. warning-level issues never invalidate a file — they report lossy or suspicious data the receiver should see.

Level Meaning Examples
error The file does not conform. Do not import it. wrong format, unsupported version, unknown document_type, document_count mismatch, total_without_tax + total_tax ≠ total_amount, a tax breakdown that does not sum, a money field at the wrong scale, broken signature
warning Conforms, but something is off. Import and flag. missing party name, missing country, empty items, a credit note referencing nothing, an unlisted identifier scheme, line total inconsistent with quantity × price

The sign of an amount is not checked: the validator accepts a negative money string anywhere except in payment, where it is E payment.negative. "Amounts are always positive" (§3) is a rule for producers, and a file that breaks it is wrong without being invalid.

Forward compatibility: a reader MUST ignore unknown keys (a warning, not an error) and MUST refuse an unknown MAJOR version.

The schema at dekra-1.0.schema.json carries shape, presence and enumerated values. It cannot carry the arithmetic cross-checks or the signatures, so a file that satisfies the schema is well-formed, not necessarily valid.

Validation is implemented in App\Services\Dekra\DekraValidatorService (validateJson() for source text, validateFile() for a path, validateArray() for an already-decoded container; DekraStreamService::validateFile() for the streaming form). The portal runs it in two places: on every recognised document before it is stored — an error there is logged, because it means the builder or the prompt is wrong — and on the document page and its API twin, which validate the single-document container the export would produce, so the issues shown are exactly the ones a recipient would see. There is no command-line entry point. php artisan dekra:publish --check confirms the published schema and registries still match the code.

Every issue carries a stable code, an RFC 6901 pointer (/documents/2/totals/total_amount) and, when the source text was validated, the line and column in the source file, so a failure points at a place, not at a file. The format carries no display text: the code is the finding, and its wording is looked up in the reader's language (lang/{locale}/check.php); a code with no wording is shown as itself.

8. Files, media types and identification

Container Stream
Extension .dkr .dkrs
Media type application/vnd.dkr+json application/vnd.dkr+jsonl

Systems route on the media type, not on the extension. An extension is a hint to a desktop; a media type is what an API, a mail gateway and a web server actually read, and a .dkr served as application/octet-stream is a file everything downstream has to guess about. Servers must be configured to emit it — deploy/nginx/ carries the types block for this portal.

The +json suffix (RFC 6839) is deliberate: generic tooling that knows nothing about DEKRA still knows it is looking at JSON, so it can pretty-print, diff, search and validate it with a schema. Inventing an opaque type would have thrown that away for nothing.

.dkr rather than .dk: the two-letter form is Denmark's country-code top-level domain, so invoice.dk is turned into a clickable link by mail clients, chat apps and anything else that autolinks text. A filename that reads as a URL is a filename somebody will click by mistake.

Identification without a filename. A file arriving as a mail attachment or an API body often has no useful name. Both forms open with "format": "DEKRA" as the first key — the producer emits the container keys in the order of §2 — so content sniffing works. dkr.xml in this directory is the freedesktop.org shared-mime-info definition carrying both the glob and the magic; installing it teaches a Linux desktop and anything using xdg-mime what these files are.

Registration in the IANA vendor tree (vnd.) requires no standards process — the vendor tree exists for exactly this — and is worth doing before the first outside integrator, not after.

9. Registries and change

Every closed list — document types, reference types, tax types and categories, fiscal schemes, identifier schemes, allowance reasons, profiles — is published under registry/ and generated from the code the validator enforces. dekra:publish --check fails when the two drift, which is how a published standard stays honest about what its reference implementation does.

Some lists are deliberately open: payment methods, entity types, exemption reasons, and the document's own wording everywhere. A format that stores what a document says cannot refuse a document because the world used a word a list had not heard of.

An identifier scheme outside the registry is a warning, not an error: there are more company registers than any list holds, and refusing a document because a country is missing from ours would be the provincialism the scheme qualifier exists to remove.

10. Converting to PEPPOL

An invoice converts to PEPPOL BIS Billing 3.0 (UBL 2.1) — App\Services\Peppol\DekraPeppolConverter, CONVERTIBLE. Every other type is refused, including the other billing types: a credit note is a different UBL document (CreditNote, type code 381), not an Invoice with a flag, and this converter does not produce one yet. Emitting an Invoice for anything but an invoice would be a well-formed file that lies about what it is. Receipts are refused for a second reason — BIS needs a named buyer with a registration number and an address, and a till receipt prints none of it, so no amount of correcting would make one pass.

Conversion produces a document, not a transmission. Sending over PEPPOL requires an accredited Access Point, which this portal does not have.

What the mapping does that is not obvious:

DEKRA UBL Why
issuer / recipient AccountingSupplierParty / AccountingCustomerParty One fixed meaning, so no role juggling
party.country Country/IdentificationCode Read, never guessed. An absent country produces no code and a reported gap, rather than a default that silently decides the document's tax treatment
identifiers[] PartyIdentification, PartyTaxScheme, EndpointID The register is in the scheme, so a US or Indian party maps as readily as a Latvian one
totals.discount_percent folded into each line BIS validates sum(lines) = LineExtensionAmount; a discount left at document level makes the two disagree by exactly the discount
items[].taxes[] one ClassifiedTaxCategory BIS models one recoverable consumption tax per line. A withholding or excise entry is reported as a gap rather than mapped into the VAT slot, which would state something false about what the buyer owes
payment.paid_amount PrepaidAmount A partly paid invoice then shows the right PayableAmount
comment Note The document's own note, verbatim, and nothing else — no type label prepended
items[].comment not mapped BIS has no per-line note the holder's remark would honestly fit
endpoint_id / endpoint_scheme EndpointID with schemeID Used as printed. Only when the document states none does the converter fall back to the VAT or registration number under the party's own country's EAS scheme — a routing convention of the converter, never written back into the signed body

problems() also refuses a currency other than EUR: the converter does not carry tax-currency conversion, and a non-EUR invoice would map to a message whose VAT totals it cannot vouch for.

problems() lists which BT/BG fields are missing, and the document page shows them next to the download — a file that will be rejected is worth knowing about before it is sent, not after.

11. What DEKRA deliberately does not do

  • No encryption. Anything private must not be put in a .dkr file that leaves a trusted channel.
  • No attachments. The original PDF or photo stays where it is; DEKRA carries the data and the original's hash, not the scan.
  • No accounting semantics. Ledger accounts and deduction rights are the receiver's own rules.
  • No transport. Producing the file and delivering it are separate problems.
  • No tax calculation. DEKRA records the tax a document states. It does not decide what should have been charged — that depends on facts no document carries.