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:
- One message per sale, a
Trzbaelement in the namespacehttp://fs.gov.cz/eet/schema/v4, posted over HTTPS with TLS 1.2 or higher. - A WS-Security signature over the SOAP body, with exclusive canonicalization, SHA-256 and RSA, and the taxpayer certificate carried in a
wsse:BinarySecurityToken. - The taxpayer certificate is a PKCS#12 file, and its common name is the taxpayer identifier.
- The whole envelope must not exceed 12 kB, which is 12,288 bytes.
- The answer carries the pok, the receipt time and any warnings, or an error code.
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:
- Validates every field of the
TsgcEETSalerecord against the schema rules, so an invalid sale is refused locally with a readable reason and never reaches the service. - Builds the
Trzbaelement and wraps it in a SOAP 1.1 envelope, with a newuuid_zpravyfor the message. - 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.
- Measures the finished envelope against the 12 kB ceiling before anything is sent.
- 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.
- Parses the answer: the pok, the receipt time, the test flag, the warnings and the error code.
- 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:
- Call
LoadFromFile. SettingFileNameandPasswordonly records the values, andBuildMessagerefuses to build a message without a certificate. TaxpayerEICmust equal the common name of the signing certificate, otherwise the answer carries warning 1. ReadingCertificate.SubjectCNguarantees it.- A registration unit number that is really assigned has at least two digits and ends in 1, 2, 3 or 4. A made up value such as 1 passes validation but earns warning 6.
- Read an amount typed by a cashier with
sgcEETParseAmount, notStrToCurr, which follows the machine decimal separator.
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:
| Outcome | Meaning |
|---|---|
eoAcknowledged | The sale is reported and the pok is in TsgcEETResponse.POK. This is the only outcome that reports a sale. |
eoVerified | The verification mode success. Nothing was filed. |
eoRejected | Everything 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:
BuildMessagevalidates, builds, signs and measures the envelope without sending it. Theuuid_zpravyit used is inLastMessageUUID.SendRawposts a stored envelope unchanged, its originaluuid_zpravyandprvni_zaslaniincluded, which is right for a message that never left the machine.Resendreplays a sale that was sent but never acknowledged, with a freshuuid_zpravyandprvni_zaslaniset to false, which is the retry the specification describes.
// 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
- C++Builder uses the same component, and
Demos\CBuilder\EETmirrors the Delphi demo. - .NET has
TsgcEETClientwith the same API, and a WinForms demo indemos\EET. - sgcSign Server adds
POST /api/v1/sign/eet, which builds and signs a sale with a key the server holds, and can submit it and return the answer. A chain of tills can then share one taxpayer certificate kept on the server instead of every till holding a copy. - The
sgcsigncommand line tool has the matchingeetverb. It reads the sale from a JSON file, and its exit code tells a till script what happened: 6 means the tax authority rejected the sale, 5 means the server could not sign it or could not reach the tax authority, and 4 means the sgcSign server itself could not be reached.
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.
