Download OpenAPI specification:
Shipping order creation, tracking, and price estimation.
The API follows Semantic Versioning: MAJOR.MINOR.PATCH. Backwards-incompatible changes bump MAJOR, additive (backwards-compatible) features bump MINOR, and bug fixes / non-breaking improvements bump PATCH.
GET /api/tracking/{order_id}/ no longer returns tracking for a code alone. Authenticated callers get their own orders unchanged; every other lookup — unauthenticated, or an account asking about an order it did not place — must pass the order's collection or delivery postcode as ?postcode=. A request with no postcode returns 400 postcode_required naming the field to add; a wrong postcode returns 404 tracking_not_found. Both answers are identical for codes that do not exist, so neither can be used to discover valid codes. This matches the postcode gate the web tracker has always had, and stops order codes from being enumerable.429 too_many_invalid_lookups.event_code field to each tracking event in GET /api/tracking/{order_id}/. The code is a stable 4-digit identifier (see the Tracking Event Codes section below) that integrators can map to their own status taxonomy without parsing the localized event_description.1.00 kg on order create. Parcels with weight < 1.00 are bumped to 1.00 before pricing.Parcel weight below minimum (1 kg) — bumped to 1 kg for parcel(s): EPDQRZ01, EPDQRZ03./api/shipping-price/ endpoint returns prices tailored to the authenticated account.label_format override on order create/update to force a specific label format (e.g. A4, A6) regardless of the carrier default./api/order/{order_id}/labels/ honours the per-order format override.GET /api/order/ — list orders with filtering and pagination.GET /api/order/{order_id}/ — retrieve a single order.GET /api/order/{order_id}/labels/ — fetch shipping labels for an existing order.POST /api/order/ — create draft or pay-on-create orders.PUT /api/order/{order_id}/ — update / confirm and pay drafts.DELETE /api/order/{order_id}/ — delete draft orders.GET /api/pudo/ — search PUDO points (lockers).GET /api/tracking/ — public tracking endpoint./api/access/ and /api/access/refresh/.The API uses Bearer (JWT) authentication.
You can find your API Token in Integrations.
Obtain a JWT token pair by exchanging your existing API Token at the /api/access/ endpoint.
POST /api/access/
Authorization: Token your-api-token-here
Response:
{
"access": "eyJhbGciOiJIUzI1NiIs...",
"refresh": "BZssDasdfjJkd1NiIs..."
}
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
The access token is valid for 30 minutes.
POST /api/access/refresh/
Content-Type: application/json
{"refresh": "BZssDasdfjJkd1NiIs..."}
Response:
{
"access": "eyJhbGciOiJIUzI1NiIs...new..."
}
The refresh token is valid for 7 days and rotates on each use.
cURL:
# Get JWT tokens
curl -X POST https://www.ecoparcel.eu/api/access/ \
-H "Authorization: Token abc123def456"
# Use Bearer token
curl -X POST https://www.ecoparcel.eu/api/order/ \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{"orders": [...]}'
Python (requests):
import requests
# Exchange Token for JWT
token_response = requests.post(
"https://www.ecoparcel.eu/api/access/",
headers={"Authorization": "Token abc123def456"},
)
access_token = token_response.json()["access"]
# Use Bearer token
response = requests.post(
"https://www.ecoparcel.eu/api/order/",
headers={"Authorization": f"Bearer {access_token}"},
json={"orders": [...]},
)
JavaScript (fetch):
// Exchange Token for JWT
const tokenRes = await fetch("https://www.ecoparcel.eu/api/access/", {
method: "POST",
headers: { "Authorization": "Token abc123def456" },
});
const { access } = await tokenRes.json();
// Use Bearer token
const res = await fetch("https://www.ecoparcel.eu/api/order/", {
method: "POST",
headers: {
"Authorization": `Bearer ${access}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ orders: [...] }),
});
The Tracking endpoint is publicly accessible without authentication. Anonymous requests are rate-limited to 10 requests per minute.
Orders go through a simple lifecycle. Understanding the statuses and the available actions at each stage will help you integrate smoothly.
| Status | Meaning |
|---|---|
TEMP |
Draft order — not yet paid or submitted to a carrier. Can be edited, confirmed, or deleted. |
NEW |
Paid and queued for processing. The carrier will receive the shipment shortly. |
IN_PROGRESS |
Handed over to the carrier. Tracking events will start appearing. |
DELIVERED |
Successfully delivered to the recipient. |
CANCELLED |
Cancelled due to collection failure or request. |
REFUNDED |
Cancelled and refunded. |
The simplest integration — create an order and pay for it in a single request.
POST /api/order/
{ "orders": [...], "confirm_and_pay": true }
The order is created, priced, paid, and submitted to the carrier immediately. The response includes a base64-encoded shipping label PDF in the labels field.
Recommended for: production integrations where the sender and receiver addresses are known upfront and no PUDO selection is needed (door-to-door shipments).
Create the order first as a draft, then confirm and pay separately. Useful when you need to review pricing or make adjustments before committing.
Step 1 — Create a draft order:
POST /api/order/
{ "orders": [...], "confirm_and_pay": false }
The order is saved with status TEMP. No charge is made.
Step 2 — Confirm and pay:
PUT /api/order/{order_id}/
{ "confirm_and_pay": true }
The order is paid and submitted. Labels are returned in the response.
Recommended for: testing, or when you want to inspect the calculated price before paying.
When using locker-based transportation types (door_to_locker, locker_to_door, locker_to_locker) and you want the user to choose a specific locker, follow this flow:
Step 1 — Create a draft order without a PUDO point:
POST /api/order/
{
"orders": [{
...,
"transportation_type": "door_to_locker"
}],
"confirm_and_pay": false
}
The system auto-selects the nearest locker. The order is saved as TEMP.
Step 2 — Search for nearby PUDO points:
GET /api/pudo/?order_id={order_id}&for_consignee=true
Returns a list of available lockers near the consignee address, filtered by the carrier assigned to the order.
Step 3 — Update the order with the chosen PUDO and confirm:
PUT /api/order/{order_id}/
{
"consignee_details": { "pudo_point": "87" },
"confirm_and_pay": true
}
Note: You can also provide
pudo_pointdirectly in thePOST /api/order/call if you already know the PUDO ID, skipping the search step entirely.
Any TEMP order can be updated — change addresses, parcel dimensions, transportation type, or anything else.
PUT /api/order/{order_id}/
{
"consignee_details": { "address": "New Street 5", "city": "Tallinn", "postcode": "10115" },
"parcels": [{ "description": "Updated", "width": 25, "height": 15, "length": 35, "weight": 3, "value": 80 }]
}
The service card and price are recalculated on every update. Only provided fields are changed — omitted fields keep their current values.
Draft orders (TEMP) can be deleted outright:
DELETE /api/order/{order_id}/
Orders that have already been paid (
NEW,IN_PROGRESS) cannot be cancelled via the API. Use the Ecoparcel dashboard or contact support.
When confirm_and_pay is set to true (either on create or update), the system charges your account in this order:
If neither balance nor card can cover the cost, the order stays in TEMP status with a warning in the response.
| Value | Description |
|---|---|
door_to_door |
Door to Door |
door_to_locker |
Door to Locker |
locker_to_door |
Locker to Door |
locker_to_locker |
Locker to Locker |
For locker-based transportation types, a pudo_point field can be included inside shipper_details and/or consignee_details to specify an exact drop-off/pick-up locker (by ID).
| Transportation Type | shipper_details.pudo_point |
consignee_details.pudo_point |
|---|---|---|
door_to_door |
Not allowed | Not allowed |
door_to_locker |
Not allowed | Optional (auto-selected if omitted) |
locker_to_door |
Optional (auto-selected if omitted) | Not allowed |
locker_to_locker |
Optional (auto-selected if omitted) | Optional (auto-selected if omitted) |
When pudo_point is omitted for a locker side, the system automatically selects the nearest locker based on the provided address.
Important: sending pudo_point for a side that does not require a locker (e.g. shipper side on door_to_locker) will return a 400 validation error.
Each entry in tracking_events[] returned by GET /api/tracking/{order_id}/ includes a stable 4-digit event_code. Use the code (not the human-readable event_description) to drive your own logic — descriptions are localized and may change wording, but codes are stable.
| Code | Description |
|---|---|
0002 |
Prepared for collection |
0101 |
Out for collection |
0102 |
Collection successful (includes parcel locker / shop drop-off by shipper) |
0103 |
Collection problem |
0105 |
Collection problem — Wrong address |
0107 |
Collection cancelled — Wrong address |
0108 |
Collection cancelled — Shipper was absent |
0109 |
Cancelled |
0110 |
Collection cancelled — Insufficient customs declaration |
0111 |
Refunded to the balance |
0112 |
Contacted |
0201 |
Inbound |
0202 |
Outbound |
0204 |
Return |
0205 |
Non-conveyable |
0301 |
Out for delivery |
0302 |
Delivery successful — Consignee |
0303 |
Delivery successful — Parcelshop / Locker (consignee-side) |
0304 |
Delivery successful — Post Office |
0305 |
Delivery successful — Neighbour |
0306 |
Delivery problem — Consignee was absent |
0307 |
Delivery problem — Wrong address |
0308 |
Delivery problem — Consignee refused |
0309 |
Delivery problem — Not out for delivery |
0310 |
Delivery problem |
0311 |
Delivery problem — Consignee refused |
0501 |
Parcel stored in warehouse |
0502 |
Additional information |
0503 |
Customs |
0505 |
Changed collection date |
The following subset indicates the parcel hit a problem state and may need attention (failed collection, failed delivery, return):
0103, 0105, 0107, 0108, 0204, 0306, 0307, 0308, 0309, 0310
Returns a paginated list of your orders, newest first. Supports limit / offset pagination and optional status filter.
| limit | integer Number of results to return (default 100). |
| offset | integer Index of the first result to return. |
| status | string Enum: "CANCELED" "COMPLETED" "IN_PROGRESS" "IN_QUEUE" "NEW" "PARTIALLY_COMPLETED" "REFUNDED" "REJECTED" "TEMP" Filter by order status. |
{- "count": 123,
- "results": [
- {
- "count": 42,
- "previous": null,
- "results": [
- {
- "id": "KR4PZK",
- "parcels": [
- {
- "description": "Electronics",
- "width": "30.00",
- "height": "20.00",
- "length": "40.00",
- "weight": "5.00",
- "value": "150.00"
}
], - "collection_date": "2026-03-10",
- "total_price": "7.78",
- "paid": true,
- "status": "NEW",
- "created_at": "2026-03-09T14:23:11Z"
}, - {
- "id": "7AKXY7",
- "parcels": [
- {
- "description": "Books",
- "width": "20.00",
- "height": "10.00",
- "length": "30.00",
- "weight": "3.00",
- "value": "25.00"
}
], - "collection_date": "2026-03-08",
- "total_price": "5.50",
- "paid": true,
- "status": "COMPLETED",
- "customer_reference": "REF-002",
- "created_at": "2026-03-07T09:15:00Z"
}
]
}
]
}Creates one or more shipment orders. Optionally confirms and initiates payment.
required | Array of objects (SingleOrder) |
| confirm_and_pay required | boolean |
(LabelFormatEnum (string or null)) or (NullEnum (any or null)) Override label format (A4, A5, A6). If omitted, user preference is used.
|
{- "orders": [
- {
- "shipper_details": {
- "country": "LT",
- "contact_name": "John Sender",
- "address": "Gedimino pr. 1",
- "city": "Vilnius",
- "postcode": "01103",
- "phone": "+37060000000",
- "email_address": "sender@example.com"
}, - "consignee_details": {
- "country": "EE",
- "contact_name": "Jane Receiver",
- "address": "Narva mnt 10",
- "city": "Tallinn",
- "postcode": "10117",
- "phone": "+3725000001",
- "email_address": "receiver@example.com"
}, - "parcels": [
- {
- "description": "Electronics",
- "width": 30,
- "height": 20,
- "length": 40,
- "weight": 5,
- "value": 150
}
], - "package_type": "parcel",
- "transportation_type": "door_to_door",
- "customer_reference": "REF-001"
}
], - "confirm_and_pay": true,
- "label_format": "A6"
}{- "orders": [
- {
- "id": "KR4PZK",
- "parcels": [
- {
- "description": "Electronics",
- "width": "30.00",
- "height": "20.00",
- "length": "40.00",
- "weight": "5.00",
- "value": "150.00"
}
], - "collection_date": "2026-03-10",
- "total_price": "7.78",
- "paid": true,
- "status": "NEW",
- "customer_reference": "REF-001"
}
], - "labels": "JVBERi0xLjQK... (base64 PDF)"
}Returns the details of a single order by its ID.
| order_id required | string Order ID. |
{- "id": "KR4PZK",
- "parcels": [
- {
- "description": "Electronics",
- "width": "30.00",
- "height": "20.00",
- "length": "40.00",
- "weight": "5.00",
- "value": "150.00"
}
], - "collection_date": "2026-03-10",
- "total_price": "7.78",
- "paid": true,
- "status": "NEW",
- "customer_reference": "REF-001",
- "created_at": "2026-03-09T14:23:11Z"
}Updates an existing TEMP order. All fields are optional — only provided fields are changed. The full pricing pipeline always runs: service card and price are recalculated.
| order_id required | string Order ID. |
object (PartialOrderDetails) OrderDetailsSerializer with all fields optional — for partial updates. | |
object (PartialOrderDetails) OrderDetailsSerializer with all fields optional — for partial updates. | |
Array of objects (Parcel) | |
| package_type | string (PackageTypeEnum) Enum: "parcel" "pallet" "truck"
|
| transportation_type | string (TransportationTypeEnum) Enum: "door_to_locker" "door_to_door" "locker_to_locker" "locker_to_door"
|
| customer_reference | string <= 32 characters |
| confirm_and_pay | boolean Default: false |
{- "shipper_details": {
- "phone": "+37060000000"
}, - "consignee_details": {
- "phone": "++37060000000"
}
}{- "id": "string",
- "customer_reference": "string",
- "parcels": [
- {
- "description": "string",
- "width": "string",
- "height": "string",
- "length": "string",
- "weight": "string",
- "value": "string"
}
], - "collection_date": "2019-08-24",
- "total_price": "string",
- "paid": false,
- "status": "string",
- "warnings": [
- "string"
]
}Deletes a TEMP order and its associated parcels. Only orders in TEMP status can be deleted.
| order_id required | string Order ID. |
{- "error": {
- "code": "order_not_found",
- "message": "Order not found.",
- "details": { }
}
}Returns the shipping label for a confirmed order. Only available for orders that have been paid and processed (not TEMP, CANCELED, or REFUNDED).
Output parameter:
pdf (default) — binary PDF downloadbase64_pdf — JSON response with the label as a base64-encoded stringLabel format parameter:
A4 — A4 page sizeA5 — A5 page sizeA6 — A6 page size| order_id required | string Order ID. |
| label_format | string Enum: "A4" "A5" "A6" Override the label format. If omitted, the user's preferred format is used. |
| output | string Enum: "base64_pdf" "pdf"
|
Returns nearby PUDO points filtered by the partner assigned to an existing order. Provide order_id plus one of: for_shipper=true, for_consignee=true, or an explicit address/postcode/country.
| address | string Explicit address to geocode |
| country | string ISO alpha-2 country code |
| distance_km | integer Search radius in km (1-50, default 5) |
| for_consignee | boolean Use order consignee address |
| for_shipper | boolean Use order shipper address |
| limit | integer Max results (1-30, default 10) |
| order_id required | string 6-char order short code |
| postcode | string Postcode for explicit address |
[- {
- "pudo_points": [
- {
- "id": "42",
- "name": "InPost Locker Vilnius Akropolis",
- "address": "Ozo g. 25",
- "city": "Vilnius",
- "postcode": "07150",
- "country": "LT",
- "point_type": "LOCKER",
- "latitude": "54.7100000",
- "longitude": "25.2700000",
- "distance_km": 1.23
}
]
}
]Returns the cheapest shipping price for a given route and parcel dimensions.
| country_from required | string ISO 3166-1 alpha-2 sender country code |
| country_to required | string ISO 3166-1 alpha-2 receiver country code |
| height | number <double> Default: 1 Height in cm |
| length | number <double> Default: 1 Length in cm |
| package_type | string Default: "parcel" parcel or pallet |
| transportation_type | string Default: "door_to_door" door_to_door, door_to_locker, locker_to_door, locker_to_locker |
| weight | number <double> Default: 1 Weight in kg |
| width | number <double> Default: 1 Width in cm |
{- "is_business": false,
- "country_from": "DE",
- "country_to": "ES",
- "transportation_amount": "18.54",
- "vat_amount": "3.52",
- "amount": "22.06",
- "currency": "EUR",
- "weight": "1.00",
- "delivery_time": 72,
- "transportation_type": "door_to_door",
- "package_type": "parcel"
}Returns one best-value rate per available transportation type (door, locker, ...), sorted cheapest-first.
| country_from required | string ISO 3166-1 alpha-2 sender country code |
| country_to required | string ISO 3166-1 alpha-2 receiver country code |
| height | number <double> Default: 1 Height in cm |
| length | number <double> Default: 1 Length in cm |
| package_type | string Default: "parcel" parcel or pallet |
| weight | number <double> Default: 1 Weight in kg |
| width | number <double> Default: 1 Width in cm |
{- "is_business": true,
- "country_from": "string",
- "country_to": "string",
- "currency": "string",
- "rates": [
- {
- "transportation_type": "string",
- "transportation_amount": "string",
- "vat_amount": "string",
- "amount": "string",
- "delivery_time": 0
}
]
}Returns tracking information for an order by 6-char order code or 7-8 char parcel code. Authenticated callers get their own orders directly; every other caller must pass the postcode of the order's collection or delivery address, as the public web tracker requires.
| order_id required | string 6-char order code or 7-8 char parcel code |
| postcode | string Collection or delivery postcode of the order. Required unless the order belongs to the authenticated account. |
{- "order_id": "KR4PZK",
- "country_from": "DE",
- "country_to": "EE",
- "status": "NEW",
- "estimated_collection_date": "2026-03-10",
- "estimated_delivery_date": "2026-03-13",
- "addons": [ ],
- "parcels": [
- {
- "parcel_id": "KR4PZKA",
- "status": "NEW",
- "tracking_events": [
- {
- "tracked_on_time": "2026-03-09T14:30:00Z",
- "event_code": "0002",
- "event_description": "Prepared for collection",
- "event_comment": "",
- "event_country": "DE"
}
]
}
]
}