Docs / Protocol
The EFRIS protocol
What a request actually looks like on the wire, and the handful of details either absent from URA's specification or contradicted by the live service. These are the reasons a first integration fails before it ever submits a document.
The envelope
Every request is the same three-part structure. Only data.content
changes between interfaces.
{
"data": {
"content": "...", // the interface's request, encoded
"signature": "...", // RSA/SHA1 over the ENCODED content
"dataDescription": { "codeType": "0", "encryptCode": "2", "zipCode": "0" }
},
"globalInfo": { "appId": "AP04", "interfaceCode": "T109", "tin": "...", ... },
"returnStateInfo": { "returnCode": "", "returnMessage": "" }
}
encryptCode decides the encoding
| Value | content holds | Signature over |
|---|---|---|
2 | base64( AES-128-ECB( json ) ) | that base64 ciphertext |
1 | base64( json ) | that base64 string |
0 | the JSON object itself, embedded | not signed |
The signature covers the transmitted value, not the plaintext. For an encrypted request that means signing the base64 ciphertext. Signing the original JSON produces a well-formed request that URA rejects.
Cryptography
- AES-128-ECB with PKCS#7 padding. Not CBC, not GCM, and there is no IV. ECB is the mode every modern guide says never to use, so a careful implementer reaches for CBC and loses days to an opaque signature error.
- RSA PKCS#1 v1.5 over SHA1. Not SHA256.
- The signing key is the private key inside the PKCS#12 (
.pfx) certificate URA issued for the TIN.
The T104 handshake
T101 (server time) and T104 (key exchange) precede the session key, so they are sent unsigned and unencrypted. T104 returns the AES session key, wrapped:
- Base64-decode
data.contentto get a small JSON object. - Read
passowrdDesfrom it — URA's spelling. ReadingpasswordDesraises a KeyError against a live server. - Base64-decode that value, then RSA-decrypt it with your private key.
- The result is another base64 string. Decode it again to reach the 16 raw key bytes.
If your key is not exactly 16 bytes, you stopped one step early. The RSA output is base64 text, not the key. Skipping the second decode leaves a 24-character value that looks plausible and fails every later call on padding.
inner = json.loads(base64.b64decode(envelope["data"]["content"]))
wrapped = base64.b64decode(inner["passowrdDes"]) # URA's spelling
intermediate = private_key.decrypt(wrapped, padding.PKCS1v15())
aes_key = base64.b64decode(intermediate) # decode TWICE
assert len(aes_key) == 16
Responses
A response may be gzipped before encryption. Content beginning
H4sI is a base64 gzip stream, and what is inside it may still be AES
encrypted — decompress first, then attempt decryption, then parse.
Where the specification is wrong
- Encryption flags. T119 is documented as Request Encrypted: Y but the live service accepts it unencrypted. Where a documented flag and the service disagree, the service wins.
appIdis not a credential. It names the integration channel:AP04for system-to-system web-service integrations,AP05for AskURA,AP01for the TCS/offline client. The same value for every taxpayer and every vendor on a channel, so there is nothing to apply for.- T102 returns nothing useful on AP04. Its key material
(
clientPriKey,keyTable) is issued only forAP01. - Page size. Documented as up to 100; 100 is rejected. See error 1526.
Next
- Quickstart — from install to a fiscalised invoice
- Error codes — what each rejection means
- Error 15 — when the session key expires under you
The EFRIS API Kit handles this case already — it is one of the rejections the library was calibrated against. This page stays free either way.