MydemaPlugin API
app.mydema.gr · stage.mydema.gr

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:

FileWhat it is
mydema-plugin-api.postman_collection.jsonAll 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.jsonStaging base URL and placeholders for your key
mydema-production.postman_environment.jsonProduction base URL and placeholders for your key

#1. Environments

EnvironmentBase URL
Productionhttps://app.mydema.gr
Staginghttps://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.

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

  1. Generate a random secret (for example 32 random bytes as hex) and keep it on your side.
  2. POST/api/plugin/site-request with the shop details and claimSecretHash = SHA-256 hex of that secret. You get a requestId.
  3. Poll POST/api/plugin/site-request/status with requestId + the raw claimSecret until status is approved (or rejected, with rejectReason). Polling every few minutes is enough; approval is manual.
  4. POST/api/plugin/site-request/claim with the same two values. You get apiKey, apiEndpoint and integrationId. 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

HTTP
POST /api/auth/plugin-login
Content-Type: application/json

{"apiKey": "mk_live_…", "apiSecret": "", "domain": "myshop.gr"}
FieldRequiredNotes
apiKeyyesThe mk_live_… key
apiSecretyes, as ""Must be an empty string for mk_live_ keys. (A non-empty value switches to an older key + secret scheme.)
domainif the site has a domainYour 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):

JSON
{
  "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:

HTTP
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 / error wrapper):

    JSON
    {"message": "User timed out.", "code": "TOKEN_EXPIRED"}
    codeMeaning
    MISSING_TOKENNo / malformed Authorization header, or unknown token
    TOKEN_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. Send Accept: application/json.
  • List and search endpoints are POST, even when they only read. Calling them with GET either 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:

JSON
{"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):

JSON
{"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):

JSON
{"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

StatusMeaningRetry?
200Success—
401Token problem (section 3.3)Log in again, then retry once
403Login rejectedNo; fix key / domain / IP
404, 405Wrong path or wrong HTTP methodNo
500Business or validation error (bad input, not found, insufficient credit…) — the real reason is in the bodyNo: the same request fails again
503Temporary database lockYes, 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:

KeyTypeNotes
paramsobjectEndpoint-specific filters, e.g. {"state": 65}
limitintPage size. No default on most lists: omit it and you get everything
offsetintRows to skip
orderByobjecte.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
  1. 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).
  2. 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). Keep basketId and the route's identity (routeId of the segments) with the customer's choice.
  3. 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 with payment-complete, then call complete once. You get the Mydema transportation order id.
  4. 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 haveUse
A place name typed by a customerPOST/api/location/district/avail-autocomplate → locations[].id (served districts only)
A prefecture, want its served districtsPOST/api/location/district/avail-list with params.state → locations[].districtId
A postal codePOST/api/autocomplate/location/postal-code → place names (not ids); then match the name with the two calls above
Prefecture name → state idPOST/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):

CodeTypeVolume band (m³)Billed as (m³)Weight (kg)Footprint (cm)
SM3 (box)0 – 0.200.200 – 80—
MM3 (box)0.2001 – 0.500.500 – 80—
LM3 (box)0.5001 – 1.001.000 – 80—
XLM3 (box)1.0001 – 1.601.600 – 80—
PEUR1PL (pallet)—1.9280 – 100080 × 120, height 200
PEUR2PL (pallet)—2.4080 – 1000100 × 120, height 200
PMNI6PL (pallet)—0.4880 – 30060 × 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):

FieldTypeRequiredNotes
weightTypestringyesM3 = box / parcel, PL = pallet, KG = special items
weightnumberyeskg, one parcel
width, height, depthnumberyescm
isSensitiveboolnoFragile goods; default false
shipmentType, shipmentSizestringsee belowTemplate / special-item codes
productWidth, productHeight, productDepthnumbernoThe 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. Omit shipmentType and shipmentSize — an unknown code makes the whole request fail with Size is not valid / Type is not valid.
  • PL (pallet): by footprint: width and depth must be one of the pallet footprints above (either orientation, e.g. 80 and 120 for PEUR1). height is the load height; it does not affect the price. Omit shipmentType / shipmentSize.
  • KG (special items): both shipmentType and shipmentSize are 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

JSON
{
  "firstName": "Μαρία",
  "lastName": "Παπαδοπούλου",
  "email": "[email protected]",
  "phone": "6900000000",
  "customerType": "I",
  "taxId": "",
  "address": {
    "address": "Οδός Παραλήπτη 5",
    "postalCode": "53100",
    "district": 158,
    "city": null,
    "officeName": "",
    "taxOffice": "",
    "location": ""
  }
}
FieldNotes
customerTypeI individual, S business
taxIdGreek VAT number (ΑΦΜ) for businesses, max 20 characters
address.districtRequired. District id (section 6.1). A missing or unknown district fails the request
address.cityCity id, or null to take it from the district (recommended)
email, phoneUse 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

FieldRequiredNotes
domainyesShop host name
companyNameyesmax 191
contactNameyesmax 100
contactEmailyesApproval e-mail goes here
claimSecretHashyesSHA-256 hex (64 chars) of your secret
platformnoopencart3, opencart4, woocommerce, magento2; other values are stored as empty
siteName, pluginVersion, taxId, taxOffice, address, postalCode, city, contactPhonenoBilling / contact details
JSON
{"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>"}

JSON
{"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>"}

JSON
{"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

EndpointNotes
POST/api/auth/plugin-loginSection 3.1
POST/api/auth/plugin-autologinOpens 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)

JSON
{"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}}

JSON
{"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).

JSON
{"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"}}

JSON
{"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

JSON
{"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

JSON
{
  "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).

EndpointBodyResponse
POST/api/suppliersupplier 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 send integrationId (or send 0, which means "keep"). Sending "districtId": null clears the district.
  • sync upserts by externalCode (max 200 rows per call) and needs cityId on every row. Row errors are listed in errors with the row index; the rest are saved.
  • An externalCode of 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

JSON
{
  "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}}
}
JSON
{"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": "…"}

JSON
{"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:

  • routes is 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 = Σ price of 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 serviceFees or data.price as 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 at data.endPointName / data.endPointAddress.
  • Carrier names for display: data.company of 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 (see scenarios).

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}

JSON
{"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.

JSON
{"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.

JSON
{"basketId": "…", "packages": ["…same as create…"], "sender": {"…": "…"}, "receiver": {"…": "…"},
 "receiverComment": "Παρακαλώ τηλεφωνήστε πριν την παράδοση"}

Response: {"data": {"basketId": "…"}}. Errors: B.

  • Always send packages again: 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.
  • receiverComment appears 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:

JSON
{"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.

JSON
{"basketId": "…", "totalPrice": 11.10, "creditAmount": 11.10, "status": "success",
 "conversationId": "order-1001", "paymentType": "CREDIT", "currency": "EUR", "bank": "MyShop", "card": "MyShop"}
FieldNotes
totalPriceΣ segment price of the chosen route
creditAmountAmount to take from the wallet. Use the same value as totalPrice
statusMust be success, otherwise complete refuses the basket
conversationIdYour order reference, max 50
paymentType, currency, bank, cardRequired 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": "…"}.

JSON
{"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:

  • creditAmount is 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, complete returns that same shipment again and charges nothing; set-routes and payment-complete on 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}

JSON
{"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.codeMeaning
CRCreated
PPPending payment
PAPaid
TRIn transport
CMCompleted
CLCancelled
Shipment shipmentStatus.codeMeaning
DOOrder created
PEPending
REReceived by the carrier
TRIn transit
DTOut for delivery
DEDelivered

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

JSON
{"data": {"balance": 42.5, "currency": "EUR"}}

#GET/api/customer/credit/transactions?page=1&limit=50

limit 1–200, default 50.

JSON
{"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.

  1. POST/api/customer/credit/topup with {"amount": 50} (net amount, 1–1000 €). Response: {"data": {"basketId": "…", "customerId": 273, "amount": 50, "tax": 12, "chargeTotal": 62, "currency": "EUR"}} (24% VAT is added).
  2. POST/api/auth/plugin-autologin with {"redirect": "/customer/<customerId>/credit/payment/<basketId>/init"} → {"data": {"token": "…"}} (valid for 2 minutes).
  3. 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.

JSON
{"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.

EndpointNotes
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.

EndpointBodyResponse
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.