AI translation
This page was translated from German with AI. If in doubt, the German original applies.
Consignment note by role¶
This page follows a consignment note (Begleitschein) through all parties and shows, for each role, what its system does: fetch, create its own layer, enter details, have it signed, save and pass it on. Why the steps follow one another in this order is explained in Procedure and roles. The individual calls with all parameters and their REST form are described under Consignment note and acceptance note.
The examples are C# against Zedal.Sdk. Each role stands on its own: a carrier system only needs the common building blocks and the section Carrier.
Common building blocks¶
Setup¶
using System;
using System.Linq;
using System.Threading.Tasks;
using Google.Protobuf.WellKnownTypes; // Timestamp
using Infotech.Zedal.Bxa.Bmu.V1.Api; // XmlOptions
using Infotech.Zedal.Core.V1.Api; // DocumentId, FetchDocumentNumbersRequest
using Infotech.Zedal.Documents.V1; // Document, StringField, DoubleField, BoolField, TimestampField, FieldMeta
using Infotech.Zedal.Documents.V1.Bmu; // BGSDokument, UNSDokument, Nummer, Beteiligter, Adresse, Befoerderer, Entsorger, LayerType, Rolle
using Zedal.Sdk;
using Zedal.Sdk.DocumentHandler; // BmuDocumentHandler
In all examples, the logged-in IZedalClient is called client (Getting started).
Field values¶
UpdateDocument only writes values with Meta.InLayer = true and ignores all others without an error (Writing fields). A few small helpers keep the examples readable:
static FieldMeta InLayer() => new FieldMeta { InLayer = true };
static StringField Text(string value) => new StringField { Value = value, Meta = InLayer() };
static DoubleField Amount(double value) => new DoubleField { Value = value, Meta = InLayer() };
static BoolField Flag(bool value) => new BoolField { Value = value, Meta = InLayer() };
// Date fields are timestamps. For a calendar day, you pass midnight UTC of that day.
static TimestampField Date(DateTime day) => new TimestampField
{
Value = Timestamp.FromDateTime(DateTime.SpecifyKind(day.Date, DateTimeKind.Utc)),
Meta = InLayer()
};
// Number without check digit; the service calculates the check digit (XmlOptions.Pruefziffer = Calculate).
static Nummer Number(string number) => new Nummer { Nr = Text(number) };
// A party with official number, name and address from your master data.
static Beteiligter Party(MasterData m) => new Beteiligter
{
BehoerdlicheNr = Number(m.Number),
Adresse = new Adresse
{
Name1 = Text(m.Name),
Strasse1 = Text(m.Street),
Hausnummer = Text(m.HouseNumber),
Plz = Text(m.PostalCode),
Ort1 = Text(m.City),
Staat = Text(m.Country) // two uppercase letters, such as "DE"
}
};
MasterData, Transport and the other input types in the examples stand for the data from your system. Protobuf fields do not accept null: omit a value you do not have entirely instead of passing Text(null).
Fetching incoming documents¶
A consignment note that another party saves and forwards (vermittelt) reaches you in one of two ways:
| Sender | Where the consignment note arrives | Fetch with |
|---|---|---|
| Party at ZEDAL | Transport paper area (Versandpapier-Bereich), in a queue | PeekNextTransportDocument, SetTransportDocumentReceived |
| Party with another provider of the German electronic waste record procedure (eANV), via the ZKS-Abfall | Inbox | inbox importer (ImportTransportDocumentFromInbox) |
Your system therefore polls both paths. The queue of the transport paper area only advances after your acknowledgment:
// Fetches the next delivered transport document and loads it as a BMU document.
// Returns null if nothing is pending.
async Task<BmuDocumentHandler> NextIncoming()
{
var (docId, logId) = await client.CoreApi.Documents.PeekNextTransportDocument();
if (string.IsNullOrEmpty(docId?.Id))
return null;
byte[] xml = await client.CoreApi.Documents.ExportCurrentDocument(docId, logId);
var bmu = client.DocumentHandler.CreateBmuHandler();
await bmu.Load(xml);
await SaveInOwnSystem(bmu.DocXml); // save first ...
await client.CoreApi.Documents.SetTransportDocumentReceived(docId, logId); // ... then acknowledge
return bmu;
}
The SDK’s inbox importer imports the inbox into the transport paper area. After that, you load the imported documents as above:
var importer = client.DocumentHandler.CreateInboxImporter();
var report = await importer.ImportAsTransportAsync(new[] { "BGS", "UNS" });
foreach (var entry in report.Imported)
{
byte[] xml = await client.CoreApi.Documents.ExportCurrentDocument(entry.LocalDocId);
// load and save as in NaechsterEingang
}
// bericht.Failed: entries that could not be imported; they remain in the inbox
Four rules for fetching:
- Save first, then acknowledge. Until you acknowledge,
PeekNextTransportDocumentreturns the same entry again and again. If your processing aborts in between, nothing is lost. An entry you never acknowledge, however, stalls the queue. - The queue contains all transport documents, not just consignment notes. Check the document type before you treat an entry as a BMU document (
bmu.Document.DocumentsCase). - Every new version arrives again. ZEDAL forwards on every save, so the same consignment note is delivered to you again after each additional layer. Match incoming documents by the consignment note number and decide from the state of the layers whether there is anything for you to do.
- Importing from the inbox is only available via gRPC and the SDK, not via REST (Limits of the REST path).
There is no push channel yet; how often to poll is described under Webhooks and event notification.
Is it my role’s turn?¶
The layers, with their signatures, are in Document.Meta.Layers. Before you append your layer, check whether your predecessor has signed:
static bool IsSigned(BmuDocumentHandler bmu, string layerType) =>
bmu.Document.Meta.Layers.Any(l => l.Type == layerType && l.Signatures.Count > 0);
The type names are those of the BMU schema: BGSVorlageLayer, BGSERZLayer, BGSBEFLayer, BGSZWLLayer, BGSENTLayer, BGSErgaenzungsLayer and BGSBEHLayer.
The API does not enforce the order of signatures
AddLayer appends a layer even if the previous one has not been signed yet. Your system must ensure that each party only takes over after its predecessor has signed, for example with IsSigned as above.
Signing¶
Each role has its layer signed with a qualified signature by a human. In the examples, a single call stands for this:
// Has a human sign the top layer and loads the signed XML into the handler.
Task Sign(BmuDocumentHandler bmu, Rolle role);
How to implement it with the WebSigner and what already works there today is described in the section Signing at the end of this page.
Numbers and templates¶
Every consignment note and every acceptance note (Übernahmeschein) needs a number from the system of the German states. ZEDAL fetches the numbers centrally and issues them via FetchNumbers:
string bgsNr = (await client.CoreApi.Common.FetchNumbers(1,
FetchDocumentNumbersRequest.Types.NumberType.BmuBgs)).Single();
// 14 characters: "1" and 13 digits, without check digit
Only fetch numbers when you need them
ZEDAL permanently assigns every fetched number to your account; a second fetch returns different numbers. If you hold too many fetched, not yet used numbers from the last 14 days, usually 100, ZEDAL rejects further fetches. So only fetch numbers when you create the consignment note, and do not keep a large stock. If ZEDAL’s stock is running low, a fetch may also return fewer numbers than requested, without an error; check the count.
FetchNumbers is only available via gRPC and the SDK, not via REST.
Instead of from data, a consignment note can be created from a ZEDAL template stored in your account. How to list and load templates is described under From a ZEDAL template. After that, continue as for the waste producer from step 2.
Waste producer¶
The waste producer creates the consignment note, enters the transport details and signs no later than at handover to the carrier.
async Task<DocumentId> CreateConsignmentNote(Transport t)
{
var bmu = client.DocumentHandler.CreateBmuHandler();
string bgsNr = (await client.CoreApi.Common.FetchNumbers(1,
FetchDocumentNumbersRequest.Types.NumberType.BmuBgs)).Single();
// 1. Template layer: everything already known before pickup, with all parties
var bgs = new BGSDokument
{
BgsNr = Number(bgsNr),
NachweisNr = Number(t.CertificateNumber),
Abfallschluessel = Text(t.WasteCode), // six digits, without spaces
Abfallbezeichnung = Text(t.WasteDescription),
Erzeuger = Party(t.Producer),
Befoerderer1 = new Befoerderer { Beteiligter = Party(t.Carrier) },
Entsorger = new Entsorger { Beteiligter = Party(t.Disposer) }
};
if (t.InterimStorage != null)
bgs.Zwischenlager = new Zwischenlager { Beteiligter = Party(t.InterimStorage) };
await bmu.Create(new Document { Bgs = bgs }, vorlageLayer: true, new XmlOptions
{
AtbRolle = Rolle.Erz,
Pruefziffer = XmlOptions.Types.Pruefziffer.Calculate,
Validation = XmlOptions.Types.Validation.None // a template layer has no mandatory fields
});
// 2. Append the producer layer and enter the handover details
await bmu.AddLayer(LayerType.Erzeuger);
await bmu.UpdateDocument(new Document
{
Bgs = new BGSDokument
{
Menge = Amount(t.QuantityTons),
Erzeuger = new Beteiligter { Datum = Date(t.HandoverDate) }
}
});
// 3. A human at the waste producer signs
await Sign(bmu, Rolle.Erz);
// 4. Save and forward to carrier and disposer
return await bmu.SubmitTransportDocumentToZedal(distribute: true);
}
What matters here:
- All parties belong in the consignment note before it is saved. ZEDAL forwards to the parties whose official number is in the document. If the number of the disposer (Entsorger), an interim storage facility (Zwischenlager) or a second carrier is missing, that party receives nothing.
- Template layer (Vorlage) and producer layer are two steps. The template layer captures what is already known and needs the role of the party creating it. The producer layer contains the details the waste producer vouches for with their signature, such as the quantity. Instead of from data, the template layer can also come from a ZEDAL template.
- Validation: The template layer is created without validation.
AddLayerdoes not validate, whereasUpdateDocumentwithout options validates fully. An error arrives asCANCELLED(Error codes → BMU). - Quantities in metric tons, volumes in cubic meters, each with at most three decimal places (Field rules).
Carrier¶
The carrier fetches the consignment note, appends its layer, enters the takeover and the vehicle, and signs no later than at takeover.
async Task ConfirmTakeover(BmuDocumentHandler bmu, Trip f)
{
if (!IsSigned(bmu, "BGSERZLayer"))
throw new InvalidOperationException("The waste producer has not signed yet.");
// 1. Append the carrier layer and enter the takeover
await bmu.AddLayer(LayerType.Befoerderer);
await bmu.UpdateDocument(new Document
{
Bgs = new BGSDokument
{
Befoerderer1 = new Befoerderer
{
Beteiligter = new Beteiligter { Datum = Date(f.TakeoverDate) },
Kfz = Text(f.LicensePlate)
}
}
});
// 2. The driver signs at takeover
await Sign(bmu, Rolle.Bef);
// 3. Save and forward: waste producer and disposer see the takeover
await bmu.SubmitTransportDocumentToZedal(distribute: true);
}
The method is called with the handler from NextIncoming as soon as the pickup is due.
- A trailer goes into
KfzAuflieger; both license plates have at most ten characters. - Second and third carriers write to
Befoerderer2andBefoerderer3respectively. Whoever takes over first checks that the layer of their predecessor is signed: carrier or interim storage facility. - Signing on the road: The signing link can be opened on any device, including the driver’s phone. What this looks like is described under WebSigner → Integrating it into your application.
- Signing later, after the takeover, is only permitted with a written agreement with the predecessor (Section 19(2) of the German ordinance on waste records, Nachweisverordnung (NachwV)).
Interim storage facility¶
The interim storage facility takes over from the first carrier and hands over to another one. It only receives the consignment note if it is already in the document, usually since its creation.
async Task ConfirmStorage(BmuDocumentHandler bmu, Storage l)
{
if (!IsSigned(bmu, "BGSBEFLayer"))
throw new InvalidOperationException("The carrier has not signed yet.");
await bmu.AddLayer(LayerType.Zwischenlager);
await bmu.UpdateDocument(new Document
{
Bgs = new BGSDokument
{
Zwischenlager = new Zwischenlager
{
Beteiligter = new Beteiligter { Datum = Date(l.TakeoverDate) },
UebergabeDatum = Date(l.HandoverDate)
}
}
});
await Sign(bmu, Rolle.Zwl);
await bmu.SubmitTransportDocumentToZedal(distribute: true);
}
After that, the next carrier appends its layer and writes to Befoerderer2. An interim storage facility layer is always followed by a carrier, never directly by the disposer.
Disposer¶
The disposer accepts the waste, enters the acceptance date and the weighed quantity, signs without delay and completes the consignment note. When it is saved, it goes to the authorities and to all other parties.
async Task ConfirmAcceptance(BmuDocumentHandler bmu, Acceptance a)
{
if (!IsSigned(bmu, "BGSBEFLayer"))
throw new InvalidOperationException("The carrier has not signed yet.");
// 1. Append the disposer layer and enter the acceptance
await bmu.AddLayer(LayerType.Entsorger);
await bmu.UpdateDocument(new Document
{
Bgs = new BGSDokument
{
Menge = Amount(a.WeighedQuantityTons),
Entsorger = new Entsorger
{
Beteiligter = new Beteiligter { Datum = Date(a.AcceptanceDate) },
AnnahmeVerweigert = Flag(false)
}
}
});
// 2. Sign without delay after acceptance
await Sign(bmu, Rolle.Ent);
// 3. Save, forward to all parties and report to the authorities
await bmu.SubmitTransportDocumentToZedal(distribute: true, dekade: true);
}
dekade: truereports the consignment note to the authorities; the name comes from the ten-day report (Dekadenmeldung). ZEDAL sends it via the ZKS-Abfall, but not immediately: at the earliest on the day after saving and usually 10 days after the acceptance date. Only a signed consignment note whose top layer is the disposer layer or a supplement layer (Ergänzungslayer) and that carries an acceptance date is reported. ZEDAL never reports canceled consignment notes.- Reporting must be set up for your account. ZEDAL only reports for disposer numbers that are registered with your account for the ten-day report. Clarify this with ZEDAL support before you go live; otherwise no report is sent, and there is no error message.
distribute: truedelivers the completed consignment note to the waste producer and the carriers (Section 19(4) NachwV).- Acceptance refused: Set
AnnahmeVerweigert = Flag(true)and give the reason for the refusal inVermerke.
distribute and dekadenmeldung are independent of each other
distribute: false only suppresses delivery to the parties, not the report to the authorities. Also, via gRPC or REST, a flag that is not set counts as true; the SDK, by contrast, sets both to false if you do not specify them. So always specify both explicitly.
Collector¶
In collective disposal (Sammelentsorgung), the collector (Einsammler) picks up from many waste producers. One acceptance note is created per waste producer, and one consignment note for the entire tour (Procedure → Collective disposal).
Acceptance note at the waste producer¶
async Task<string> CreateAcceptanceNote(Pickup pickup)
{
var uns = client.DocumentHandler.CreateBmuHandler();
var producer = Party(pickup.Producer);
producer.Datum = Date(pickup.Date);
var collector = Party(pickup.Collector);
collector.Datum = Date(pickup.Date);
string unsNr = (await client.CoreApi.Common.FetchNumbers(1,
FetchDocumentNumbersRequest.Types.NumberType.BmuUns)).Single();
await uns.Create(new Document
{
Uns = new UNSDokument
{
UnsNr = Number(unsNr),
NachweisNr = Number(pickup.CollectiveCertificateNumber), // SN...
Abfallschluessel = Text(pickup.WasteCode),
Abfallbezeichnung = Text(pickup.WasteDescription),
Menge = Amount(pickup.QuantityTons),
Erzeuger = producer,
Befoerderer = new Befoerderer { Beteiligter = collector, Kfz = Text(pickup.LicensePlate) },
Entsorger = new Entsorger { Beteiligter = Party(pickup.Disposer) }
}
}, vorlageLayer: false, new XmlOptions
{
AtbRolle = Rolle.Bef, // mandatory for the acceptance note: ERZ, BEF or ENT
Pruefziffer = XmlOptions.Types.Pruefziffer.Calculate
});
await Sign(uns, Rolle.Bef);
await uns.SubmitTransportDocumentToZedal(distribute: true);
return unsNr;
}
The waste producer and the collector fill in the acceptance note together (Section 12(3) NachwV); when it is kept electronically, both sign it with a qualified signature. By way of exception, the acceptance note may also be kept on the paper form (Section 21 NachwV).
The acceptance note has one carrier and no interim storage facility. Further layers cannot yet be appended to an acceptance note via the API (Acceptance note).
Consignment note for the collection tour¶
The collector enters itself as the carrier and sets the fictitious producer number in the producer field, made up of the state code, “S” and zeros (Section 13(1) NachwV). The tour’s acceptance notes go into UnsNrn:
var bgs = new BGSDokument
{
BgsNr = Number(bgsNr),
NachweisNr = Number(tour.CollectiveCertificateNumber),
Abfallschluessel = Text(tour.WasteCode),
Abfallbezeichnung = Text(tour.WasteDescription),
Erzeuger = new Beteiligter { BehoerdlicheNr = Number(tour.StateCode + "S0000000") },
Befoerderer1 = new Befoerderer { Beteiligter = Party(tour.Collector) },
Entsorger = new Entsorger { Beteiligter = Party(tour.Disposer) }
};
bgs.UnsNrn.AddRange(tour.AcceptanceNoteNumbers.Select(Number));
From here on, the consignment note follows the path from the sections Waste producer, Carrier and Disposer. If the collector collects in several German states, it creates a separate consignment note per state (Section 13(2) NachwV).
Changing acceptance note numbers afterward
A non-empty list you send determines how long UnsNrn is in the current layer. To void a number that is in an earlier layer, enter the deletion value 29999999999999 at its index; this is what the BMU specification provides for.
Correction with a supplement layer¶
After the disposer has signed, nobody changes an existing layer anymore. Whoever needs to correct something appends a supplement layer with their role, enters the correction, signs and sends the consignment note again to the authorities and parties (Section 11(6) NachwV):
await bmu.AddLayer(LayerType.Ergaenzung, new XmlOptions { AtbRolle = Rolle.Ent });
await bmu.UpdateDocument(new Document
{
Bgs = new BGSDokument
{
Menge = Amount(correctedQuantity),
Vermerke = Text("Menge nach Nachwiegung berichtigt")
}
});
await Sign(bmu, Rolle.Ent);
await bmu.SubmitTransportDocumentToZedal(distribute: true, dekade: true);
Without AtbRolle, the service rejects the supplement layer with CANCELLED.
Cancellation¶
With cancel: true, you save the current version as a cancellation; after that, the document shows this in zusatz.storniert:
ZEDAL delivers the cancellation to the parties, and to parties with other providers as a cancellation message via the ZKS-Abfall. A canceled consignment note never goes to the authorities.
The NachwV itself has no concept of cancellation. If the waste has already been handed over, the consignment note remains valid, and a correction belongs in a supplement layer. A cancellation is appropriate for a consignment note for which there is no transport.
What ZEDAL checks when saving¶
ImportTransportDocument uses the consignment note number to look for an existing document in your document store. If there is one, a new version is created; otherwise, a new document is created. ZEDAL rejects a version
- whose signatures are broken, that is, whose content has changed after signing,
- that carries fewer layers or fewer signatures than the saved version.
For these checks, a layer without a signature is not a reason for rejection, and ZEDAL does not check whether the right party signed. Making sure that the right human has signed before you save and forward is up to your system.
Implementing signing¶
Signing takes place via the WebSigner: your system creates a signing process with the current XML and shows the human a link. There, they choose their signature device and authorize the signature with their trust service provider. Your system then retrieves the signed XML and loads it into the handler:
async Task Sign(BmuDocumentHandler bmu, Rolle role)
{
var result = await client.SignerApi.SigningProcesses.SignAsync(
new[]
{
new DocumentInput
{
DisplayName = "Begleitschein",
Kind = DocumentKind.BmuXml,
Content = ByteString.CopyFrom(bmu.DocXml)
}
},
presentSigningUrl: url => ShowSigningLink(url));
var signed = result.Results.SingleOrDefault();
if (!result.Completed || !DocumentResults.IsUsable(signed))
throw new InvalidOperationException($"Not signed: {result.SigningProcess.State}");
await bmu.Load(signed.Content.ToByteArray());
}
For this, add Google.Protobuf (ByteString), Infotech.Signer.Sign.V1 (DocumentInput, DocumentKind) and Zedal.Sdk.Apis.Signer (DocumentResults) to the namespaces.
Layer-specific signatures will follow
Today, the WebSigner signs a BMU XML as a whole. It does not yet apply a signature that completes exactly one layer for a specific role, as the examples above require. The parameter role is intended for this. Until this is released, the examples run up to creating and filling the layers; the current status is given on the WebSigner page.
Via REST¶
Every step on this page has a REST equivalent except two: fetching numbers and importing from the inbox. A pure REST integration in the eANV therefore lacks the consignment note numbers.
| Step | REST |
|---|---|
Fetch number (FetchNumbers) |
– (gRPC and SDK only) |
| Create, append layer, write fields, read | BmuDocumentApi, see Via REST |
| Save, forward, report, cancel | POST …/DocumentApi/ImportTransportDocument with distribute, dekadenmeldung, cancel |
| Fetch incoming document | GET …/DocumentApi/PeekNextTransportDocument, then GET …/DocumentApi/ExportCurrentDocument |
| Acknowledge incoming document | PATCH …/DocumentApi/SetTransportDocumentReceived |
| Sign | SignerApi, see WebSigner |
Next¶
- Procedure and roles: layer sequence, time limits, special cases
- Consignment note and acceptance note: all calls in detail
- Signing with the WebSigner: signing process, states, integration