Language-agnostic guide to extracting multi-layer encoded responses from URA EFRIS APIs.
EFRIS API responses are wrapped in multiple encoding layers. Decode them wrong, pass the wrong format to your crypto function, or get the order wrong — and you’ll get binary garbage or JSON parse errors. Here’s the correct pattern.
The Envelope
Every EFRIS response wraps its payload in the same structure:
{
"returnStateInfo": {
"returnCode": "00",
"returnMessage": "SUCCESS"
},
"data": {
"content": "H4sIAAAAAAAAA6tWKkktLlGyUlA...",
"dataDescription": {
"codeType": "1",
"encryptCode": "2",
"zipCode": "1"
}
}
}
The content field is always Base64-encoded. But what’s inside that Base64 depends entirely on the three metadata flags in dataDescription:
| Flag | Value | Meaning |
|---|---|---|
zipCode | "1" | Content is GZIP-compressed |
codeType | "1" | Content is encrypted |
encryptCode | "2" | Encryption uses AES-128-ECB |
These flags determine how many layers you need to peel off. In the worst case (T115 System Dictionary), you need all three.
The Four Scenarios
1. Plaintext (Health Check, T101)
zipCode: "0", codeType: "0"
Decoding: Base64 → UTF-8 → JSON ✓
Example:
Response: H4sIAAAAA... (base64 of plaintext)
After decode: {"name": "System"}
Result: Parse and use directly
2. Encrypted Only (TIN Query, T119)
zipCode: "0", codeType: "1", encryptCode: "2"
Decoding: Base64 → AES-decrypt → JSON ✓
Example:
Response: a7d43d26ba5b0f6c43... (base64 of encrypted bytes)
After AES decrypt: {"tin": "123456", ...}
Result: Parse and use directly
3. Compressed Only (Some Reference Data)
zipCode: "1", codeType: "0"
Decoding: Base64 → GZIP-decompress → JSON ✓
Example:
Response: H4sIAAAAA... (base64 of gzip stream)
After decompress: {"items": [...], ...}
Result: Parse and use directly
4. Compressed AND Encrypted (System Dictionary, T115)
zipCode: "1", codeType: "1", encryptCode: "2"
Decoding: Base64 → GZIP-decompress → AES-decrypt → JSON ✓
CRITICAL: Decompression happens BEFORE decryption.
The data was compressed first, then encrypted.
Example:
Response: H4sIAA... (base64 of gzipped+encrypted bytes)
After decompress: <encrypted binary>
After AES decrypt: {"currencyType": [...], ...}
Result: Parse and use directly
Common Mistake
The most common error is passing the wrong format to your AES decrypt function:
WRONG ❌
base64_content = base64_decode(responseContent)
decrypted = decrypt_aes(convert_to_utf8(base64_content), sessionKey)
CORRECT ✅
base64_content = base64_decode(responseContent)
if (needs_decompression):
base64_content = decompress_gzip(base64_content)
decrypted = decrypt_aes(convert_to_base64(base64_content), sessionKey)
The decrypt_aes() function expects Base64 input, not UTF-8. Passing UTF-8 where Base64 is expected produces garbled output — no error, just wrong bytes.
The Correct Pipeline
After the fix, the full T115 extraction works like this:
Pseudocode:
function extract_response(responseContent, dataDescription, sessionKey):
// 1. Base64 decode
buffer = base64_decode(responseContent)
// buffer: [1F 8B 08 00 ...] — GZIP magic bytes
// 2. Decompress if needed
if (dataDescription.zipCode == "1" OR first_two_bytes(buffer) == 0x1F 0x8B):
buffer = gzip_decompress(buffer)
// buffer: [A7 D4 3D 26 ...] — encrypted binary
// 3. Decrypt if needed
if (dataDescription.codeType == "1" AND dataDescription.encryptCode == "2"):
// CRITICAL: re-encode buffer as base64 BEFORE decrypting
base64_string = buffer_to_base64(buffer)
json_string = aes_128_ecb_decrypt(base64_string, sessionKey)
else:
json_string = buffer_to_utf8(buffer)
// 4. Parse
data = json_parse(json_string)
return data
Key points:
- Decompression happens before decryption
- The encryption function expects Base64 input, not raw bytes
- Check both metadata AND magic bytes for compression
The Shared Function
We had this logic duplicated in two places — the generic tester and the sync handler — and they diverged. One passed Base64, the other passed UTF-8. Classic copy-paste drift.
We extracted a single shared function:
Pseudocode:
function extract_response_content(
content: string, // Base64-encoded response content
dataDescription: object, // Response metadata (zipCode, codeType, etc.)
sessionKey: string // AES decryption key (Base64)
) -> string:
// Step 0: Base64 decode
buffer = base64_decode(content)
// Step 1: Decompress (check BOTH metadata AND magic bytes)
isCompressed = (dataDescription.zipCode == "1")
hasGzipHeader = (buffer[0] == 0x1F AND buffer[1] == 0x8B)
if (isCompressed OR hasGzipHeader):
buffer = gzip_decompress(buffer)
// Step 2: Decrypt
isEncrypted = (dataDescription.codeType == "1")
if (isEncrypted AND dataDescription.encryptCode == "2"):
// Convert buffer to Base64 for decryption
base64_buffer = buffer_to_base64(buffer)
return aes_128_ecb_decrypt(base64_buffer, sessionKey)
else:
return buffer_to_utf8(buffer)
Now every endpoint — sync, tester, future integrations — calls the same function. One place to fix, one place to test.
How to Debug When It Breaks
If you’re staring at garbled output, add this single debug statement after each step:
print_hex(first_10_bytes(buffer))
The first two bytes tell you everything:
| First Bytes | Hex | Meaning | Next Step |
|---|---|---|---|
{" | 7B 22 | Raw JSON | Parse directly |
[ | 5B ... | JSON array | Parse directly |
| GZIP | 1F 8B | GZIP stream | Decompress first |
| Other | Random | Encrypted binary | Decrypt via Base64 |
If you see 1F 8B after decompressing: Something went wrong with decompression. Re-check the decompression library call.
If you see random bytes after decryption: Your session key is wrong, expired, or you passed UTF-8 instead of Base64 to the decryption function.
If decryption throws an error about “block length”: You’re probably passing a UTF-8 string where Base64 is expected.
Key Takeaways
-
Order is critical: Decompress BEFORE decrypt. Never the reverse.
-
Format matters: Your AES decrypt function expects Base64. Don’t convert to UTF-8 first.
-
Check both metadata AND magic bytes: Use
dataDescription.zipCodeandcodeType, but also verify GZIP magic bytes (0x1F 0x8B) since metadata can be missing. -
Create a shared function: Don’t duplicate this logic across endpoints. Put it in one place, test it once.
-
Debug with hex: Print byte values in hex, not as UTF-8 strings. Garbled characters tell you nothing; hex bytes tell you exactly what’s happening (GZIP, encrypted data, or JSON).
-
Check your crypto library docs: Different languages treat base64/raw bytes differently. Verify what your AES implementation expects.
Implement this pattern once in your language of choice, and you won’t hit this problem again.