Skip to content

Developer notes

Part of the PostNord Shipping documentation. Previous: Troubleshooting. Next: Conformance.

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”).
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=).

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) and additionalServiceCode[];
  • 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).

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.

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.