Developer notes
Part of the PostNord Shipping documentation. Previous: Troubleshooting. Next: Conformance.
Sources
Section titled “Sources”Only the official PostNord documents were used (read 2026-11-08):
- developer.postnord.com: the OpenAPI documents of the public APIs (Booking APIs 3.5.29.1, Service Points V5 5.0.14, PostNord Track & Trace API v7 FindByIdentifier / FindByReference (public), Track Shipment URL 1.0.2, Transit time calculation V1 and V2 2.0.3.1, Booking Data Validation 1.0.6.1).
- guide.developer.postnord.com: “PostNord Customer API Guides” (hosts, authentication, examples “MyPack Collect (19) PDF labels” and “Return Drop Off (24) PDF labels”).
Hosts and authentication
Section titled “Hosts and authentication”| Item | Value | Source |
|---|---|---|
| Sandbox host | https://atapi2.postnord.com |
Guides, “For development”: “The test URL Host is https://atapi2.postnord.com” |
| Production host | https://api2.postnord.com |
Guides, “For production” |
| Authentication | query parameter apikey on every call |
Every OpenAPI document: apikey (query, required) “The unique consumer (client) identifier 32 characters”; Guides: “If the apikey is missing or wrong PostNord API will respond with 403 Forbidden” |
| Errors | errorResponse {message, compositeFault {faults [{faultCode, explanationText, paramValues}]}}; Service Points wrap it in servicePointInformationResponse; the gateway answers {"error": {status_code, error_type, message}} |
Booking APIs definitions errorResponse, compositeFault, fault; Service Points 400 example |
| Rate limit | 429 Too Many Requests | Guides, “Rate limit” |
The key is sent only in the query string, never logged (the log stores the operation label, not the URL; Log::mask() masks apikey=).
Endpoints used
Section titled “Endpoints used”| Use | Endpoint | Fields used | Code |
|---|---|---|---|
| Connection test | GET /rest/shipment/v1/validate/customernumber |
customerNumber, countryCode (DK, SE, NO, FI); 200 = exists, 404 = not found |
Api::validateCustomerNumber() |
| Service points near the address | GET /rest/businesslocation/v5/servicepoints/nearest/byaddress |
returnType=json, countryCode, agreementCountry (only for another destination country), postalCode (“without country code and spaces”), streetName, streetNumber, city, numberOfServicePoints, context=optionalservicepoint (“Recommended value for checkout-solutions”), responseFilter=public; answer servicePointInformationResponse.servicePoints[]: servicePointId, name, visitingAddress {streetName, streetNumber, postalCode, city, countryCode}, coordinates[], openingHours.postalServices[] {openDay, openTime, closeTime}, routeDistance |
Api::servicePointsNear(), ServicePoints::normalise() |
| Check of the chosen point | GET /rest/businesslocation/v5/servicepoints/ids |
returnType, countryCode, ids, responseFilter=public |
Api::servicePointsByIds() |
| Booking + labels | POST /rest/shipment/v3/edi/labels/pdf and /zpl |
query paperSize (A4, A5, A6, LABEL), labelType (standard, small), multiPDF / multiZPL, labelsPerPage, page; body ediInstruction (below); answer ediLabelResponse: bookingResponse.bookingId, idInformation[] {status OK/FAIL, ids[] {idType itemId, value, printId}, urls[] {type TRACKING, url}, errorResponse}, labelPrintout[].printout {labelFormat, encoding base64/none, data} |
Api::bookWithLabels(), Shipments::bookingFrom(), labelFrom() |
| Label again | POST /rest/shipment/v3/labels/ids/pdf and /zpl |
body ids_label [{id, labelType}] (printId when PostNord sent one: “The printId is use to print the label, in the endpoints /v3/labels/ids/(zpl/pdf)”, else the item id); answer labelPrintout |
Api::labelsByIds() |
| Return label | POST /rest/shipment/v3/returns/edi/labels/pdf and /zpl |
“Create a Return Drop Off, Varubrev Return or Rek Return”; same body and answer (ids returnId + itemId) |
Api::bookReturn(), Shipments::returnPayload() |
| Deletion (Sweden only) | POST /rest/shipment/v3/edi with updateIndicator “Deletion” |
“A Deletion can only be performed, if an Original exist”; “Update and Deletion are not supported for (Z11=PostNord Denmark, Z13=PostNord Norway, Z14=PostNord Finland)”; answer 201 bookingResponse (FAIL entries = refused) |
Api::deleteBooking(), Shipments::cancel() |
| Tracking | GET /rest/shipment/v7/trackandtrace/id/{id}/public |
locale (en, sv, no, da, fi); TrackingInformationResponse.shipments[].items[] {itemId, status (ItemStatus), statusText.header, events[] {eventTime, eventDescription, location.displayName}} |
Api::track(), Shipments::itemsFrom() |
| Lookup of a lost booking | GET /rest/shipment/v7/trackandtrace/customernumber/{customerNumber}/reference/{reference}/public |
the shipments with this reference; consignee.address.postCode compared with the order |
Api::findByReference(), Shipments::lookup() |
| Tracking address | GET /rest/links/v1/tracking/{country}/{id} |
country SE, NO, FI, DK; language; answer {url} |
Api::trackingUrl() (the booking answer’s TRACKING url is used first) |
| Delivery estimate | GET /rest/transport/v2/transittime/addresstoaddress |
startTime (“local date time”: the hand-over time in the sender country’s zone), serviceCodes, originPostalCode, originCountryCode, destinationPostalCode, destinationCountryCode; answer entries service.basicServiceCode, estimatedTimeOfArrival.timeOfArrival or dayRangeOfArrival {daysMinimum, daysMaximum}, isBookable, errorMessage |
Api::transitTime(), RateCalculator::etaText() |
The EDI instruction (definition ediInstruction)
Section titled “The EDI instruction (definition ediInstruction)”messageDate, messageFunction “Instruction”, messageId (= customer reference, max 36), application {applicationId?, name (max 35), version}, language “EN”, updateIndicator “Original”, shipment[0]:
shipmentIdentification.shipmentId“0” (PostNord assigns),dateAndTimes.loadingDate;service.basicServiceCode(19, 17, 18 or the method’s code) andadditionalServiceCode[];numberOfPackages.value,totalGrossWeight {value, unit KGM};references[] {referenceNo, referenceType "CU"}(max 35);parties.consignor {issuerCode Z11-Z14, partyIdentification {partyId = customer number, partyIdType 160}, party {nameIdentification.name (60), address {streets[] (50 each), postalCode, city (35), countryCode}, contact}};parties.consignee.party {nameIdentification {name, companyName}, address, contact {contactName (50), emailAddress (70), phoneNo, smsNo (50)}};- MyPack Collect:
parties.deliveryParty {partyIdentification {partyId = service point id, partyIdType 156}, party {name, address}}(as in the official example); goodsItem[0] {packageTypeCode PC, goodsDescription (35), items[] {itemIdentification {itemId "0", itemIdType SSCC}, grossWeight}};- optional
freeText[] {usageCode DEL, text}.
Returns follow the official “Return Drop Off (24)” example: consignor = your agreement (issuer code, customer number) with the return address, consignee = the customer, reference R + booking reference.
Every instruction the add-on builds is validated against the official ediInstruction definition in tests/unit.php (required fields, unknown fields, maxLength, types).
Duplicate protection and states
Section titled “Duplicate protection and states”The Booking API takes no idempotency key. The shipment row with its reference is written before the call under a per-order lock (GET_LOCK). A timeout, 5xx or 499 leaves the row “unknown”: the queue looks the reference up every 10 minutes (FindByReference) and adopts the items found; it never books again. “Book again” is an explicit admin action, allowed 30 minutes after the first attempt and only after a lookup found nothing. A 4xx answer or an idInformation FAIL deletes the pending row. A reference held by another sender’s shipment closes the attempt (new reference on the next booking). Tracking states never move backwards.
Hooks and tables
Section titled “Hooks and tables”PHP hooks: change_order_status_post (automatic booking), get_order_info, place_order, place_suborders, delete_order, template_document_order_context_init. Tables: kv_postnord_accounts, _shipments, _parcels, _points, _spcache, _eta, _jobs, _log. Test hook: $config['kv_postnord_base_url'] in config.local.php points the client at a mock.