This repository provides a C# implementation for integrating with a fiscalization system using classes generated by Protobuf. The process includes constructing fiscal receipts (Citizen and POS Coupons), digitally signing them, and submitting them to the fiscalization service. This guide walks you through the steps necessary to integrate and execute the solution.
- Project Overview
- Getting Started
- Generating PROTOBUF models
- Model Explanation
- PKI Key Generation
- Digital Signing
- Sending Data to Fiscalization Service
- Running the Application
This project provides a set of C# classes to interact with a fiscalization system. The key components include:
- Models: This is the models generated by Protobuf
- ModelBuilder: Constructs the Citizen and POS coupons (receipts) using predefined tax groups, items, and payment methods.
- Signer: Signs the receipts using a digital signature created with an ECDSA private key.
- Fiskalizimi: Contains methods for constructing, signing, and sending fiscal coupons to the fiscalization service.
- Protobuf: Used for serializing the data models (CitizenCoupon, PosCoupon) to binary.
- ECDSA: Elliptic curve algorithm used for digital signatures.
- HttpClient: For sending data to the fiscalization service.
Before integrating the system, ensure you have the following installed:
- .NET SDK
- Protobuf Compiler
- A valid ECDSA private key for signing the data.
- Clone this repository:
git clone https://github.com/fiskalizimi/pos-csharp.git cd fiskalizimi-integration
To manually generate C# classes from a .proto file using protoc, the Protobuf compiler, you will first need to have the Protobuf tools
installed on your system. The protoc compiler is responsible for compiling .proto files into language-specific classes, including C#. Follow these steps to generate C# classes manually:
- Install the Protobuf compiler: If you don't have
protocinstalled, download and install it from the official Protobuf releases. Ensure the executable is in your system's PATH. - Download the C# plugin: You need to download the Protobuf C# plugin if it's not bundled with
protoc. It can be found in the official releases or by installing theGrpc.Toolspackage in a .NET project. - Run the protoc command: Use the following command in your terminal to generate the C# classes from the
models.protofile. Replace paths accordingly for your setup:protoc --proto_path=./path/to/protos --csharp_out=./path/to/output ./path/to/protos/models.proto--proto_path=./path/to/protos: Specifies the directory where your .proto files are located.--csharp_out=./path/to/output: Specifies the directory where the generated C# files should be saved.models.proto: The.protofile you are compiling.
- Generated C# classes: After running the
protoccommand, it will generate C# classes corresponding to the Protobuf messages and enums defined in themodels.protofile. The classes will contain methods likeParseFrom(),ToByteArray(), and properties representing each field in the messages. - Include the generated classes in your project: After generating the C# files, you can manually add them to your .NET project by copying them into your solution’s directory or directly referencing them in your project’s code.
To generate Protobuf models in .NET 8.0 from the models.proto file, you'll first need to install the Google.Protobuf package
and the Grpc.Tools package, which provides the necessary tooling to compile .proto files into C# classes. This process leverages the Protobuf compiler (protoc),
which takes the .proto definition and generates C# model classes for use in your .NET project.
Here are the steps to generate Protobuf models:
-
Install the required NuGet packages:
- Install Google.Protobuf to use Protobuf runtime classes.
- Install Grpc.Tools to compile .proto files.
Run the following command in your project:
dotnet add package Google.Protobuf dotnet add package Grpc.Tools -
Add the .proto file to your project: Place your
.protofile (in our casemodels.proto) inside your project directory, usually in aProtosfolder for organization. In the.csprojfile, reference the.protofile to instruct the compiler to generate the necessary C# classes. -
Modify your .csproj file to include Protobuf file generation instructions:
<ItemGroup> <Protobuf Include="Protos/models.proto" GrpcServices="None" /> </ItemGroup>Setting
GrpcServices="None"ensures that only data models are generated, without gRPC service code, since we are only interested in the models (e.g.,PosCoupon,CitizenCoupon,Payment, etc.). -
Build the project: Run the following command to compile the
.protofile and generate the C# classes:dotnet buildThis will automatically generate C# classes that correspond to the Protobuf messages (like
PosCoupon,CitizenCoupon,CouponItem, etc.) in your.protofile.
The CitizenCoupon represents a simplified receipt that will be the part of QR Code. Below is the example structure created by the ModelBuilder class:
public CitizenCoupon GetCitizenCoupon()
{
var citizenCoupon = new CitizenCoupon
{
BusinessId = 1,
PosId = 1,
BranchId: 1,
CouponId = 1234,
Type = CouponType.Sale,
Time = new DateTimeOffset(2024, 10, 1, 15,30, 20, TimeSpan.Zero).ToUnixTimeSeconds(),
Total = 1820,
TaxGroups =
{
new TaxGroup { TaxRate = "C", TotalForTax = 450, TotalTax = 0 },
new TaxGroup { TaxRate = "D", TotalForTax = 320, TotalTax = 26 },
new TaxGroup { TaxRate = "E", TotalForTax = 1050, TotalTax = 189 }
},
TotalTax = 215,
TotalNoTax = 1605
};
return citizenCoupon;
}
The Citizen Coupon includes:
- BusinessId which is NUI of the business (received from ATK)
- PosId is the unique id of the POS. POS is the computer/till that has the POS system installed. Each POS unit must have a unique ID.
- BranchID is the unique id of Branch where the POS system is located
- CouponId is the unique identifier of the fiscal coupon generated by POS system
- Type this is the type of the coupon. It is an enum value and can be
SALE,RETURNorCANCEL - Time the time fiscal coupon is issued. The value is Unix timestamp
- Total that represents the total value to be paid by customer
- TaxGroups is an array of
TaxGroupobjects. EachTaxGroupobject represents the details about tax category - TotalTax is the amount of the tax in total that customer will have to pay
- TotalNoTax is the total amount without tax that customer will have to pay
Warning
NOTE: These details must match the POS Coupon details, otherwise the coupon will be marked as FAILED VERIFICATION !
The PosCoupon includes all details of the POS Coupon that will be printed and given to the customer located in ModelBuilder class
public PosCoupon GetPosCoupon()
{
var posCoupon = new PosCoupon
{
BusinessId = 1,
PosId = 1,
CouponId = 1234,
BranchId = 3,
Location = "Prishtine",
OperatorId = "Kushtrimi",
ApplicationId = 1,
ReferenceNo = 0,
VerificationNo = "1234567890123456",
Type = CouponType.Sale,
Time = new DateTimeOffset(2024, 10, 1, 15,30, 20, TimeSpan.Zero).ToUnixTimeSeconds(),
Items =
{
new CouponItem { Name = "uje rugove", Price = 150, Unit = "cope", Quantity = 3, Total = 450, TaxRate = "C", Type = "TT" },
new CouponItem { Name = "sendviq", Price = 300, Unit = "cope", Quantity = 2, Total = 600, TaxRate = "E", Type = "TT" },
new CouponItem { Name = "buke", Price = 80, Unit = "cope", Quantity = 4, Total = 320, TaxRate = "D", Type = "TT" },
new CouponItem { Name = "machiato e madhe", Unit = "cope", Price = 150, Quantity = 3, Total = 450, TaxRate = "E", Type = "TT" }
},
Payments =
{
new Payment { Type = PaymentType.Cash, Amount = 500 },
new Payment { Type = PaymentType.CreditCard, Amount = 1000 },
new Payment { Type = PaymentType.Voucher, Amount = 320 }
},
Total = 1820,
TaxGroups =
{
new TaxGroup { TaxRate = "C", TotalForTax = 450, TotalTax = 0 },
new TaxGroup { TaxRate = "D", TotalForTax = 320, TotalTax = 26 },
new TaxGroup { TaxRate = "E", TotalForTax = 1050, TotalTax = 189 }
},
TotalTax = 215,
TotalNoTax = 1605,
TotalDiscount = 75
};
return posCoupon;
}
The POS Coupon includes:
- BusinessId which is NUI of the business (received from ATK)
- PosId is the unique id of the POS. POS is the computer/till that has the POS system installed. Each POS unit must have a unique ID.
- CouponId is the unique identifier of the fiscal coupon generated by POS system. CouponId has to be unique for Business (across all branches)
- BranchID is the unique id of Branch where the POS system is located
- Location is the location/city of the Sale Point
- OperatorId is the ID/Name of the operator/server
- ApplicationId is the unique ID of the Application/POS System used. This code is provided by the Software provider that has implemented the POS Solution.
- ReferenceNo is the number of the original coupon when there is a return or cancellation of a coupon. Otherwise the field is value should be 0.
- VerificationNo is a unique value for each coupon, and it is 16 characters long max. Verification Number is used to check if the Coupon has been verified by the citizen.
- Type this is the type of the coupon. It is an enum value and can be
SALE,RETURNorCANCEL - Time the time fiscal coupon is issued. The value is Unix timestamp
- Items is an array of
CouponItemobjects. EachCouponItemrepresents an item sold to the customer. - Payments is an array of
Paymentthat represent the types of the payment methods and the amoun used by customer to pay for the goods. The valid types are:Cash,CreditCard,Voucher,Cheque,CryptoCurrency, andOther. - Total that represents the total value to be paid by customer
- TaxGroups is an array of
TaxGroupobjects. EachTaxGroupobject represents the details about tax category - TotalTax is the amount of the tax in total that customer will have to pay
- TotalNoTax is the total amount without tax that customer will have to pay
- TotalDiscount is the total amount of discount the customer received for this transaction
Upon receiving the POS Coupon, Fiscalisation Service will return a uniques uint64 value called TransactionNo. The response will be a JSON:
{
"message" : "string" // The message
"transaction_id: "uint64" // The unique Transaction ID for this coupon
}
Warning
NOTE: These details must match the Citizen Coupon details, otherwise the coupon will be marked as FAILED VERIFICATION !
There are different ways to generate a PKI key pair, depending on the operating system.
Warning
WARNING! Each POS system (PC/till) needs to have a unique ID and its own PKI key pair. The private key should never leave the machine that it is generated on !!!
To onboard your business, you need the following information:
- NUI of the business
- Fiscalization Number - (this is obtained from EDI)
- Pos ID - each POS should have a unique ID which is a numeric value
- Branch ID - is the unique id of Branch where the POS system is located
- Application ID - obtained from the ATK upon certifying the POS Application
We have provided a tool that simplifies the process a lot by creating the key pair, generating a CSR and sending the CSR to ATK Certificate Authority to be digitally signed and verified.
If you have cloned this repository the tool for different operating systems is located under the folder onbarding or, alternatively to download the tool on you machine, click on one of the links below (depending on the operating system you are using):
Once you have downloaded the onboarder tool, and extracted/unzipped it to a folder, then you need to run the application.
You need to provide an environment flag as an argument to the executable. For testing purposes the environment value should be TEST, and for production the environment value should be PROD
For example
On Windows Platform you need to open a command prompt then execute the application like the example below (for test change environment to TEST):
onboarder.exe -env=PRODOn linux/macos you need to open a terminal and then exeucte the application like the example below (for test change environment to TEST):
./onboarder -env=PRODif everything went Ok, then you will get a success message:
To view certificate and private key in PEM format, on the Certificate tab, first tick the Show private key checkbox, then click on the Show Certificate button:
To extract certificate and private key in PEM format, on the Certificate tab, first tick the Show private key checkbox, then click on the Export Certificate button.
This action will create another two files in the folder private-key.pem and signed-certificate.pem
To use the API, you need to create new ECDSA private key using the P-256 elliptic curve and a secure random number generator. A sample code of how to generate a private key is on Pki.cs and Program.cs classes.
The next step is to get VerificationCode from the Fiscalisation Service. To get the VerificationCode a POST request needs to be sent to the https://fiskalizimi.atk-ks.org/ca/verify/{nui} (for test the url is: https://fiskalizimi-test.atk-ks.org/ca/verify/{nui}) and JSON body of:
{
"fiscalization_no" : "string" // The fiscalization number from EDI
"pos_id" : "uint64" // The Pos ID to be registered
"branch_id" : "uint64" // The Branch ID where the POS is located
"application_id" : "uint64" // The Application ID
}
The response will be a JSON:
{
"business_name" : "string" // The name of the Business (to be used in CSR)
"verification_code: "uint64" // The Verification Code (to be used in CSR)
}
Once you have the privte key, business name and verification code then you need to generate a CSR with the following information:
- Country: "RKS" (or "XK" if only 2 letters to be used for country)
- Organization: Business ID
- Organization Unit: Pos ID
- Locality: Branch ID
- CommonName: The name of the business
The CSR needs to be in .pem format. A sample of a CSR is below:
-----BEGIN CERTIFICATE REQUEST-----
MIHwMIGWAgEAMDQxDDAKBgNVBAYTA1JLUzEKMAgGA1UEChMBMTEYMBYGA1UEAxMP
Rml0aW0ncyBDb21wYW55MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEhWoAnHs6
/2EWf2bvtHrJwQXxtap8QjJlTbI3Y/eSvmtaBWdJyhs9QsakDLYfSytcyxbYDsYT
+uuo1knlR2xL2qAAMAoGCCqGSM49BAMCA0kAMEYCIQD3XmlSMXXlCoGL1i8FvjpM
7cEFG0caI8lo6gwQvHy3jwIhAKP4m5nnbncPANmp++Z3vMFsSsua4iybjs7WYofX
tAiM
-----END CERTIFICATE REQUEST-----
A sample code of how to generate a CSR is on Pki.cs and Program.cs classes.
After the CSR is generated and signed with the private key, then a POST request is sent to the https://fiskalizimi.atk-ks.org/ca/signcsr (for test the url is: https://fiskalizimi-test.atk-ks.org/ca/signcsr) endpoint with the following JSON:
{
"business_name" : "string" // name of the business
"business_id" : "uint64" // Business ID (which is same as NUI)
"branch_id" : "uint64" // Branch ID
"verification_no": "uint64" // Verification Code
"pos_id" : "uint64" // Pos ID
"application_id": "uint64" // The Application ID
"csr" : "string" // CSR in .pem format
}
If everything is ok, a response will be retrieved with the signed certificate:
{
"signed_certificate" : "string" // Signed Certificate in .pem format
}
This completes the onboarding step.
Note
For a complete example in C# .NET 8, a member of community Gazmend Mehmeti has shared a repo in Github
Warning
WARNING ! Make sure to keep private key safe.
Before the data is sent to the Fiscalization System, the POS Coupon details need to be digitally signed using the private key to ensure the authenticity and integrity of the data transmitted to the fiscalization system.
The fiscalization system requires each coupon to be signed digitally before submission to ensure:
- Data Integrity: Ensures that the data sent to the fiscalization service has not been tampered with during transmission.
- Authentication: Confirms that the coupon is issued by a legitimate entity (in this case, your business), preventing fraudulent submissions.
- Non-Repudiation: Guarantees that the sender cannot deny sending the data once it has been signed and submitted.
The digital signature is generated using a private key, and the fiscalization service verifies the signature using a corresponding public key. If the signature is valid, the coupon is considered authentic.
The steps to provide a valid signature are:
- Serialization: First, the coupon (either a Citizen or POS coupon) is serialized into a Protobuf binary format. This format ensures that the data can be transmitted efficiently and consistently.
byte[] posCouponProto = posCoupon.ToByteArray(); - Base64 Encoding: The serialized Protobuf binary data is then encoded into a Base64 string. Base64 is a binary-to-text encoding scheme that makes it easy to transfer data as a string format.
var base64EncodedProto = Convert.ToBase64String(posCouponProto); - Hashing: Before signing, the data is hashed using a SHA-256 cryptographic hash function. Hashing converts the coupon data into a fixed-length string of bytes, ensuring that even a small change in the original data will produce a completely different hash value.
var sha256 = SHA256.Create(); var hash = sha256.ComputeHash(base64EncodedProto); - Signature Creation: The hash is then signed using the ECDSA private key. This generates a digital signature, which is unique to the data and the private key. The fiscalization system can later verify this signature using the corresponding public key.
var ecdsa = ECDsa.Create(); ecdsa.ImportFromPem(_key); // Load the private key var signature = ecdsa.SignHash(hash); - Base64 Signature: The generated signature is then encoded into a Base64 string, which makes it easy to include in the final request to the fiscalization service.
var encodedSignature = Convert.ToBase64String(signature);
The Signer class digitally signs both Citizen and POS coupons using the ECDSA algorithm. A private key is loaded and used to create a signature over the serialized coupon data.
public string SignBytes(byte[] data)
{
var ecdsa = ECDsa.Create();
ecdsa.ImportFromPem(_key);
var hash = SHA256.Create().ComputeHash(data);
var signature = ecdsa.SignHash(hash);
return Convert.ToBase64String(signature);
}
The return value is a base64-encoded signature.
We have also provided a DLL that is used to digitally sign a string and return a Base64 string of the signature. You can utilize it in a C# application using the DllImport
attribute to call the external method from the DLL. The DLL exposes a function called DigitallySign, which takes a message and a private key as inputs and returns a digitally signed Base64 string.
The class DllImporter shows the interaction with provided external DLL to perform digital signing of a message which can be a base64 string encoding of protobuf representation of
either PosCoupon or CitizenCoupon that is used in QR Code.
This is done by importing the DigitallySign function from a native DLL (named "signer") using the [DllImport] attribute,
which allows unmanaged functions to be called in C#. The external DLL performs the actual cryptographic operation, digitally signing the input message using a private key.
- Initialization: The class is initialized with a private key, provided through the constructor and stored in the
_keyfield. This private key, in PEM format or another appropriate format, will be used by the external DLL to sign messages. - Importing the DLL Function: The DllImport attribute is used to declare the
DigitallySignmethod. This method is marked asextern, meaning it is defined externally (in the DLL). It takes two byte arrays as parameters: the first is the message to be signed, and the second is the private key. The method returns anIntPtr, which points to the memory location of the resulting digital signature.[DllImport("signer", EntryPoint = "DigitallySign")] static extern IntPtr DigitallySign(byte[] msg, byte[] key); - Signing a Message: The
SignMessagemethod converts the input message (string) into a byte array usingEncoding.ASCII.GetBytes()and does the same for the private key. These byte arrays are passed to theDigitallySignfunction from the DLL. The result from this call is a pointer(IntPtr)to the digital signature, which is then converted into a Base64-encoded string usingMarshal.PtrToStringAnsi(). The method returns this Base64 string, which is the digital signature of the input message.public string SignMessage(string msg) { var msgBytes = Encoding.ASCII.GetBytes(msg); // Convert message to bytes var result = DigitallySign(msgBytes, Encoding.ASCII.GetBytes(_key)); // Call DLL method var signature = Marshal.PtrToStringAnsi(result); // Convert result to string return signature; // Return the signature }
Printed fiscal coupon needs to also have a QR Code that can be scanned by citizens to verify the authenticity of the receipt.
In the Fiscalization System, QR codes are generated based on the serialized and signed data of a Citizen Coupon. The data, once encoded into a QR code, is typically printed on the customer receipt.
In this implementation, the QR code contains:
- The Base64-encoded serialized data of the Citizen Coupon.
- The Base64-encoded digital signature of that data.
These two parts are combined into a single string, separated by a pipe | symbol, which forms the data to be encoded in the QR code.
The following steps show how the QR code data is generated in the Fiskalizimi class using the CitizenCoupon model.
- Serialize the CitizenCoupon to Protobuf binary: This ensures that the receipt data is in a compact binary format.
byte[] citizenCouponProto = citizenCoupon.ToByteArray(); - Base64 encode the Protobuf data: This converts the binary data into a Base64-encoded string, making it suitable for use in the QR code.
var base64EncodedProto = Convert.ToBase64String(citizenCouponProto); - Generate a digital signature: Using the ECDSA private key, sign the Base64-encoded Protobuf data to ensure its authenticity and integrity.
var base64EncodedBytes = Encoding.UTF8.GetBytes(base64EncodedProto); string signature = signer.SignBytes(base64EncodedBytes); - Combine the data and signature: The Base64-encoded coupon data and the Base64-encoded signature are concatenated with a pipe | symbol to form the final string, which will be encoded into a QR code.
string qrCodeString = $"{base64EncodedProto}|{signature}"; - Print or display the QR code: The resulting qrCodeString can now be encoded into a QR code and printed on the receipt or displayed on a screen.
Below is the method in the Fiskalizimi class that generates the QR code string for a Coupon:
public static string SignCitizenCoupon(CitizenCoupon citizenCoupon, ISigner signer)
{
// Serialize the citizen coupon message to protobuf binary
byte[] citizenCouponProto = citizenCoupon.ToByteArray();
// convert the serialized protobuf of citizen coupon to base64 string
var base64EncodedProto = Convert.ToBase64String(citizenCouponProto);
// convert the base64 string of the citizen coupon to byte array
var base64EncodedBytes = Encoding.UTF8.GetBytes(base64EncodedProto);
// digitally sign the bytes and return the signature
string signature = signer.SignBytes(base64EncodedBytes);
Console.WriteLine($"Coupon : {base64EncodedProto}");
Console.WriteLine($"signature: {signature}");
// Combine the encoded data and signature to create QR Code string and return it
string qrCodeString = $"{base64EncodedProto}|{signature}";
Console.WriteLine($"qr code : {qrCodeString}");
return qrCodeString;
}
QR Code will be scanned by the Citizen Mobile App, which in turn will send the data to the Fiscalization System for verification. This method mimics the Citizen Mobile App, and is used for testing purposes. The SendQrCode method sends the serialized and signed citizen coupon to the fiscalization service.
This is how you prepare and submit the request:
public static async Task SendQrCode()
{
var builder = new ModelBuilder();
var signer = new Signer(PrivateKeyPem);
var citizenCoupon = builder.GetCitizenCoupon();
var qrCode = SignCitizenCoupon(citizenCoupon, signer);
var request = new { citizen_id = 1, qr_code = qrCode };
var response = await new HttpClient().PostAsJsonAsync(url, request);
response.EnsureSuccessStatusCode();
}
The json to be sent should look like:
{
"citizen_id": 7872345678,
"qr_code":"CIHI0p4CENIJGAEgATABOJy/w64GQJwOSgYKAUMQwgNKCAoBRBDAAhgaSgkKAUUQmggYvQFQ1wE=|MEUCIA0Cg2dC+JkPyxkpUDY9VPE6+WCJDOPFkpcyNHbQ0MxTAiEAhn4kdIebWarW2zvRz2k2dLZf29MxzG+4RFeY1g/C95k="
}
Similar to citizen coupons, you can send POS coupons with the SendPosCoupon method:
public static async Task SendPosCoupon()
{
var builder = new ModelBuilder();
var signer = new Signer(PrivateKeyPem);
var posCoupon = builder.GetPosCoupon();
var (coupon, signature) = SignPosCoupon(posCoupon, signer);
var request = new { details = coupon, signature = signature };
var response = await new HttpClient().PostAsJsonAsync(url, request);
response.EnsureSuccessStatusCode();
}
The json to be sent should look like:
{
"details":"CIHI0p4CENIJGAEiCVByaXNodGluZSoJS3VzaHRyaW1pMAE4wMQHQhAxMjM0NTY3ODkwMTIzNDU2SAFQnL/DrgZaJAoKdWplIHJ1Z292ZRCWARoEY29wZSUAAEBAKMIDMgFDOgJUVFohCgdzZW5kdmlxEKwCGgRjb3BlJQAAAEAo2AQyAUU6AlRUWh0KBGJ1a2UQUBoEY29wZSUAAIBAKMACMgFEOgJUVFoqChBtYWNoaWF0byBlIG1hZGhlEJYBGgRjb3BlJQAAQEAowgMyAUU6AlRUYgUIARD0A2IFCAIQ6AdiBQgDEMACaJwOcgYKAUMQwgNyCAoBRBDAAhgacgkKAUUQmggYvQF41wGAAcUM",
"signature":"MEQCIA5bgcs5gqxIAoKujhK7LXobNqa130DCjUTdBrCctYV1AiAzkvoSi+mvzFL4ThvzYPVsKegJ5sL00msyrlEiEJ6NmA=="
}
Before you can run the sample application provided, you first need to be onboarded. Once you have been onboarded, then you need to copy the private key and replace the existing private key in the Program.cs file
To execute the program and send the coupons:
dotnet run
Note
Make sure to configure the correct URL of the fiscalization service and have a valid ECDSA private key to sign the coupons.



