Mydema Plugin API — Integration Guide
This guide is for developers who connect an e-shop or ERP to Mydema the same way the official OpenCart / WooCommerce plugins do: show live shipping prices at checkout, turn paid orders into Mydema shipments (vouchers), and track them.
A ready-to-run Postman collection is in this folder:
| File | What it is |
|---|---|
mydema-plugin-api.postman_collection.json | All requests in this guide, in call order, with scripts that carry the token, basket id and chosen route from one request to the next |
mydema-staging.postman_environment.json | Staging base URL and placeholders for your key |
mydema-production.postman_environment.json | Production base URL and placeholders for your key |
#1. Environments
| Environment | Base URL |
|---|---|
| Production | https://app.mydema.gr |
| Staging | https://stage.mydema.gr |
Every path in this guide is relative to the base URL. When you get a key through the self-service connect flow (section 2.1), the claim response tells you the base URL to use (apiEndpoint).
Staging uses its own data and its own keys; a production key does not work on staging.
#2. Getting an API key
An API key (mk_live_…, 48 characters) belongs to one site (an "integration" in Mydema). Each site has:
- a service user that the API acts as,
- a prepaid credit wallet that pays for its shipments (section 7.9),
- optionally a domain and an IP allowlist that every login is checked against.
There are two ways to get a key.
#2.1 Self-service connect (recommended for plugins)
The shop asks to connect, Mydema staff approve it in the panel, and the shop picks up its key. These three endpoints are public (no token).
- Generate a random secret (for example 32 random bytes as hex) and keep it on your side.
POST/api/plugin/site-requestwith the shop details andclaimSecretHash= SHA-256 hex of that secret. You get arequestId.- Poll
POST/api/plugin/site-request/statuswithrequestId+ the rawclaimSecretuntilstatusisapproved(orrejected, withrejectReason). Polling every few minutes is enough; approval is manual. POST/api/plugin/site-request/claimwith the same two values. You getapiKey,apiEndpointandintegrationId. Every claim issues a new key and invalidates the previous one, so claim once and store the key.
#2.2 Key issued by Mydema
Mydema staff can create the site in the panel and send you the key directly. It works exactly like a claimed key.
Store the key like a password. Mydema keeps only its hash and cannot show it again; a lost key means a new claim or a new key from Mydema.
#3. Authentication
#3.1 Log in
POST /api/auth/plugin-login
Content-Type: application/json
{"apiKey": "mk_live_…", "apiSecret": "", "domain": "myshop.gr"}| Field | Required | Notes |
|---|---|---|
apiKey | yes | The mk_live_… key |
apiSecret | yes, as "" | Must be an empty string for mk_live_ keys. (A non-empty value switches to an older key + secret scheme.) |
domain | if the site has a domain | Your shop's host name. Scheme, path, port and a leading www. are ignored on both sides. |
If the site has an IP allowlist, the request must come from one of the listed addresses (exact match, no CIDR ranges). The rejection message tells you the address Mydema saw.
Response (shortened):
{
"data": {
"accessToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9…",
"expiresAt": "2026-10-31T07:59:12+00:00",
"expiresIn": 43200,
"integration": {"id": 8, "name": "My Shop", "platform": null, "domain": "myshop.gr"},
"tokenData": {
"user": {"id": 260, "userRole": "PLUGIN", "customer": 273},
"company": {"id": 4, "name": "Customers Company", "type": "CUS"},
"plugin": true,
"integrationId": 8,
"pluginCustomerId": 273,
"pluginCustomerName": "Customers Company"
}
}
}Keep integrationId and pluginCustomerId (the wallet owner); a few endpoints need them in the path.
#3.2 Use the token
Send it on every other request:
Authorization: Bearer <accessToken>Exactly Bearer + one space; the check is case-sensitive.
#3.3 Lifetime and renewal
- The token is valid for 12 hours, sliding: every successful request moves the expiry to 12 hours from now. A shop with regular traffic keeps the same token indefinitely.
- Logging in again while the token is still valid returns the same token, so several workers of the same shop can share it safely.
When the token is no longer valid, any endpoint returns HTTP 401 with this body (note: no
data/errorwrapper):JSON{"message": "User timed out.", "code": "TOKEN_EXPIRED"}codeMeaning MISSING_TOKENNo / malformed Authorizationheader, or unknown tokenTOKEN_EXPIREDMore than 12 hours without a request TOKEN_CHANGEDA newer token was issued for this site On any of these: log in again and repeat the request once.
Login failures are HTTP 403 in the standard error envelope (section 4.3), for example Invalid credentials., Domain not allowed for this key., IP not allowed for this key. Detected IP: ….
#4. Conventions: requests, responses, errors
#4.1 Requests
- JSON bodies, UTF-8,
Content-Type: application/json. SendAccept: application/json. - List and search endpoints are
POST, even when they only read. Calling them withGETeither returns 405 or hits a different route and fails. - Money is EUR. Weights are kg. Package dimensions are cm in requests (Mydema stores and returns them in metres).
#4.2 Successful responses
HTTP 200 and a data object:
{"data": {"basketId": "695bca64-c645-4398-8b67-c16adcc89de4"}}#4.3 Error responses
Mydema uses more than one error shape. Handle all of them.
A. Standard envelope (most endpoints):
{"error": {"message": "{\"message\":\"District is not valid.\",\"code\":400}", "code": 500, "file": null, "line": null}}error.message is usually itself a JSON string. Decode it a second time to get the readable message and the intended code (400 bad input, 403 not yours, 404 not found, 409 conflict). If it is not valid JSON, use it as plain text.
B. Framework error (some endpoints, marked "B" in section 7):
{"type": "https://tools.ietf.org/html/rfc2616#section-10", "title": "An error occurred", "status": 500, "detail": "Internal Server Error"}On production the detail is generic, so you will not see the reason. Validate your input before sending (section 6) and log the request you sent.
C. Token errors: HTTP 401, {"message": …, "code": "TOKEN_EXPIRED"} (section 3.3).
D. Connect endpoints (/api/plugin/site-request*) use a clean envelope with the real HTTP status: {"error": {"message": "Request is not approved.", "code": 409}}.
E. Temporary database lock: HTTP 503 with a Retry-After header and {"error": {"message": "…temporary database lock…", "code": "DB_RETRY"}}. Wait and retry.
#4.4 HTTP status codes
| Status | Meaning | Retry? |
|---|---|---|
| 200 | Success | — |
| 401 | Token problem (section 3.3) | Log in again, then retry once |
| 403 | Login rejected | No; fix key / domain / IP |
| 404, 405 | Wrong path or wrong HTTP method | No |
| 500 | Business or validation error (bad input, not found, insufficient credit…) — the real reason is in the body | No: the same request fails again |
| 503 | Temporary database lock | Yes, after Retry-After |
| Network error / timeout | — | Reads and create: yes. complete: yes, it is safe per basket (rule 8.2) |
Most validation and business errors come back as HTTP 500, not 4xx. Do not treat 500 as a server outage and do not retry it blindly.
#4.5 List request format
Endpoints described as "ListRequest" accept this optional body:
| Key | Type | Notes |
|---|---|---|
params | object | Endpoint-specific filters, e.g. {"state": 65} |
limit | int | Page size. No default on most lists: omit it and you get everything |
offset | int | Rows to skip |
orderBy | object | e.g. {"name": "ASC"} |
{} is a valid body. Numbers must be numbers, not strings.
Results look like {"data": {"<items>": [...], "meta": {"total": 17}}}.
#5. The integration flow at a glance
one-time setup checkout (per cart) order (per paid order)
────────────── ─────────────────── ──────────────────────
plugin-login create basket ──► search-routes update-basket (real contact, comment)
shipment/templates (one basket per origin warehouse) search-routes (fresh)
suppliers (warehouses) show Σ segment.price per route set-routes (chosen route)
location lookups no route? ──► scenarios credit balance check
payment-complete (creditAmount)
complete ──► transportation id
voucher-pdf, tracking- Setup. Log in. Load the package templates. Register each warehouse the goods ship from as a supplier (or use one origin district for everything). Map your customers' addresses to Mydema district ids (section 6.1).
- Checkout quote. For each origin warehouse in the cart, create a basket with the packages, sender (warehouse) and receiver, then search routes. Each route is one shipping option; its price is the sum of its segments'
price(section 7.6). KeepbasketIdand the route's identity (routeIdof the segments) with the customer's choice. - Order → shipment. When the order is paid, update the basket with the customer's real contact details and comment, search routes again (prices are fresh at that moment), send the chosen route with
set-routes, record the payment withpayment-complete, then callcompleteonce. You get the Mydema transportation order id. - After. Download the voucher PDF and poll the tracking endpoint for status.
A cart with goods from several warehouses is several baskets: one per warehouse (sender), each priced and completed separately. The official plugins show their sum as one shipping price.
#6. Reference data: locations, packages, people
#6.1 Locations
Mydema's location tree is country → state → city → district. On production, a state is a Greek prefecture (νομός) and a city is a municipality.
The shipment endpoints need the district id of the sender and of the receiver. Routes run between districts, and only served districts have routes.
| You have | Use |
|---|---|
| A place name typed by a customer | POST/api/location/district/avail-autocomplate → locations[].id (served districts only) |
| A prefecture, want its served districts | POST/api/location/district/avail-list with params.state → locations[].districtId |
| A postal code | POST/api/autocomplate/location/postal-code → place names (not ids); then match the name with the two calls above |
| Prefecture name → state id | POST/api/location/state/list |
The full served list (avail-list without filters) is small enough to cache for a few hours.
#6.2 Packages
Load the package catalogue once a day from GET/api/shipment/templates. Values at the time of writing (October 2026):
| Code | Type | Volume band (m³) | Billed as (m³) | Weight (kg) | Footprint (cm) |
|---|---|---|---|---|---|
| S | M3 (box) | 0 – 0.20 | 0.20 | 0 – 80 | — |
| M | M3 (box) | 0.2001 – 0.50 | 0.50 | 0 – 80 | — |
| L | M3 (box) | 0.5001 – 1.00 | 1.00 | 0 – 80 | — |
| XL | M3 (box) | 1.0001 – 1.60 | 1.60 | 0 – 80 | — |
| PEUR1 | PL (pallet) | — | 1.92 | 80 – 1000 | 80 × 120, height 200 |
| PEUR2 | PL (pallet) | — | 2.40 | 80 – 1000 | 100 × 120, height 200 |
| PMNI6 | PL (pallet) | — | 0.48 | 80 – 300 | 60 × 80, height 100 |
Mydema can change these values; read them from the API rather than hard-coding them.
Package object (used by create, update-basket, quote-districts):
| Field | Type | Required | Notes |
|---|---|---|---|
weightType | string | yes | M3 = box / parcel, PL = pallet, KG = special items |
weight | number | yes | kg, one parcel |
width, height, depth | number | yes | cm |
isSensitive | bool | no | Fragile goods; default false |
shipmentType, shipmentSize | string | see below | Template / special-item codes |
productWidth, productHeight, productDepth | number | no | The product's own size in cm. Printed on the voucher only; not used for pricing |
How the price is found:
- M3 (box): by volume (
width × height × depth, m³) and weight. One parcel may weigh at most 220 kg. Send one package per physical parcel. OmitshipmentTypeandshipmentSize— an unknown code makes the whole request fail withSize is not valid/Type is not valid. - PL (pallet): by footprint:
widthanddepthmust be one of the pallet footprints above (either orientation, e.g. 80 and 120 for PEUR1).heightis the load height; it does not affect the price. OmitshipmentType/shipmentSize. - KG (special items): both
shipmentTypeandshipmentSizeare required and must be codes Mydema gave you for that item.
Several identical parcels = several package objects. A package with weight 0 and no shipmentType is silently ignored.
#6.3 Sender and receiver
{
"firstName": "Μαρία",
"lastName": "Παπαδοπούλου",
"email": "[email protected]",
"phone": "6900000000",
"customerType": "I",
"taxId": "",
"address": {
"address": "Οδός Παραλήπτη 5",
"postalCode": "53100",
"district": 158,
"city": null,
"officeName": "",
"taxOffice": "",
"location": ""
}
}| Field | Notes |
|---|---|
customerType | I individual, S business |
taxId | Greek VAT number (ΑΦΜ) for businesses, max 20 characters |
address.district | Required. District id (section 6.1). A missing or unknown district fails the request |
address.city | City id, or null to take it from the district (recommended) |
email, phone | Use real values when you complete an order: Mydema e-mails the receiver when the shipment is created and the courier sees the phone number. At quote time placeholders are fine |
The sender is your warehouse. Use customerType S and the warehouse's district.
#7. Endpoint reference
All endpoints except those in 7.1 need Authorization: Bearer <token>. "Errors: A/B" refers to the error shapes in section 4.3.
#7.1 Connect (public)
#POST/api/plugin/site-request
| Field | Required | Notes |
|---|---|---|
domain | yes | Shop host name |
companyName | yes | max 191 |
contactName | yes | max 100 |
contactEmail | yes | Approval e-mail goes here |
claimSecretHash | yes | SHA-256 hex (64 chars) of your secret |
platform | no | opencart3, opencart4, woocommerce, magento2; other values are stored as empty |
siteName, pluginVersion, taxId, taxOffice, address, postalCode, city, contactPhone | no | Billing / contact details |
{"data": {"requestId": "9b2f8f8e-3c1a-4f7e-9d1e-1a2b3c4d5e6f", "status": "pending"}}Sending the same domain and secret again returns the same request. Errors (shape D): 400 invalid field, 409 another request for this domain is open, 429 more than 5 requests per hour from one IP.
#POST/api/plugin/site-request/status
Body: {"requestId": "…", "claimSecret": "<raw secret>"}
{"data": {"status": "approved", "siteName": "My Shop", "domain": "myshop.gr", "rejectReason": null}}status: pending, approved, rejected, claimed. Unknown id or wrong secret → 404.
#POST/api/plugin/site-request/claim
Body: {"requestId": "…", "claimSecret": "<raw secret>"}
{"data": {"apiKey": "mk_live_…", "apiEndpoint": "https://app.mydema.gr", "integrationId": 57}}Each call rotates the key. Errors: 404, 409 not approved, 410 site no longer active.
#7.2 Authentication
| Endpoint | Notes |
|---|---|
POST/api/auth/plugin-login | Section 3.1 |
POST/api/auth/plugin-autologin | Opens a Mydema panel page (credit top-up payment) in the merchant's browser without a password. Section 7.9 |
#7.3 Locations
All are POST. Errors: B.
#POST/api/location/district/avail-autocomplate
Served districts matching a name (Greek or Latin, accents optional; falls back to a fuzzy match).
Body: {"term": "Κομοτην", "limit": 10} (limit default 100)
{"data": {"locations": [{
"id": 1, "name": "Κομοτηνή", "nameLatin": "Komotini", "cityName": "Κομοτηνή", "CountryName": "Ελλάδα",
"city": {"id": 67, "name": "Κομοτηνή", "state": {"id": 65, "name": "Ροδόπης"}, "country": {"code": "GR"}},
"country": {"code": "GR", "name": "Ελλάδα"}
}]}}(CountryName really starts with a capital C.)
#POST/api/location/district/avail-list — ListRequest
Every served district, flat. Filters: params.state (state id), params.city (city id). Without limit you get all of them.
Body: {"params": {"state": 65}}
{"data": {
"locations": [{"districtId": 1, "name": "Κομοτηνή", "cityId": 67, "cityName": "Κομοτηνή", "stateId": 65, "stateName": "Ροδόπης"}],
"locationDistricts": ["…same districts in the long format of avail-autocomplate…"],
"meta": {"total": 4, "limit": null, "offset": 0}
}}Use locations; locationDistricts is kept for older clients.
#POST/api/autocomplate/location/postal-code
(The spelling autocomplate is part of the path.) Body: {"term": "69131", "limit": 50} (prefix match; limit default 10).
{"data": {"locationPostalCodes": [
{"id": 12752, "code": "69131", "district": "ΑΓΙΑ ΒΑΡΒΑΡΑ", "city": "ΡΟΔΟΠΗΣ"},
{"id": 12754, "code": "69131", "district": "ΚΟΜΟΤΗΝΗ", "city": "ΡΟΔΟΠΗΣ"}
]}}district is the settlement name and city the prefecture name — text, not ids. A postal code can cover several settlements.
#POST/api/location/state/list — ListRequest
Body: {"orderBy": {"name": "ASC"}}
{"data": {"locationStates": [{"id": 65, "name": "Ροδόπης", "nameLatin": null, "originalName": "Ροδόπης", "country": {"code": "GR"}}], "meta": {"total": 59}}}#POST/api/location/city/avail-list — ListRequest
All cities of one state. params.state is required (without it the list is empty).
Body: {"params": {"state": 65}} → {"data": {"locationCities": [{"id": 542, "name": "Αίγειρος", "state": {"id": 65, "name": "Ροδόπης"}}], "meta": {"total": 14}}}
#7.4 Package templates
#GET/api/shipment/templates
{"data": {"templates": [
{"code": "S", "name": "Small", "shipmentType": "M3", "lengthCm": 0, "widthCm": 0, "heightCm": 0,
"minWeightKg": 0, "maxWeightKg": 80, "minVolumeM3": 0, "maxVolumeM3": 0.2, "volumeM3": 0.2,
"billingVolumeM3": 0.2, "isPallet": false, "displayOrder": 1},
{"code": "PEUR1", "name": "Standard EUR 1 80x120x200", "shipmentType": "PL", "lengthCm": 80, "widthCm": 120, "heightCm": 200,
"minWeightKg": 80, "maxWeightKg": 1000, "minVolumeM3": null, "maxVolumeM3": null, "volumeM3": 1.92,
"billingVolumeM3": 1.92, "isPallet": true, "displayOrder": 5}
]}}See section 6.2 for how to use them. Errors: A.
#7.5 Suppliers (warehouses / pickup addresses)
A supplier is a place your goods are picked up from. Its district is the sender district of the baskets for those goods. Each site sees and edits only its own suppliers. Errors: A (permission problems: B).
Supplier object
{
"id": 3, "integrationId": 8, "companyId": 4, "companyUserId": 260,
"name": "Αποθήκη Κομοτηνής", "pickupAddress": "Οδός Δοκιμής 1",
"districtId": 1, "districtName": "Κομοτηνή", "cityId": 67, "cityName": "Κομοτηνή", "postalCode": "69100",
"latitude": null, "longitude": null, "googlePlaceId": null,
"contactName": "Γιώργος", "contactPhone": "2531000000", "contactEmail": null,
"pickupHoursStart": "09:00:00", "pickupHoursEnd": "17:00:00",
"notes": null, "externalCode": "WH-1", "isActive": true,
"createdAt": "2026-10-30T19:58:41+00:00", "updatedAt": "2026-10-30T19:58:41+00:00"
}Writable fields: name (required, max 200), pickupAddress (required, max 500), districtId (the city is derived from it), postalCode, latitude, longitude, googlePlaceId, contactName, contactPhone, contactEmail, pickupHoursStart, pickupHoursEnd ("09:00"), notes, externalCode (your own id, unique per site).
| Endpoint | Body | Response |
|---|---|---|
POST/api/supplier | supplier fields | {"data": {"supplier": {…}}} |
GET/api/supplier/{id} | — | {"data": {"supplier": {…}}} |
PUT/api/supplier/{id} | only the fields to change | {"data": {"supplier": {…}}} |
DELETE/api/supplier/{id} | — | {"data": {"deleted": true}} |
POST/api/supplier/list | {} | {"data": {"suppliers": […], "total": 1}} |
GET/api/integration/{integrationId}/supplier | — | Same as list |
POST/api/supplier/sync | {"integrationId": 8, "rows": [{…, "externalCode": "WH-1", "cityId": 67}]} | {"data": {"created": 1, "updated": 0, "errors": []}} |
Notes:
- On
PUT, do not sendintegrationId(or send0, which means "keep"). Sending"districtId": nullclears the district. syncupserts byexternalCode(max 200 rows per call) and needscityIdon every row. Row errors are listed inerrorswith the rowindex; the rest are saved.- An
externalCodeof a deleted supplier cannot be reused. - Common errors:
District is not valid.,Please select a district.,Supplier not found.,Supplier with this externalCode already exists for this integration.(409).
#7.6 Shipments: quote
#POST/api/transportation/create — create a basket
{
"packages": [{"weightType": "M3", "weight": 25, "width": 58.48, "height": 58.48, "depth": 58.48, "isSensitive": false}],
"sender": {"firstName": "Αποθήκη", "lastName": "Κομοτηνής", "email": "[email protected]", "phone": "2531000000", "customerType": "S",
"address": {"address": "Οδός Δοκιμής 1", "postalCode": "69100", "district": 1, "city": null}},
"receiver": {"firstName": "Μαρία", "lastName": "Παπαδοπούλου", "email": "[email protected]", "phone": "6900000000", "customerType": "I",
"address": {"address": "Οδός Παραλήπτη 5", "postalCode": "", "district": 158, "city": null}}
}{"data": {"basketId": "695bca64-c645-4398-8b67-c16adcc89de4"}}A basket is a temporary quote/order draft. Nothing is booked yet. Errors: B. Common reasons: an unknown district, Size is not valid / Type is not valid (see 6.2), missing sender or receiver. Send the order comment later with update-basket (the comments field here is limited to 10 characters).
#POST/api/transportation/search-routes
Body: {"basketId": "…"}
{"data": {
"basketId": "695bca64-c645-4398-8b67-c16adcc89de4",
"serviceFee": 10,
"serviceFees": [{"total": 10.07, "serviceFee": 0, "tax": 0.22}],
"routes": [[
{"startPoint": 3, "endPoint": 698, "company": 15, "onlyB2B": false, "routeId": 78,
"price": 5.26, "tax": 1.01, "currency": "EUR", "hasAddressDelivery": true,
"deliveryDoor": false, "carrier": false, "deliveryDoorPrice": 0, "deliveryDoorTax": 0, "carrierPrice": 0, "carrierTax": 0,
"data": {"route": "Ροδόπης, Κομοτηνή-Θεσσαλονίκη, Κέντρο Διαλογής", "company": "ΚΟΚΟΒΙΔΗΣ ΦΡΙΓΚΟΤΡΑΝΣ",
"endPointName": "Θεσσαλονίκη, Κέντρο Διαλογής", "endPointAddress": "Π.Σ.Ο. Θεσ/νίκης-Ωραιοκάστρου\nΤηλέφωνα : 2310 785 050",
"companyPrices": [7095], "price": 4.78, "currency": "EUR"},
"packagePrices": [{"weightType": "M3", "weight": 25, "width": 0.5848, "height": 0.5848, "depth": 0.5848,
"shipmentSize": null, "shipmentType": null, "isSensitive": false, "productDimsCm": null,
"price": 4.78, "tax": 0.92, "basePrice": 3.86}],
"basePrice": 4.25, "serviceFee": 0},
{"startPoint": 698, "endPoint": 179, "company": 18, "onlyB2B": false, "routeId": 385,
"price": 5.84, "tax": 1.13, "currency": "EUR", "hasAddressDelivery": false,
"deliveryDoor": false, "carrier": false, "deliveryDoorPrice": 0, "deliveryDoorTax": 0, "carrierPrice": 0, "carrierTax": 0,
"data": {"route": "Θεσσαλονίκη, Κέντρο Διαλογής-Φλώρινας, Φλώρινα", "company": "ΣΑΛΒΑΡΙΝΑΣ ΑΛ. ΙΩΑΝΝΗΣ",
"endPointName": "Φλώρινας, Φλώρινα", "endPointAddress": "Λεωφόρος Αβέρωφ 47, Φλώρινα\nΤηλέφωνο: 2385023995",
"companyPrices": [953], "price": 5.31, "currency": "EUR"},
"packagePrices": ["…"], "basePrice": 4.71, "serviceFee": 0}
]]
}}How to read it:
routesis a list of routes; each route is a list of segments (legs), one per carrier. The example has one route with two legs: Komotini → Thessaloniki hub → Florina.- Price shown to the customer = Σ
priceof the route's segments, VAT and Mydema's service fee included (here 5.26 + 5.84 = 11.10 €). The VAT inside it is Σtax(2.14 €). - Do not use
serviceFeesordata.priceas the customer price; they are internal figures. - Delivery mode comes from the last segment:
hasAddressDelivery: true= delivered to the receiver's address;false= the receiver collects it from the carrier's branch atdata.endPointName/data.endPointAddress. - Carrier names for display:
data.companyof each segment. - Routes are not sorted. If any direct (one-leg) route exists, only direct routes are returned.
routes: []with HTTP 200 means no carrier serves this combination (seescenarios).
Search-routes does not change the basket; call it as often as you need.
#POST/api/transportation/scenarios
Explains an empty result.
Body: {"receiverDistrictId": 158, "isB2B": false, "hasAfm": false}
{"data": {"inService": true, "scenarios": [{"type": "IN_SERVICE"}, {"type": "B2C_SMS_NOTIFY"}]}}When the district is not served: {"inService": false, "scenarios": [{"type": "HUB_PICKUP", "hub": {"transferLocationId": 48, "name": "…", "address": "…", "cityId": 5, "districtId": 101}}]} (hub may be null). Errors: A.
#POST/api/transportation/quote-districts — price several destinations at once
For when you know the customer's area but not the exact district: prices the same cart to up to 15 districts in one call, without creating a basket.
Body: the create body plus "districtIds": [158, 1]. The receiver's address.district is replaced by each id in turn.
{"data": {"districts": [
{"districtId": 158, "found": true, "name": "Φλώρινα", "nameLatin": "Florina", "cityId": 192, "cityName": "Φλώρινα",
"serviceFee": 10, "serviceFees": ["…"], "routes": [["…segments as in search-routes…"]]},
{"districtId": 999999, "found": false, "routes": [], "serviceFees": []}
]}}When the customer picks one, create a basket for that district and continue normally. Errors: B.
#7.7 Shipments: order → voucher
#POST/api/transportation/update-basket
Replaces the basket's packages, sender and receiver, and sets the order comment.
{"basketId": "…", "packages": ["…same as create…"], "sender": {"…": "…"}, "receiver": {"…": "…"},
"receiverComment": "Παρακαλώ τηλεφωνήστε πριν την παράδοση"}Response: {"data": {"basketId": "…"}}. Errors: B.
- Always send
packagesagain: omitting them leaves the basket empty. - Use it at order time to put the customer's real name, phone and e-mail on the receiver.
receiverCommentappears as the receiver's instructions on the shipment.
#POST/api/transportation/set-routes
Stores the chosen route. routes holds the segments of one route (not a list of routes), copied from a search-routes response of the same basket:
{"basketId": "…", "routes": [
{"company": 15, "startPoint": 3, "endPoint": 698, "routeId": 78, "price": 5.26, "tax": 1.01, "serviceFee": 0,
"packagePrices": ["…as returned…"], "deliveryDoor": false, "deliveryDoorPrice": 0, "carrier": false, "carrierPrice": 0,
"hasAddressDelivery": true},
{"company": 18, "startPoint": 698, "endPoint": 179, "routeId": 385, "price": 5.84, "tax": 1.13, "serviceFee": 0,
"packagePrices": ["…"], "deliveryDoor": false, "deliveryDoorPrice": 0, "carrier": false, "carrierPrice": 0,
"hasAddressDelivery": false}
]}Every field above is required except hasAddressDelivery; packagePrices must not be empty. Send the values exactly as search-routes returned them. Response: {"data": {"basketId": "…"}}. Errors: B.
#POST/api/transportation/payment-complete
Records how the shipment is paid. For prepaid-wallet sites (all plugin sites), set creditAmount to the route total so it is taken from the wallet at complete.
{"basketId": "…", "totalPrice": 11.10, "creditAmount": 11.10, "status": "success",
"conversationId": "order-1001", "paymentType": "CREDIT", "currency": "EUR", "bank": "MyShop", "card": "MyShop"}| Field | Notes |
|---|---|
totalPrice | Σ segment price of the chosen route |
creditAmount | Amount to take from the wallet. Use the same value as totalPrice |
status | Must be success, otherwise complete refuses the basket |
conversationId | Your order reference, max 50 |
paymentType, currency, bank, card | Required non-empty strings; CREDIT, EUR and your shop name are fine |
Response: {"data": {"basketId": "…"}}. Nothing is charged yet. Errors: B.
#POST/api/transportation/complete
Creates the Mydema transportation order (the voucher) from the basket. Body: {"basketId": "…"}.
{"data": {"transportations": {"id": 5123, "orderNumber": "MY-1042", "transferStatus": {"code": "CR", "name": "Created"},
"totalPrice": 11.1, "shipments": ["…"], "details": ["…"]}}}The full object has the same shape as GET/api/transportation/{id} (7.8). Store data.transportations.id. Errors: B.
What happens:
creditAmountis taken from the site's wallet. Not enough credit: the request fails (Insufficient credit.) and nothing is created. Check the balance first (7.9).- The carriers on the route are notified, the receiver gets an e-mail (if the receiver e-mail is real), and the sender address gets a confirmation e-mail.
- Safe to repeat for the same basket. Once a basket has become a shipment,
completereturns that same shipment again and charges nothing;set-routesandpayment-completeon that basket are ignored (rule 8.2).
Preconditions, in order: basket exists; payment recorded with status: success (Payment is not valid otherwise); enough credit.
#7.8 Shipments: voucher and tracking
#GET/api/transportation/{id}/voucher-pdf?status=confirmed
Returns the voucher as application/pdf (Content-Disposition: attachment; filename="MY-1042.pdf"). status is confirmed or temporary; it only sets the X-Mydema-Voucher-Status response header. Errors: A.
#GET/api/transportation/{id}
{"data": {"transportationOrder": {
"id": 5123, "orderNumber": "MY-1042", "totalPrice": 11.1, "taxTotal": "2.14", "serviceFee": 0,
"transferStatus": {"code": "CR", "name": "Created"},
"senderComment": "", "deliverComment": " Παρακαλώ τηλεφωνήστε πριν την παράδοση",
"sendAddress": {"…": "…"}, "deliverAddress": {"…": "…"},
"shipments": [{
"id": 9001, "orderNumber": "MY-1042",
"route": {"id": 78, "route": "Κομοτηνή - Θεσσαλονίκη", "company": {"id": 15, "name": "…", "phone": "…"}},
"shipmentStatus": {"code": "TR", "name": "In transit"},
"actions": [{"actionDate": "2026-10-31", "shipmentStatus": {"code": "TR", "name": "In transit"}}],
"packages": ["…"]
}],
"details": ["…one per package…"],
"createdAt": "2026-10-30 10:00:00", "updatedAt": "2026-10-31 09:12:00"
}}}shipments has one entry per leg; actions is that leg's history. Errors: A (Company not found. means the order id does not exist).
Order transferStatus.code | Meaning |
|---|---|
| CR | Created |
| PP | Pending payment |
| PA | Paid |
| TR | In transport |
| CM | Completed |
| CL | Cancelled |
Shipment shipmentStatus.code | Meaning |
|---|---|
| DO | Order created |
| PE | Pending |
| RE | Received by the carrier |
| TR | In transit |
| DT | Out for delivery |
| DE | Delivered |
There is no webhook; poll this endpoint (for example hourly for open shipments).
#7.9 Credit wallet
Shipments are paid from the site's prepaid wallet. All three endpoints work on the wallet of the logged-in site. Errors: B.
#GET/api/customer/credit/balance
{"data": {"balance": 42.5, "currency": "EUR"}}#GET/api/customer/credit/transactions?page=1&limit=50
limit 1–200, default 50.
{"data": {"transactions": [
{"id": 9, "type": "shipment", "amount": -11.1, "balanceAfter": 31.4, "currency": "EUR",
"refType": "basket", "refId": "695bca64-…", "description": "Shipment payment", "createdAt": "2026-10-30 14:02:11"}
], "total": 1, "page": 1, "limit": 50}}type: topup, shipment, refund. createdAt is server local time without a time-zone suffix.
#POST/api/customer/credit/topup + payment page
Top-ups are paid by card on Mydema's own payment page.
POST/api/customer/credit/topupwith{"amount": 50}(net amount, 1–1000 €). Response:{"data": {"basketId": "…", "customerId": 273, "amount": 50, "tax": 12, "chargeTotal": 62, "currency": "EUR"}}(24% VAT is added).POST/api/auth/plugin-autologinwith{"redirect": "/customer/<customerId>/credit/payment/<basketId>/init"}→{"data": {"token": "…"}}(valid for 2 minutes).- Open
<base URL>/plugin/auto-login?t=<token>in the merchant's browser. Mydema logs the merchant in and shows the card payment page. The balance grows when the payment succeeds.
#7.10 Optional
#POST/api/afm — Greek VAT number lookup
Body: {"afmNumber": "099999999"} (9–12 characters). Errors: B.
{"data": {"taxData": {"taxId": "099999999", "taxOffice": "ΦΑΕ ΑΘΗΝΩΝ", "name": "ACME ΑΕ", "address": "ΟΔΟΣ 1", "zip": "10431", "location": "ΑΘΗΝΑ"}}}An unknown number returns the same shape with empty strings.
#Site settings
Free key/value settings stored at Mydema for your site.
| Endpoint | Notes |
|---|---|
GET/api/integration/{integrationId}/settings | {"data": {"settings": {"currency": {"key": "currency", "value": "EUR", "valueType": "string"}}}} ([] when empty) |
GET/api/integration/{integrationId}/settings/{key} | {"data": {"setting": {…}}}; unknown key → Setting not found. |
PUT/api/integration/{integrationId}/settings | {"settings": [{"key": "currency", "value": "EUR"}]} → {"data": {"written": 1, "errors": []}} |
DELETE/api/integration/{integrationId}/settings/{key} | {"data": {"deleted": true}} |
Allowed keys: free_shipping_threshold, hide_shipping_cost, currency, language, default_pickup_supplier_id, default_pickup_branch_id, buffer_ratio, risk_premium_ratio, auto_voucher_email_to, voucher_email_subject, pickup_warning_html, emergency_mode_enabled, fallback_template_under_35kg, fallback_template_over_35kg. Other keys are reported in errors.
#Voucher payments (weekly invoices)
Invoices Mydema issues for the site's vouchers in weekly batches, paid by bank transfer. Only relevant if Mydema invoices your site this way. All POST except the two GETs.
| Endpoint | Body | Response |
|---|---|---|
POST/api/voucher-payment/summary | {} | {"data": {"count": 0, "totalAmount": 0, "oldestDueDate": null}} |
POST/api/voucher-payment/list | {"status": "unpaid", "page": 1, "perPage": 50} (also weekBatchId, fromDate, toDate) | {"data": {"payments": [], "page": 1, "perPage": 50}} |
GET/api/voucher-payment/{id} | — | {"data": {"payment": {…}}} |
GET/api/voucher-payment/{id}/invoice-pdf | — | application/pdf |
#8. Integration rules and pitfalls
8.1 Business errors are HTTP 500. Read the body (section 4.3) instead of the status code, and do not auto-retry 500 responses. Only 503 and network failures are worth retrying.
8.2 complete is idempotent per basket. A basket becomes at most one shipment: calling complete again for the same basket returns the existing shipment (same id) and does not charge the wallet again, so after a timeout you can simply call it again. While a first call is still running, a second one fails with This basket is already being completed. Please try again in a moment. — retry a few seconds later. complete can take several seconds (it sends the e-mails before answering); use a timeout of at least 30 s. Still store the returned id against your order: one order with goods from several warehouses has one basket, and one shipment, per warehouse.
8.3 Price the route the customer saw, again, at order time. Call search-routes at order time, pick the route with the same routeIds, and send exactly those segments to set-routes. Prices can change between quote and order.
8.4 Check the wallet before complete. GET/api/customer/credit/balance ≥ route total, otherwise ask the merchant to top up (7.9) and retry later.
8.5 One basket per warehouse. Goods that leave from different districts are different shipments. Create, price, and complete a basket per sender.
8.6 Districts, not names. Every quote needs a district id for both sides. Resolve the customer's address to a served district (6.1) before calling create; when you can only narrow it to a prefecture, use quote-districts and let the customer choose.
8.7 Box packages: omit shipmentType / shipmentSize. An unknown code fails the whole request (6.2).
8.8 Resend packages on update-basket. Omitting them empties the basket.
8.9 Real receiver contact at order time. Mydema e-mails the receiver and the courier calls them; quote-time placeholders must be replaced through update-basket before complete.
8.10 Keep the token. Log in once, reuse the token, and log in again only after a 401 (3.3). Logging in on every request is unnecessary.