Czech EET 2.0 in Delphi: Register Sales with TsgcEETClient

· Components
Czech EET 2.0 in Delphi: Register Sales with TsgcEETClient

The Czech Republic is bringing back the electronic registration of sales. Under EET 2.0 (Elektronická evidence tržeb) a point of sale reports every sale to the tax authority as it happens, and the tax authority answers with an acknowledgement code, the pok, which is the proof that the sale was reported. Reporting starts on 1 January 2027, and the playground where tills are built and tested is open now.

sgcSign has a new component for it, TsgcEETClient. It validates the sale, builds the message, signs it with the taxpayer certificate, sends it, checks the signature on the acknowledgement and hands back the result. This post explains what EET 2.0 asks for, how the component does the round trip, and the Delphi code for a first sale, an offline queue and a verified acknowledgement.

A registered sale from the Delphi demo, from verification mode to a real pok. Also on YouTube.

EET 2.0 is a new protocol, not an update

If you built a till for the first EET scheme, start from a clean slate. Data interface version 4.1 is not compatible with the old version 3.1, and it is simpler: there is no PKP or BKP security code to compute, no VAT breakdown and no TLS client certificate. A sale is ten data attributes. What remains is a standard SOAP 1.1 web service:

Registering as a taxpayer, obtaining the certificate through the MOJE daně portal and being assigned a registration unit number all happen before any code runs. From that paperwork the library needs one PKCS#12 file and two numbers, the taxpayer identifier and the unit identifier.

How TsgcEETClient does the round trip

One call to Send runs every step, in this order:

  1. Validates every field of the TsgcEETSale record against the schema rules, so an invalid sale is refused locally with a readable reason and never reaches the service.
  2. Builds the Trzba element and wraps it in a SOAP 1.1 envelope, with a new uuid_zpravy for the message.
  3. Signs the SOAP body under WS-Security with the key of any sgcSign key provider: a PFX file, the Windows certificate store, a PKCS#11 token or smart card, or a cloud key service.
  4. Measures the finished envelope against the 12 kB ceiling before anything is sent.
  5. Posts it to the tax authority. The playground is the default endpoint, so a component dropped on a form cannot file a real sale by accident.
  6. Parses the answer: the pok, the receipt time, the test flag, the warnings and the error code.
  7. Verifies the signature on the acknowledgement. Error responses are unsigned by design, so a rejection never turns into a signature failure.

Your first sale in Delphi

The specification tells taxpayers to start with verification mode. The message is checked in full, exactly like a real one, and then discarded, so nothing is filed. If it passes, the certificate, the signature, the TLS connection and every field of the sale are correct. The code below runs that check first and then files the sale for real.

var
  oProvider: TsgcPFXKeyProvider;
  oClient: TsgcEETClient;
  oSale: TsgcEETSale;
  oResponse: TsgcEETResponse;
begin
  oProvider := TsgcPFXKeyProvider.Create(nil);
  oClient := TsgcEETClient.Create(nil);
  try
    oProvider.FileName := 'CZ00000019.p12';
    oProvider.Password := '...';
    // Without LoadFromFile the certificate is empty and the message would
    // carry no token for the tax authority to verify the signature with.
    oProvider.LoadFromFile;
    oClient.KeyProvider := oProvider as IsgcKeyProvider;
    oClient.Environment := eetPlayground;

    sgcEETInitSale(oSale);
    oSale.SendDateTime := Now;
    oSale.SaleDateTime := Now;
    oSale.FirstSending := True;
    // The common name of an EET certificate IS the taxpayer identifier.
    oSale.TaxpayerEIC := oProvider.Certificate.SubjectCN;
    oSale.UnitID := 11;
    oSale.PosID := '1';
    oSale.ReceiptNumber := '0/6460/ZQ42';
    oSale.TotalAmount := 349;

    // Verification mode first. Nothing is filed.
    oClient.VerificationMode := True;
    oResponse := oClient.Send(oSale);
    if sgcEETResponseOutcome(oResponse) <> eoVerified then
      raise Exception.CreateFmt('Verification failed, code %d: %s',
        [oResponse.ErrorCode, oResponse.ErrorText]);

    // Now for real. Only eoAcknowledged reports a sale.
    oClient.VerificationMode := False;
    oResponse := oClient.Send(oSale);
    if sgcEETResponseOutcome(oResponse) = eoAcknowledged then
      PrintReceipt(oResponse.POK, oResponse.Test) // your own routine
    else
      // Not filed. Store the sale and replay it later with Resend.
      QueueSale(oSale); // your own routine
  finally
    oClient.Free;
    oProvider.Free;
  end;
end;

A few details decide whether that first run works:

For the playground, the tax administration publishes shared test certificates, CZ00000019 among them, on eet.gov.cz. An acknowledgement from the playground carries test="true" and a pok ending in ff, which proves nothing about a real sale.

Reading the answer

This is the part of the protocol most likely to surprise you. Every outcome arrives as HTTP 200, a rejection included, so the HTTP status tells you nothing. And a verification mode success arrives inside an error element carrying code 0, so TsgcEETResponse.IsError is True on a perfectly good verification run.

sgcEETResponseOutcome applies both rules and returns one of three answers:

OutcomeMeaning
eoAcknowledgedThe sale is reported and the pok is in TsgcEETResponse.POK. This is the only outcome that reports a sale.
eoVerifiedThe verification mode success. Nothing was filed.
eoRejectedEverything else. The sale has not been reported and is still owed to the tax authority.

Warnings are not critical. Up to ten of them can ride along with a valid acknowledgement, and OnWarning fires once for each, while OnError fires for an error whose code is not 0. After every message, log LastTransactionId, the X-Global-Transaction-Id response header, because it is the first thing EET support asks for. LastRequestXML and LastResponseXML keep both messages exactly as they travelled.

When the line is down: an offline queue

A till has to keep selling when the connection drops. TsgcEETClient splits the round trip so a point of sale can queue messages and send them later:

// The line is down: sign the message now and keep it
sEnvelope := oClient.BuildMessage(oSale);
StoreInQueue(oClient.LastMessageUUID, sEnvelope); // your own storage

// The line is back: post the stored envelope exactly as it was built
oResponse := oClient.SendRaw(sEnvelope);

// Sent earlier but no answer arrived: replay the sale as a repeat
oResponse := oClient.Resend(oSale);

One trap is worth knowing before you build the queue. The sale time is written with a time zone offset, and unless the sale record carries its own offset the library uses the offset of the machine at the moment the message is built. A July sale replayed with Resend in December would be stamped with the December offset. Store the offset with the sale, and set SaleOffsetMinutes and HasSaleOffsetMinutes when you replay it.

Verifying the acknowledgement

VerifyResponseSignature is True by default, so the signature on every acknowledgement is checked out of the box. Checking the certificate chain as well takes the right trust anchors, and they are not the obvious ones. The acknowledgement is signed by a commercial I.CA certificate, not by the EET certificates that come with the playground test material, and neither I.CA issuer is in the Windows root store. Download I.CA Root CA/RSA 05/2022 and I.CA Public CA/RSA 06/2022 from ica.cz and name them as anchors:

oClient.TrustedCertificates.Add('ica-root-ca-rsa-05-2022.cer');
oClient.TrustedCertificates.Add('ica-public-ca-rsa-06-2022.cer');
oClient.RequireTrustedChain := True;

If a check fails, LastVerificationDetails reports the step that failed. When an acknowledgement arrives but its signature does not verify, Send raises, and LastResponse still holds the parsed answer with its pok, so a sale that was already registered is never sent twice by mistake.

The 12 kB ceiling

The service refuses a message larger than 12 kB with error code 7, and BuildMessage checks the size before anything is sent. Every sale field is capped by the schema, so the only part of the envelope whose size really varies is the signing certificate in the wsse:BinarySecurityToken. That is also why the envelope carries exactly one SOAP header, and why the component offers no way to add another.

C++Builder, .NET, the server and the command line

set SGCSIGN_SERVER=https://sign.shop.local:8443
set SGCSIGN_APIKEY=sgcsk_...

sgcsign eet --provider eet-taxpayer --submit sale.json

Try it

The Delphi demo in Demos\Delphi\EET walks the whole round trip against the playground on one form. Load a test certificate, send in verification mode, then untick it and send a real sale to get an acknowledgement with its pok. Build Message (no send) shows the signed envelope an offline queue would store, and Resend Stored Sale replays the last sale as a repeat submission. The test certificates are not included with the demo, because the document that hands them out is restricted, so download them from eet.gov.cz.

Every property, method and event is documented in the sgcSign online help, and the EET 2.0 section of the sgcSign country profiles page sums up the component.

Availability

TsgcEETClient ships in sgcSign 2026.10 for Delphi, C++Builder and .NET, together with the sgcSign Server route and the eet verb of the command line tool.

Questions, or a till that has to be ready for January? Get in touch. If something does not behave as you expect, send the request and response XML together with the X-Global-Transaction-Id, and you will get a reply from the people who wrote the code.