For developers
An SDK for the UJP e-invoicing system. Signing, validation, totals and sending from your own software, the same code the app uses.
Quick start
- Get access to the test environment of UJP e-Faktura: an EDB, an eUJP ID and a qualified certificate.
- Add the SDK for your language from the repo, from the path in the table below.
- Open a signer (USB token, .p12 file or macOS Keychain) and create a client for the test environment.
- Send a document and check the EUID in the response.
import { UjpClient, Pkcs11Signer } from "@rid/efakt";
const signer = await Pkcs11Signer.open({
module: "/usr/lib/opensc-pkcs11.so",
pin,
});
const ujp = new UjpClient({
environment: "test",
edb, eujpId,
signer,
});
const sent = await ujp.sendInvoice(invoice);
console.log(sent.euid, sent.qrLink);from efakt import Environment, Pkcs11Signer, UjpClient
signer = Pkcs11Signer.open(
module="/usr/lib/opensc-pkcs11.so",
pin=pin,
)
ujp = UjpClient(
environment=Environment.TEST,
edb=edb, eujp_id=eujp_id,
signer=signer,
)
sent = ujp.send_invoice(invoice)
print(sent.euid, sent.qr_link)<?php
use Rid\Efakt\{Environment, Pkcs11Signer, UjpClient};
$signer = Pkcs11Signer::open(
module: '/usr/lib/opensc-pkcs11.so',
pin: $pin,
);
$ujp = new UjpClient(
environment: Environment::Test,
edb: $edb, eujpId: $eujpId,
signer: $signer,
);
$sent = $ujp->sendInvoice($invoice);
echo $sent->euid, ' ', $sent->qrLink;using Rid.Efakt;
using Rid.Efakt.Signing;
using var signer = Pkcs11Signer.Open(
module: "/usr/lib/opensc-pkcs11.so",
pin: pin);
var ujp = new UjpClient(new ClientConfig
{
Environment = UjpEnvironment.Test,
Edb = edb, EujpId = eujpId,
Signer = signer,
});
var sent = await ujp.SendInvoiceAsync(invoice);
Console.WriteLine($"{sent.Euid} {sent.QrLink}");import mk.rid.efakt.*;
var signer = Pkcs11Signer.open(
"/usr/lib/opensc-pkcs11.so", pin);
var ujp = UjpClient.builder()
.environment(Environment.TEST)
.edb(edb).eujpId(eujpId)
.signer(signer)
.build();
var sent = ujp.sendInvoice(invoice);
System.out.println(sent.euid() + " " + sent.qrLink());signer, err := efakt.OpenPKCS11(efakt.PKCS11{
Module: "/usr/lib/opensc-pkcs11.so",
PIN: pin,
})
if err != nil {
return err
}
ujp := efakt.NewClient(efakt.Config{
Environment: efakt.Test,
EDB: edb,
EUJPID: eujpID,
Signer: signer,
})
sent, err := ujp.SendInvoice(ctx, invoice)
if err != nil {
return err
}
fmt.Println(sent.EUID, sent.QRLink)let signer = SignerSource::Pkcs11 {
module: "/usr/lib/opensc-pkcs11.so".into(),
slot_id: None,
object_id_hex: None,
}.open(&pin)?;
let ujp = UjpClient::new(ClientConfig {
environment: Environment::Test,
edb, eujp_id,
..Default::default()
})?.with_signer(signer)?;
let sent = ujp.send_invoice(invoice).await?;
println!("{:?} {:?}", sent.euid, sent.qr_link);SDKs by language
The SDKs are paid, so they are not on public registries (crates.io, npm, PyPI and others). You install them from the repo, which you get when you buy them.
| Language | Package | Install | Status |
|---|---|---|---|
| Rust | efakt-sdk, efakt-signer | from the repo (crates/efakt-sdk, crates/efakt-signer) | Available |
| TypeScript | @rid/efakt | from the repo (sdks/typescript) | Available |
| Python | efakt | from the repo (sdks/python) | Available |
| PHP | rid/efakt | from the repo (sdks/php) | Available |
| .NET | Rid.Efakt | from the repo (sdks/dotnet) | Available |
| Java | mk.rid:efakt | from the repo (sdks/java) | Available |
| Go | github.com/rid/efakt-go | from the repo (sdks/go) | Available |
Signing
Every request to UJP that sends or changes a document is signed as a compact JWS with RS256 (RSASSA-PKCS1-v1_5 with SHA-256). The header is exactly {"alg":"RS256","typ":"JWT"}, as in the UJP examples, and the payload is the document JSON together with requestTimestamp.
The key can live in three places:
- PKCS#11: a USB token or smart card. The SDK knows the usual module paths (SafeNet/Thales, OpenSC, SoftHSM). Tokens and certificates can be listed without a PIN, and the certificate is selected by slot and
CKA_ID. - PKCS#12: a
.p12or.pfxfile, from disk or from memory. - macOS Keychain: macOS only, including tokens through CryptoTokenKit. macOS then asks for the PIN itself.
The private key never leaves the token or the file. The PIN is held only in memory and wiped after use. The local database (efakt.db, SQLite, shared by the app and the CLI) contain no secrets.
Each request also carries the X-EUJP-ID, X-EDB and X-SERIAL-NUMBER headers. The certificate serial number can be sent in hex, lowercase hex or decimal, or set manually if UJP doesn’t recognise it.
requestTimestamp is in Skopje local time, formatted yyyy-MM-ddTHH:mm:ss. UJP accepts a deviation of ±5 minutes, so before sending, the SDK syncs its clock with the UJP server time.
Before the first send you can run a test signature: a local sign and verify, with no network. The SDK also checks the certificate locally the way UJP does: whether it has expired or isn’t valid yet (E5002, E5004), whether it has an RSA key of at least 2048 bits (E5004, E5005), whether it’s an issuer (CA) certificate instead of a personal one, and whether the EDB in the certificate matches the configured one (E1013, E4023). Revocation (OCSP/CRL) is checked only by UJP.
Validation against the UJP structure
Before sending, the document is checked against the official UJP JSON structure and the page for each document type. The rules are generated from the wiki: the format of every field (string length, date YYYY-MM-DD, decimals, integer, boolean) and the fields that are unconditionally required for each document type.
Conditionally required fields (marked З* on the wiki, for example those that depend on VAT, residency or references) aren’t required locally. UJP checks them when you send. Fields in items, references and the address are checked when that part is present.
You can also read the requirements for a document type directly from UJP (document_type_requirements).
Totals
The SDK calculates items, totals and the VAT recapitulation according to the UJP rules:
- items are calculated to 4 decimals and not rounded to 2;
- totals are summed unrounded, then rounded to 2 decimals (half up);
docGrossAmountRis a whole number in denars (or with no currency), and keeps 2 decimals in a foreign currency (UJP rule from 19.10.2026);- in denars
docAvansAmount,docFinalAmount,docGrossAmountR,docAmountIncreaseanddocAmountDecreasemust be whole numbers, otherwise validation returns error UJP08-08 to UJP08-12; docTotalsandvatTotalsare filled from the items and tax indicators.
Advance, increase and decrease amounts are input only and aren’t calculated. After the calculation, validation confirms that the sum of vatTaxableAmount and docNetAmountDisc + docVatAmount match the totals within the UJP tolerance of 0.1.
Sending
The document is sent to JSONReceiver/api/v1/sales-invoices/send, with the document type in the X-DOC-TYPE-CODE header. The whole flow is one call (send_checked): calculate, validate, sync the clock, take a fresh requestTimestamp, sign and send. If validation finds an error, nothing is sent.
After a successful send, the response contains the EUID (the number UJP assigns to the document), a message and status, whether the buyer uses the system, the time of receipt and the link for the QR code.
For cancellations and corrections, the SDK prepares the new document from the original: a cancellation with negative quantities, docStorno = 1 and reference “ST”, a correction with docStorno = 2 and reference “KR”.
UJP allows one request per second per user. The SDK waits at least 1050 ms between requests, and on a 429 response it retries according to Retry-After, at most 3 times.
Status and error codes
Besides sending, the SDK covers the other UJP services:
- current status and status history of a document, changes per day, lists of EUIDs and statuses for a period;
- the PDF, the signed JSON and the ASiC archive of a document;
- accepting and rejecting incoming documents (rejecting needs a reason code) and VAT totals for incoming documents;
- taxpayer and subsidiaries by EDB, countries, currencies and the NBRM middle exchange rate;
- code lists: correction, cancellation and rejection reasons, payment types, statuses, document types, tax groups and indicators.
UJP allows a period of at most one month, so the SDK splits longer periods into 28-day chunks.
The error codes come from the official UJP list, in three parts: eInvoiceApi, JSON Receiver and the web app. Some codes mean different things in each part (for example E4023), so a code is read according to the service that returned it. The groups are:
| Group | Meaning |
|---|---|
| E1xxx | business errors: taxpayer, certificate, document status |
| E4xxx | request errors: parameters, headers, format |
| E5xxx | processing errors: certificate, JWS, server |
For each code the SDK gives the UJP text, whether it relates to the certificate, whether it’s worth retrying, and a hint in Macedonian. An error from UJP comes back with the HTTP status, the code, the message and the full response.
Test and production
| Environment | eInvoiceApi | JSON Receiver |
|---|---|---|
| Test (default) | https://efakturatest.ujp.gov.mk/einvoice_api |
https://efakturatest.ujp.gov.mk/JSONReceiver |
| Production | https://efaktura.ujp.gov.mk/einvoice_api |
https://efaktura.ujp.gov.mk/JSONReceiver |
Documents sent to the test environment have no legal effect. The production addresses aren’t published in the UJP documentation and haven’t been verified yet. If UJP gives you other addresses, you set them as custom ones (Custom).
References
- JSON structure on the UJP wiki.
- The UJP e-Faktura wiki.
Didn't find an answer? Write to us