SDK version
postnl-ecommerce-sdk 1.0.5
Summary
Calling get_status_by_barcode(..., detail=True) raises an AttributeError for the vast majority of barcodes. Two distinct bugs were found, both caused by the same root cause: the PostNL API serialises XML-to-JSON in a way that returns a single object {} when there is one entry, and an array [{}, {}] when there are multiple. The SDK does not handle the single-object case.
Bug #1 — OldStatus returned as dict instead of list
Root cause
In complete_status_shipment.py the SDK always iterates OldStatus as a list:
# postnlecommerce/models/complete_status_shipment.py – lines 203-207
old_status = None
if dictionary.get('OldStatus') is not None:
old_status = [OldStatus.from_dictionary(x) for x in dictionary.get('OldStatus')]
else:
old_status = APIHelper.SKIP
When the shipment has exactly one previous status, the API returns OldStatus as a plain JSON object:
"OldStatus": {
"TimeStamp": "17-01-2026 19:07:32.778",
"StatusCode": "1",
"StatusDescription": "Zending voorgemeld",
"PhaseCode": "1",
"PhaseDescription": "Collectie"
}
When iterated as for x in dict, Python yields the dict's keys (strings like "TimeStamp", "StatusCode", …). OldStatus.from_dictionary("TimeStamp") is then called with a str, crashing immediately in old_status.py:
# postnlecommerce/models/old_status.py – line 83
time_stamp = dictionary.get("TimeStamp") if dictionary.get("TimeStamp") else APIHelper.SKIP
# ^^^ AttributeError: 'str' object has no attribute 'get'
Full traceback
File ".../postnlecommerce/controllers/shipping_status_controller.py", line 103, in get_status_by_barcode
).execute()
File ".../apimatic_core/response_handler.py", line 100, in apply_deserializer
return self._deserializer(response.text, self._deserialize_into)
File ".../postnlecommerce/models/shippingstatus_response.py", line 78, in from_dictionary
complete_status = CompleteStatus.from_dictionary(...)
File ".../postnlecommerce/models/complete_status.py", line 60, in from_dictionary
shipment = CompleteStatusShipment.from_dictionary(...)
File ".../postnlecommerce/models/complete_status_shipment.py", line 205, in from_dictionary
old_status = [OldStatus.from_dictionary(x) for x in dictionary.get('OldStatus')]
File ".../postnlecommerce/models/old_status.py", line 83, in from_dictionary
time_stamp = dictionary.get("TimeStamp") if dictionary.get("TimeStamp") else APIHelper.SKIP
AttributeError: 'str' object has no attribute 'get'
Suggested fix — complete_status_shipment.py
old_status_raw = dictionary.get('OldStatus')
if old_status_raw is not None:
if isinstance(old_status_raw, dict): # single entry → wrap in list
old_status_raw = [old_status_raw]
old_status = [OldStatus.from_dictionary(x) for x in old_status_raw]
else:
old_status = APIHelper.SKIP
Bug #2 — Shipment returned as list instead of dict
Root cause
In complete_status.py the SDK always treats Shipment as a single object:
# postnlecommerce/models/complete_status.py – line 60
shipment = CompleteStatusShipment.from_dictionary(dictionary.get('Shipment')) if 'Shipment' in dictionary.keys() else APIHelper.SKIP
When a barcode covers multiple shipments (e.g. a multi-collo shipment), the API returns Shipment as a JSON array:
"Shipment": [ { ... }, { ... }, { ... } ]
CompleteStatusShipment.from_dictionary([...]) receives a list, and the first .get() call on it crashes:
AttributeError: 'list' object has no attribute 'get'
Suggested fix — complete_status.py
shipment_raw = dictionary.get('Shipment')
if shipment_raw is not None:
if isinstance(shipment_raw, list):
# multi-collo: take first shipment or handle all as needed
shipment = CompleteStatusShipment.from_dictionary(shipment_raw[0])
else:
shipment = CompleteStatusShipment.from_dictionary(shipment_raw)
else:
shipment = APIHelper.SKIP
Alternatively, the model could expose a shipments list field to preserve all entries.
Minimal reproducible example
from postnlecommerce.configuration import Environment
from postnlecommerce.http.auth.custom_header_authentication import CustomHeaderAuthenticationCredentials
from postnlecommerce.postnlecommerce_client import PostnlecommerceClient
from postnlecommerce.models.language_enum import LanguageEnum
client = PostnlecommerceClient(
custom_header_authentication_credentials=CustomHeaderAuthenticationCredentials(
apikey='<YOUR_API_KEY>'
),
environment=Environment.PRODUCTION_SERVER
)
# Bug — barcode with exactly one previous status
client.shipping_status.get_status_by_barcode(
'BARCODE_FAIL_A/B', detail=True, language=LanguageEnum.NL, max_days=None
)
Working control example (SDK succeeds)
Barcode:
BARCODE_OK_C
Observed response classification:
HTTP status: 200
Shipment type: dict
OldStatus type: list
OldStatus length: 11
Sample output (parsed from API response):
First OldStatus item:
{"TimeStamp":"09-01-2026 00:49:12","StatusCode":"99","StatusDescription":"Niet van toepassing","PhaseCode":"4","PhaseDescription":"Afgeleverd"}
Current Status:
{"TimeStamp":"11-01-2026 03:00:00","StatusCode":"11","StatusDescription":"Zending afgeleverd","PhaseCode":"4","PhaseDescription":"Afgeleverd"}
Why this works:
This barcode returns OldStatus as an array (multiple entries), which matches what the SDK expects.
The failing barcodes mostly return OldStatus as a single object, or Shipment as an array, which the SDK currently does not handle.
SDK version
postnl-ecommerce-sdk1.0.5Summary
Calling
get_status_by_barcode(..., detail=True)raises anAttributeErrorfor the vast majority of barcodes. Two distinct bugs were found, both caused by the same root cause: the PostNL API serialises XML-to-JSON in a way that returns a single object{}when there is one entry, and an array[{}, {}]when there are multiple. The SDK does not handle the single-object case.Bug #1 —
OldStatusreturned asdictinstead oflistRoot cause
In
complete_status_shipment.pythe SDK always iteratesOldStatusas a list:When the shipment has exactly one previous status, the API returns
OldStatusas a plain JSON object:When iterated as
for x in dict, Python yields the dict's keys (strings like"TimeStamp","StatusCode", …).OldStatus.from_dictionary("TimeStamp")is then called with astr, crashing immediately inold_status.py:Full traceback
Suggested fix —
complete_status_shipment.pyBug #2 —
Shipmentreturned aslistinstead ofdictRoot cause
In
complete_status.pythe SDK always treatsShipmentas a single object:When a barcode covers multiple shipments (e.g. a multi-collo shipment), the API returns
Shipmentas a JSON array:CompleteStatusShipment.from_dictionary([...])receives alist, and the first.get()call on it crashes:Suggested fix —
complete_status.pyMinimal reproducible example
Working control example (SDK succeeds)
Barcode:
BARCODE_OK_C
Observed response classification:
HTTP status: 200
Shipment type: dict
OldStatus type: list
OldStatus length: 11
Sample output (parsed from API response):
First OldStatus item:
{"TimeStamp":"09-01-2026 00:49:12","StatusCode":"99","StatusDescription":"Niet van toepassing","PhaseCode":"4","PhaseDescription":"Afgeleverd"}
Current Status:
{"TimeStamp":"11-01-2026 03:00:00","StatusCode":"11","StatusDescription":"Zending afgeleverd","PhaseCode":"4","PhaseDescription":"Afgeleverd"}
Why this works:
This barcode returns OldStatus as an array (multiple entries), which matches what the SDK expects.
The failing barcodes mostly return OldStatus as a single object, or Shipment as an array, which the SDK currently does not handle.