# Integration overview Authenticate, choose an environment, and follow the complete Metfone Express shipping workflow. Company: Metfone Express API version: v1 Requested language: en Content language: en Source: https://mfexpress-partner-docs.zhst.app/mfexpress/en/overview ## Authentication and environments | Header | Requirement | Value / meaning | | --- | --- | --- | | X-Api-Token | Required | `${API_TOKEN}`: environment-specific token issued by TMS | | X-Client-Source | Required | `${PARTNER_CODE}`: partner code paired with the token | | Content-Type | With a JSON body | application/json | | Accept | Required by source | application/json | | Accept-Language | Optional | `vn`, `en`, `my`, `km`, `lo` | Contact TMS technical support for separate token/partner-code pairs for sandbox and production. Do not mix environments. The portal locale is `vi`, but the declared `Accept-Language` value is `vn`; several source examples use `vi`, so confirm provider behavior. Khmer, Lao, and Burmese have translated interfaces; missing document translations disclose the actual page language and fallback. ```bash export BASE_URL="https://dev-gw.metfone.com.kh/tms-partner/api/v1" # API_TOKEN and PARTNER_CODE must be supplied by your private environment. # Never put production secrets into examples or AI prompts. ``` ## Integration journey 1. [Load provinces](provinces), then [districts/villages](administrative-divisions); retain current IDs/codes. 2. [Find a post office](post-offices) if branch drop-off/collection is needed. 3. [Discover services](available-services) for the route and goods. 4. [Calculate prices](calculate-pricing) for selected service codes. 5. [Create the order](create-order), **resend moneyCollection**, compare cod, and save the response. 6. [Create a template](create-label), then [retrieve HTML](print-label) before expiry. 7. [Receive webhooks](webhook-status), [look up status](order-status) as needed; [cancel](cancel-order) only when eligible. ```mermaid flowchart LR A["Addresses"] --> B["Services"] A --> P["Optional post office"] B --> C["Pricing"] C --> D["Order and COD"] D --> E["Label template"] E --> F["Printable HTML"] D --> G["Journey webhooks"] D --> H["Status lookup"] ``` ## Request sequence ```mermaid sequenceDiagram participant P as Partner participant T as TMS Partner P->>T: GET address levels 1, 2, 3 T-->>P: IDs and codes P->>T: POST pricing/get-service T-->>P: Service codes and selection flags P->>T: POST pricing/get-all T-->>P: Pre-VAT fee breakdown P->>T: POST order with moneyCollection T-->>P: id, cod, full creation response P->>T: POST order/label/presigned T-->>P: securePath, exp P->>T: GET order/label/securePath T-->>P: text/html T->>P: POST status webhook P-->>T: HTTP 200 receipt acknowledgement ``` --- # List provinces Retrieve all level-1 administrative divisions for Cambodia or Laos. Company: Metfone Express API version: v1 Requested language: en Content language: en Source: https://mfexpress-partner-docs.zhst.app/mfexpress/en/provinces Endpoint: GET /administrativedivision/country/{country-code} ```http GET /tms-partner/api/v1/administrativedivision/country/{country-code} ``` ## Usage and important notes Use `KH` for Cambodia and `LA` for Laos. The result is an array, not a paginated object. Save both `id` and `code`; `code` is the next request’s `location-code`. `status=1` means active. Names may change with `Accept-Language`. ## Authentication headers | Header | Requirement | Value / meaning | | --- | --- | --- | | X-Api-Token | Required | `${API_TOKEN}`: environment-specific token issued by TMS | | X-Client-Source | Required | `${PARTNER_CODE}`: partner code paired with the token | | Content-Type | With a JSON body | application/json | | Accept | Required by source | application/json | | Accept-Language | Optional | `vn`, `en`, `my`, `km`, `lo` | ## Path and query parameters | Name | Location | Required | Description | | --- | --- | --- | --- | | country-code | path | Yes | ISO country code: KH Cambodia, LA Laos. | ## cURL example `BASE_URL` must include `/tms-partner/api/v1`. Load the token and partner code from your private environment; never paste secrets into documentation or AI prompts. ```bash curl --request GET "${BASE_URL}/administrativedivision/country/KH" \ --header "X-Api-Token: ${API_TOKEN}" \ --header "X-Client-Source: ${PARTNER_CODE}" \ --header "Accept-Language: en" \ --header "Accept: application/json" ``` ## Example response ```json [ { "id": 4025109, "level": 1, "status": 1, "code": "BAN", "countrycode": "KH", "name": "Banteay Meanchey", "type": "PROVINCE", "short_name": "Banteay Meanchey", "parent_code": "KH", "refs": [ { "sys": "VTP", "code": "BAN" }, { "sys": "POSTCODE", "code": "01" } ] } ] ``` ## Response fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `[].id` | number | See description | Internal location ID. | | `[].level` | number | See description | Administrative level: 1 province, 2 district, 3 village. | | `[].status` | number | See description | 1 means active. | | `[].code` | string | See description | Use the identifier returned by the API, not the illustrative value. | | `[].countrycode` | string | See description | Country code. | | `[].name` | string | See description | Localized location name. | | `[].type` | string | See description | PROVINCE, DISTRICT or VILLAGE. | | `[].short_name` | string | See description | Short name. | | `[].parent_code` | string | See description | Parent location code; generally country code at level 1. | | `[].refs` | array | See description | References to other systems. | | `[].refs[].sys` | string | See description | Reference system, e.g. VTP or POSTCODE. | | `[].refs[].code` | string | See description | Use the identifier returned by the API, not the illustrative value. | ## Error handling See [HTTP codes and source limitations](reference). HTTP success does not prove COD, fees, or cancellation are correct; inspect the documented business fields. Do not assume create-order retries are safe. --- # List districts and villages Resolve level-2 and level-3 locations from their parent codes. Company: Metfone Express API version: v1 Requested language: en Content language: en Source: https://mfexpress-partner-docs.zhst.app/mfexpress/en/administrative-divisions Endpoint: GET /administrativedivision/country/{country-code}/{level}/{location-code} ```http GET /tms-partner/api/v1/administrativedivision/country/{country-code}/{level}/{location-code} ``` ## Usage and important notes For districts, request `/administrativedivision/country/KH/2/BAN`. For villages, use the district code: `/administrativedivision/country/KH/3/0102`. A district example uses `Accept-Language: km`. Store returned IDs/codes for address components in pricing and order creation. All response fields are the same as the province endpoint. ## Authentication headers | Header | Requirement | Value / meaning | | --- | --- | --- | | X-Api-Token | Required | `${API_TOKEN}`: environment-specific token issued by TMS | | X-Client-Source | Required | `${PARTNER_CODE}`: partner code paired with the token | | Content-Type | With a JSON body | application/json | | Accept | Required by source | application/json | | Accept-Language | Optional | `vn`, `en`, `my`, `km`, `lo` | ## Path and query parameters | Name | Location | Required | Description | | --- | --- | --- | --- | | country-code | path | Yes | ISO country code: KH Cambodia, LA Laos. | | level | path | Yes | Requested child level: 2 districts or 3 villages. | | location-code | path | Yes | Parent code: province BAN for level 2, district 0102 for level 3. | | filter | query | No | Partial name match (LIKE) at every level of this endpoint. | ## cURL example `BASE_URL` must include `/tms-partner/api/v1`. Load the token and partner code from your private environment; never paste secrets into documentation or AI prompts. ```bash curl --request GET "${BASE_URL}/administrativedivision/country/KH/2/BAN" \ --header "X-Api-Token: ${API_TOKEN}" \ --header "X-Client-Source: ${PARTNER_CODE}" \ --header "Accept-Language: en" \ --header "Accept: application/json" ``` ## Example response ```json [ { "id": 4059452, "level": 2, "status": 1, "code": "0102", "countrycode": "KH", "name": "មង្គលបូរី", "type": "DISTRICT", "short_name": "មង្គលបូរី", "parent_code": "BAN", "refs": [ { "sys": "VTP", "code": "0102" } ] } ] ``` ## Response fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `[].id` | number | See description | Internal location ID. | | `[].level` | number | See description | Administrative level: 1 province, 2 district, 3 village. | | `[].status` | number | See description | 1 means active. | | `[].code` | string | See description | Use the identifier returned by the API, not the illustrative value. | | `[].countrycode` | string | See description | Country code. | | `[].name` | string | See description | Localized location name. | | `[].type` | string | See description | PROVINCE, DISTRICT or VILLAGE. | | `[].short_name` | string | See description | Short name. | | `[].parent_code` | string | See description | Parent location code; generally country code at level 1. | | `[].refs` | array | See description | References to other systems. | | `[].refs[].sys` | string | See description | Reference system, e.g. VTP or POSTCODE. | | `[].refs[].code` | string | See description | Use the identifier returned by the API, not the illustrative value. | ## Level-3 example ```http GET /tms-partner/api/v1/administrativedivision/country/KH/3/0102 ``` ```json [ { "id": 5025109, "level": 3, "status": 1, "code": "010201", "countrycode": "KH", "name": "បន្ទាយនាង", "type": "VILLAGE", "short_name": "បន្ទាយនាង", "parent_code": "0102", "refs": [ { "sys": "VTP", "code": "010201" } ] } ] ``` ## Error handling See [HTTP codes and source limitations](reference). HTTP success does not prove COD, fees, or cancellation are correct; inspect the documented business fields. Do not assume create-order retries are safe. --- # Find post offices Search branches, paginate results, and optionally sort by distance. Company: Metfone Express API version: v1 Requested language: en Content language: en Source: https://mfexpress-partner-docs.zhst.app/mfexpress/en/post-offices Endpoint: GET /department/posts ```http GET /tms-partner/api/v1/department/posts ``` ## Usage and important notes All query parameters are optional. `query` searches name, address, or phone; `codeLocation` filters by a province, district, or village. `departmentCode` targets one branch. Use `total` for pagination rather than the current `items.length`. Provide **both** `latDest` and `longDest` to get a meaningful `distance` in kilometers and nearest-first sorting. Without them, do not assume `distance` has a useful value. ## Authentication headers | Header | Requirement | Value / meaning | | --- | --- | --- | | X-Api-Token | Required | `${API_TOKEN}`: environment-specific token issued by TMS | | X-Client-Source | Required | `${PARTNER_CODE}`: partner code paired with the token | | Content-Type | With a JSON body | application/json | | Accept | Required by source | application/json | | Accept-Language | Optional | `vn`, `en`, `my`, `km`, `lo` | ## Path and query parameters | Name | Location | Required | Description | | --- | --- | --- | --- | | query | query | No | Search branch name/address/phone. | | codeLocation | query | No | Province/district/village code, e.g. BAN. | | departmentCode | query | No | Specific branch code, e.g. KOH01. | | latDest | query | No | Destination latitude; send with longDest to obtain distance and nearest-first ordering. | | longDest | query | No | Destination longitude used with latDest. | | limit | query | No | Page size, default 20. | | offset | query | No | Pagination offset, default 0. | ## cURL example `BASE_URL` must include `/tms-partner/api/v1`. Load the token and partner code from your private environment; never paste secrets into documentation or AI prompts. ```bash curl --request GET "${BASE_URL}/department/posts?query=Koh&departmentCode=KOH01&codeLocation=KOH&limit=20&offset=0&latDest=11.2&longDest=103.4" \ --header "X-Api-Token: ${API_TOKEN}" \ --header "X-Client-Source: ${PARTNER_CODE}" \ --header "Accept-Language: en" \ --header "Accept: application/json" ``` ## Example response ```json { "items": [ { "department_id": 900001, "type": "POST", "code": "EXAMPLE-POST", "phone": "${POST_OFFICE_PHONE}", "name": "Example post office", "department_code": "EXAMPLE-POST", "country_code": "KH", "province_code": "BAN", "district_code": "0102", "ward_code": "010201", "home": "Example branch", "address": "Example branch, Cambodia", "latitude": 11.2, "longitude": 103.4, "distance": 3.2, "address_component": { "components": [ { "name": "Example recipient, Street B", "type": "OTHER", "level": 999 }, { "id": "5025109", "code": "010201", "name": "បន្ទាយនាង", "type": "VILLAGE", "level": 3 }, { "id": "4059452", "code": "0102", "name": "មង្គលបូរី", "type": "DISTRICT", "level": 2 }, { "id": "4025109", "code": "BAN", "name": "Banteay Meanchey", "type": "PROVINCE", "level": 1 }, { "type": "NATIONAL", "name": "Cambodia", "code": "KH", "level": 0 } ] } } ], "total": 1 } ``` ## Response fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array | See description | Current page. | | `items[].department_id` | number | See description | Internal branch ID. | | `items[].type` | string | See description | Example: POST. | | `items[].code` | string | See description | Use the identifier returned by the API, not the illustrative value. | | `items[].phone` | string | See description | Branch phone. | | `items[].name` | string | See description | Branch name. | | `items[].department_code` | string | See description | Use the identifier returned by the API, not the illustrative value. | | `items[].country_code` | string | See description | Use the identifier returned by the API, not the illustrative value. | | `items[].province_code` | string | See description | Use the identifier returned by the API, not the illustrative value. | | `items[].district_code` | string | See description | Use the identifier returned by the API, not the illustrative value. | | `items[].ward_code` | string | See description | Use the identifier returned by the API, not the illustrative value. | | `items[].home` | string | See description | Local address text. | | `items[].address` | string | See description | Full branch address. | | `items[].latitude` | number | See description | Branch latitude. | | `items[].longitude` | number | See description | Branch longitude. | | `items[].distance` | number | See description | Distance in km, meaningful only when latDest/longDest are supplied. | | `items[].address_component` | object | See description | Branch address components. | | `items[].address_component.components` | array | See description | Use current administrative API IDs and codes. | | `items[].address_component.components[].id` | string | See description | Use the identifier returned by the API, not the illustrative value. | | `items[].address_component.components[].code` | string | See description | Use the identifier returned by the API, not the illustrative value. | | `items[].address_component.components[].name` | string | See description | Location name or street address. | | `items[].address_component.components[].type` | string | See description | NATIONAL, PROVINCE, DISTRICT, VILLAGE or OTHER according to level. | | `items[].address_component.components[].level` | number | See description | 0 country; 1 province; 2 district; 3 village; 999 street/detail. | | `total` | number | See description | Total matches for pagination. | ## Error handling See [HTTP codes and source limitations](reference). HTTP success does not prove COD, fees, or cancellation are correct; inspect the documented business fields. Do not assume create-order retries are safe. --- # Discover available services Find main, extra, and special service choices before calculating a quote. Company: Metfone Express API version: v1 Requested language: en Content language: en Source: https://mfexpress-partner-docs.zhst.app/mfexpress/en/available-services Endpoint: POST /pricing/get-service ```http POST /tms-partner/api/v1/pricing/get-service ``` ## Usage and important notes Call this before get-all and create-order. Supply a complete product and route. Missing `fromLocation`, `toLocation`, or `product` can produce HTTP 500 instead of a clean HTTP 400. Send weight in grams and dimensions in centimeters. `isDomestic` defaults to true. `customerCode` defaults to the token’s customer account. `partnerInfo` is an alternate discount identity and is normally unnecessary because `X-Client-Source` already identifies the partner. `orderService` changes the `isShow`, `isChosen`, and `isChangeable` flags of GTN/NTN/VTT; it does not change the price. Map `mainPrice.code` to order `service`, and selected `extraPrices[].code` to `vas[]`. Respect automatically included `mainPrice.extraService[]` and `specialService[]`. `isShow=false` marks an internal service, not a selectable requirement. `isCalculated=false` means displayed rather than priced. If `mainPrice.isCalculatedAutomatically=false`, use `manualPrice` when calling get-all. Use **beforeVatPrice**, not VAT-inclusive `price`, for order fee amounts. ## Authentication headers | Header | Requirement | Value / meaning | | --- | --- | --- | | X-Api-Token | Required | `${API_TOKEN}`: environment-specific token issued by TMS | | X-Client-Source | Required | `${PARTNER_CODE}`: partner code paired with the token | | Content-Type | With a JSON body | application/json | | Accept | Required by source | application/json | | Accept-Language | Optional | `vn`, `en`, `my`, `km`, `lo` | ## Request fields Recommended operational inputs; no fields have @NotNull validation. Missing locations/product may cause 500 rather than 400. | Field | Type | Required | Description | | --- | --- | --- | --- | | `product` | object | See description | Recommended product object. No @NotNull validation is documented for pricing. | | `product.weight` | number | See description | Recommended weight in grams. | | `product.type` | string | See description | Recommended goods type. Values: `GOODS`, `DOCUMENT`. | | `product.price` | number | See description | Declared goods value; currency is not specified for this request in the source. | | `product.moneyCollection` | number | See description | COD influences service selection only; it does not change the quoted amount or persist COD on an order. | | `product.length` | number | See description | Length in cm. | | `product.width` | number | See description | Width in cm. | | `product.height` | number | See description | Height in cm. | | `fromLocation` | array | See description | Recommended sender components, levels 0–3 and 999. | | `fromLocation[].id` | string | See description | Use the identifier returned by the API, not the illustrative value. | | `fromLocation[].code` | string | See description | Use the identifier returned by the API, not the illustrative value. | | `fromLocation[].name` | string | See description | Location name or street address. | | `fromLocation[].type` | string | See description | NATIONAL, PROVINCE, DISTRICT, VILLAGE or OTHER according to level. | | `fromLocation[].level` | number | See description | 0 country; 1 province; 2 district; 3 village; 999 street/detail. | | `toLocation` | array | See description | Recommended recipient components, levels 0–3. | | `toLocation[].id` | string | See description | Use the identifier returned by the API, not the illustrative value. | | `toLocation[].code` | string | See description | Use the identifier returned by the API, not the illustrative value. | | `toLocation[].name` | string | See description | Location name or street address. | | `toLocation[].type` | string | See description | NATIONAL, PROVINCE, DISTRICT, VILLAGE or OTHER according to level. | | `toLocation[].level` | number | See description | 0 country; 1 province; 2 district; 3 village; 999 street/detail. | | `isDomestic` | boolean | See description | Domestic=true, international=false. Defaults to true. Default: `true`. | | `customerCode` | string | See description | Internal customer code; defaults to the account associated with X-Api-Token. | | `partnerInfo` | object | See description | Optional discount identity; X-Client-Source normally suffices. Uses snake_case. | | `partnerInfo.id` | number | See description | Internal partner ID. | | `partnerInfo.partner_code` | string | See description | Partner code. | | `partnerInfo.partner_name` | string | See description | Partner name. | | `partnerInfo.email` | string | See description | Partner email. | | `partnerInfo.type` | string | See description | Partner type, e.g. PARTNER. | | `orderService` | object | See description | Service preferences; UNKNOW is the source spelling. | | `orderService.senderService` | string | See description | Changes display/default/lock flags of GTN/NTN/VTT, not price. Values: `GTCH`, `VTT`, `GTN`, `UNKNOW`. | | `orderService.receiverService` | string | See description | Recipient preference with the same flag-only effect. Values: `NTCH`, `NTN`, `VTT`, `UNKNOW`. | ## Example JSON body Save as `request.json`. Replace every `${…}` string and example ID with actual environment values before sending. cURL with `--data @request.json` **does not interpolate variables inside JSON files**. Identities are synthetic. ```json { "product": { "weight": 100, "type": "GOODS", "price": 50, "moneyCollection": 50, "length": 10, "width": 10, "height": 10 }, "fromLocation": [ { "name": "Example warehouse, Street A", "type": "OTHER", "level": 999 }, { "id": "5025963", "code": "120211", "name": "Voat Phnum", "type": "VILLAGE", "level": 3 }, { "id": "4059553", "code": "1202", "name": "Doun Penh", "type": "DISTRICT", "level": 2 }, { "id": "4025120", "code": "PNP", "name": "Phnom Penh", "type": "PROVINCE", "level": 1 }, { "type": "NATIONAL", "name": "Cambodia", "code": "KH", "level": 0 } ], "toLocation": [ { "name": "Example recipient, Street B", "type": "OTHER", "level": 999 }, { "id": "5025109", "code": "010201", "name": "បន្ទាយនាង", "type": "VILLAGE", "level": 3 }, { "id": "4059452", "code": "0102", "name": "មង្គលបូរី", "type": "DISTRICT", "level": 2 }, { "id": "4025109", "code": "BAN", "name": "Banteay Meanchey", "type": "PROVINCE", "level": 1 }, { "type": "NATIONAL", "name": "Cambodia", "code": "KH", "level": 0 } ], "isDomestic": true, "orderService": { "senderService": "GTN", "receiverService": "NTN" } } ``` ## cURL example `BASE_URL` must include `/tms-partner/api/v1`. Load the token and partner code from your private environment; never paste secrets into documentation or AI prompts. ```bash curl --request POST "${BASE_URL}/pricing/get-service" \ --header "X-Api-Token: ${API_TOKEN}" \ --header "X-Client-Source: ${PARTNER_CODE}" \ --header "Accept-Language: en" \ --header "Accept: application/json" \ --header "Content-Type: application/json" \ --data @request.json ``` ## Example response ```json [ { "totalPrice": 20000, "totalPriceAfterDiscount": 18000, "discount": 2000, "totalDiscountAmount": 2000, "discountType": "AMOUNT", "property": { "calculatedWeight": 100, "convertedWeight": 200, "perimeter": 30, "distance": null }, "mainPrice": { "code": "CCN", "name": "Express", "price": 8000, "priceAfterDiscount": 6000, "discount": 2000, "beforeVatPrice": 7273, "vatPrice": 727, "field": "KL", "calculatedWeight": 100, "extraService": [ "GTN" ], "specialService": [], "isCalculatedAutomatically": true }, "extraPrices": [ { "code": "GTN", "name": "Delivery to address", "price": 8000, "beforeVatPrice": 7273, "vatPrice": 727, "priceAfterDiscount": 8000, "discount": 0, "field": "CC", "isShow": true, "isChosen": false, "isChangeable": true, "isCalculated": true }, { "code": "NTN", "name": "Pickup at address", "price": 2000, "beforeVatPrice": 1818, "vatPrice": 182, "priceAfterDiscount": 2000, "discount": 0, "field": "CC", "isShow": true, "isChosen": false, "isChangeable": true, "isCalculated": true } ], "specialPrices": [], "leadTime": 0 } ] ``` ## Response fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `[].totalPrice` | number | See description | Total before the reported discount. | | `[].totalPriceAfterDiscount` | number | See description | Total after discount; difference is totalDiscountAmount. | | `[].property` | object | See description | Calculation inputs returned by pricing. | | `[].property.calculatedWeight` | number | See description | Actual weight used for pricing, grams. | | `[].property.convertedWeight` | number | See description | Volumetric weight, grams. | | `[].property.perimeter` | number | See description | Parcel perimeter. | | `[].property.distance` | number / null | See description | Distance when a distance-based service applies; otherwise null. | | `[].mainPrice` | object | See description | Fields depend on whether this is mainPrice or an extra/special price. | | `[].mainPrice.code` | string | See description | Service code; mainPrice.code becomes order.service; extraPrices[].code becomes order.vas[]. | | `[].mainPrice.name` | string | See description | Service name. | | `[].mainPrice.price` | number | See description | Listed service price; extra service list prices include VAT. | | `[].mainPrice.priceAfterDiscount` | number | See description | Service amount after discount. | | `[].mainPrice.discount` | number | See description | Service-specific discount amount. | | `[].mainPrice.beforeVatPrice` | number | See description | Pre-VAT fee to map to detailedFees.serviceFee[].fee. | | `[].mainPrice.vatPrice` | number | See description | VAT amount. | | `[].mainPrice.field` | string | See description | Calculation classification: KL/TH/CC/KG/KC/CV/CV_KL. | | `[].mainPrice.calculatedWeight` | number | See description | Weight used by the main service. | | `[].mainPrice.extraService` | array | See description | Automatically included extra services. | | `[].mainPrice.specialService` | array | See description | Automatically included special services. | | `[].mainPrice.isCalculatedAutomatically` | boolean | See description | true: automatic Drools calculation; false: provide manualPrice to get-all. | | `[].mainPrice.isShow` | boolean | See description | Extra service visible for selection; false means internal and need not be submitted. | | `[].mainPrice.isChosen` | boolean | See description | Extra service selected by default. | | `[].mainPrice.isChangeable` | boolean | See description | Whether the user may toggle the extra service; otherwise locked by orderService. | | `[].mainPrice.isCalculated` | boolean | See description | Whether the extra service is priced or only displayed. | | `[].extraPrices` | array | See description | Extra-service prices. | | `[].extraPrices[].code` | string | See description | Service code; mainPrice.code becomes order.service; extraPrices[].code becomes order.vas[]. | | `[].extraPrices[].name` | string | See description | Service name. | | `[].extraPrices[].price` | number | See description | Listed service price; extra service list prices include VAT. | | `[].extraPrices[].priceAfterDiscount` | number | See description | Service amount after discount. | | `[].extraPrices[].discount` | number | See description | Service-specific discount amount. | | `[].extraPrices[].beforeVatPrice` | number | See description | Pre-VAT fee to map to detailedFees.serviceFee[].fee. | | `[].extraPrices[].vatPrice` | number | See description | VAT amount. | | `[].extraPrices[].field` | string | See description | Calculation classification: KL/TH/CC/KG/KC/CV/CV_KL. | | `[].extraPrices[].calculatedWeight` | number | See description | Weight used by the main service. | | `[].extraPrices[].extraService` | array | See description | Automatically included extra services. | | `[].extraPrices[].specialService` | array | See description | Automatically included special services. | | `[].extraPrices[].isCalculatedAutomatically` | boolean | See description | true: automatic Drools calculation; false: provide manualPrice to get-all. | | `[].extraPrices[].isShow` | boolean | See description | Extra service visible for selection; false means internal and need not be submitted. | | `[].extraPrices[].isChosen` | boolean | See description | Extra service selected by default. | | `[].extraPrices[].isChangeable` | boolean | See description | Whether the user may toggle the extra service; otherwise locked by orderService. | | `[].extraPrices[].isCalculated` | boolean | See description | Whether the extra service is priced or only displayed. | | `[].specialPrices` | array | See description | Special-service prices. | | `[].specialPrices[].code` | string | See description | Service code; mainPrice.code becomes order.service; extraPrices[].code becomes order.vas[]. | | `[].specialPrices[].name` | string | See description | Service name. | | `[].specialPrices[].price` | number | See description | Listed service price; extra service list prices include VAT. | | `[].specialPrices[].priceAfterDiscount` | number | See description | Service amount after discount. | | `[].specialPrices[].discount` | number | See description | Service-specific discount amount. | | `[].specialPrices[].beforeVatPrice` | number | See description | Pre-VAT fee to map to detailedFees.serviceFee[].fee. | | `[].specialPrices[].vatPrice` | number | See description | VAT amount. | | `[].specialPrices[].field` | string | See description | Calculation classification: KL/TH/CC/KG/KC/CV/CV_KL. | | `[].specialPrices[].calculatedWeight` | number | See description | Weight used by the main service. | | `[].specialPrices[].extraService` | array | See description | Automatically included extra services. | | `[].specialPrices[].specialService` | array | See description | Automatically included special services. | | `[].specialPrices[].isCalculatedAutomatically` | boolean | See description | true: automatic Drools calculation; false: provide manualPrice to get-all. | | `[].specialPrices[].isShow` | boolean | See description | Extra service visible for selection; false means internal and need not be submitted. | | `[].specialPrices[].isChosen` | boolean | See description | Extra service selected by default. | | `[].specialPrices[].isChangeable` | boolean | See description | Whether the user may toggle the extra service; otherwise locked by orderService. | | `[].specialPrices[].isCalculated` | boolean | See description | Whether the extra service is priced or only displayed. | | `[].discount` | number | See description | Discount value. | | `[].totalDiscountAmount` | number | See description | Total discount amount. | | `[].discountType` | string | See description | AMOUNT: fixed amount; PERCENT: percentage. Values: `AMOUNT`, `PERCENT`. | | `[].leadTime` | number | See description | Lead-time value shown in the source; unit is unspecified. | ## Error handling See [HTTP codes and source limitations](reference). HTTP success does not prove COD, fees, or cancellation are correct; inspect the documented business fields. Do not assume create-order retries are safe. --- # Calculate shipping prices Calculate the VAT and discount breakdown for a known route and service. Company: Metfone Express API version: v1 Requested language: en Content language: en Source: https://mfexpress-partner-docs.zhst.app/mfexpress/en/calculate-pricing Endpoint: POST /pricing/get-all ```http POST /tms-partner/api/v1/pricing/get-all ``` ## Usage and important notes Call get-service first to obtain appropriate `mainService`, `extraService`, and `specialService`. Send the route and product to avoid missing-input runtime failures. `manualPrice` applies only when the chosen service is not calculated automatically; `customerCode` and `partnerInfo` follow the service-discovery rules. **COD does not change the quoted amount.** At this step and at get-service, `product.moneyCollection` only selects service rules for COD greater than zero versus zero in Drools. It does not change `totalPrice`, `mainPrice`, or `extraPrices`, and does not persist COD on an order. You must resend top-level `moneyCollection` in create-order. `totalPrice` is the final VAT-inclusive amount after discounts. `totalPriceBeforeDiscount` is before discounts. `vat` is a percentage; `vatPrice` is the tax amount; `beforeVatPrice` excludes tax. The response exposes `round`/`roundNum`, the applied rounding rule and precision. `manualPrice` echoes the manual input or is null. `discountType=AMOUNT` means a fixed amount, `PERCENT` means a percentage. ## Authentication headers | Header | Requirement | Value / meaning | | --- | --- | --- | | X-Api-Token | Required | `${API_TOKEN}`: environment-specific token issued by TMS | | X-Client-Source | Required | `${PARTNER_CODE}`: partner code paired with the token | | Content-Type | With a JSON body | application/json | | Accept | Required by source | application/json | | Accept-Language | Optional | `vn`, `en`, `my`, `km`, `lo` | ## Request fields Quote known services. Source table omits orderService but appendix explicitly references section 6; confirm its acceptance. | Field | Type | Required | Description | | --- | --- | --- | --- | | `product` | object | See description | Recommended product object. No @NotNull validation is documented for pricing. | | `product.weight` | number | See description | Recommended weight in grams. | | `product.type` | string | See description | Recommended goods type. Values: `GOODS`, `DOCUMENT`. | | `product.price` | number | See description | Declared goods value; currency is not specified for this request in the source. | | `product.moneyCollection` | number | See description | COD influences service selection only; it does not change the quoted amount or persist COD on an order. | | `product.length` | number | See description | Length in cm. | | `product.width` | number | See description | Width in cm. | | `product.height` | number | See description | Height in cm. | | `fromLocation` | array | See description | Recommended sender components, levels 0–3 and 999. | | `fromLocation[].id` | string | See description | Use the identifier returned by the API, not the illustrative value. | | `fromLocation[].code` | string | See description | Use the identifier returned by the API, not the illustrative value. | | `fromLocation[].name` | string | See description | Location name or street address. | | `fromLocation[].type` | string | See description | NATIONAL, PROVINCE, DISTRICT, VILLAGE or OTHER according to level. | | `fromLocation[].level` | number | See description | 0 country; 1 province; 2 district; 3 village; 999 street/detail. | | `toLocation` | array | See description | Recommended recipient components, levels 0–3. | | `toLocation[].id` | string | See description | Use the identifier returned by the API, not the illustrative value. | | `toLocation[].code` | string | See description | Use the identifier returned by the API, not the illustrative value. | | `toLocation[].name` | string | See description | Location name or street address. | | `toLocation[].type` | string | See description | NATIONAL, PROVINCE, DISTRICT, VILLAGE or OTHER according to level. | | `toLocation[].level` | number | See description | 0 country; 1 province; 2 district; 3 village; 999 street/detail. | | `isDomestic` | boolean | See description | Domestic=true, international=false. Defaults to true. Default: `true`. | | `customerCode` | string | See description | Internal customer code; defaults to the account associated with X-Api-Token. | | `partnerInfo` | object | See description | Optional discount identity; X-Client-Source normally suffices. Uses snake_case. | | `partnerInfo.id` | number | See description | Internal partner ID. | | `partnerInfo.partner_code` | string | See description | Partner code. | | `partnerInfo.partner_name` | string | See description | Partner name. | | `partnerInfo.email` | string | See description | Partner email. | | `partnerInfo.type` | string | See description | Partner type, e.g. PARTNER. | | `mainService` | string | See description | Recommended main service code from get-service, e.g. CCN. | | `extraService` | array | See description | Optional extra-service codes. | | `specialService` | array | See description | Optional special-service codes, usually empty. | | `manualPrice` | number | See description | Self-declared price when mainPrice.isCalculatedAutomatically=false; ignored for automatic services. | | `orderService` | object | See description | Service preferences; UNKNOW is the source spelling. | | `orderService.senderService` | string | See description | Changes display/default/lock flags of GTN/NTN/VTT, not price. Values: `GTCH`, `VTT`, `GTN`, `UNKNOW`. | | `orderService.receiverService` | string | See description | Recipient preference with the same flag-only effect. Values: `NTCH`, `NTN`, `VTT`, `UNKNOW`. | ## Example JSON body Save as `request.json`. Replace every `${…}` string and example ID with actual environment values before sending. cURL with `--data @request.json` **does not interpolate variables inside JSON files**. Identities are synthetic. ```json { "product": { "weight": 100, "type": "GOODS", "price": 50, "moneyCollection": 50, "length": 10, "width": 10, "height": 10 }, "fromLocation": [ { "name": "Example warehouse, Street A", "type": "OTHER", "level": 999 }, { "id": "5025963", "code": "120211", "name": "Voat Phnum", "type": "VILLAGE", "level": 3 }, { "id": "4059553", "code": "1202", "name": "Doun Penh", "type": "DISTRICT", "level": 2 }, { "id": "4025120", "code": "PNP", "name": "Phnom Penh", "type": "PROVINCE", "level": 1 }, { "type": "NATIONAL", "name": "Cambodia", "code": "KH", "level": 0 } ], "toLocation": [ { "name": "Example recipient, Street B", "type": "OTHER", "level": 999 }, { "id": "5025109", "code": "010201", "name": "បន្ទាយនាង", "type": "VILLAGE", "level": 3 }, { "id": "4059452", "code": "0102", "name": "មង្គលបូរី", "type": "DISTRICT", "level": 2 }, { "id": "4025109", "code": "BAN", "name": "Banteay Meanchey", "type": "PROVINCE", "level": 1 }, { "type": "NATIONAL", "name": "Cambodia", "code": "KH", "level": 0 } ], "isDomestic": true, "mainService": "CCN", "extraService": [], "specialService": [] } ``` ## cURL example `BASE_URL` must include `/tms-partner/api/v1`. Load the token and partner code from your private environment; never paste secrets into documentation or AI prompts. ```bash curl --request POST "${BASE_URL}/pricing/get-all" \ --header "X-Api-Token: ${API_TOKEN}" \ --header "X-Client-Source: ${PARTNER_CODE}" \ --header "Accept-Language: en" \ --header "Accept: application/json" \ --header "Content-Type: application/json" \ --data @request.json ``` ## Example response ```json { "totalPrice": 1.75, "totalPriceBeforeDiscount": 1.75, "vat": 10, "vatPrice": 0.16, "beforeVatPrice": 1.59, "manualPrice": null, "round": "NONE", "roundNum": 0, "property": { "calculatedWeight": 100, "convertedWeight": 200, "perimeter": 30, "distance": null }, "mainPrice": { "code": "CCN", "name": "Express", "price": 1.59, "priceAfterDiscount": 1.59, "discount": 0, "beforeVatPrice": 1.44, "vatPrice": 0.14, "field": "KL", "calculatedWeight": 100, "extraService": [], "specialService": [], "isCalculatedAutomatically": true }, "extraPrices": [], "specialPrices": [], "discount": 0, "totalDiscountAmount": 0, "discountType": "AMOUNT" } ``` ## Response fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `totalPrice` | number | See description | Final total after discounts, including VAT. | | `totalPriceBeforeDiscount` | number | See description | Total before discounts. | | `vat` | number | See description | VAT rate, percent. | | `vatPrice` | number | See description | Total VAT amount. | | `beforeVatPrice` | number | See description | Total excluding VAT. | | `manualPrice` | number / null | See description | Echoed manual price, null if omitted/not applicable. | | `round` | string | See description | Applied rounding rule; NONE appears in the example. Full enum not supplied. | | `roundNum` | number | See description | Rounding precision. | | `property` | object | See description | Calculation inputs returned by pricing. | | `property.calculatedWeight` | number | See description | Actual weight used for pricing, grams. | | `property.convertedWeight` | number | See description | Volumetric weight, grams. | | `property.perimeter` | number | See description | Parcel perimeter. | | `property.distance` | number / null | See description | Distance when a distance-based service applies; otherwise null. | | `mainPrice` | object | See description | Fields depend on whether this is mainPrice or an extra/special price. | | `mainPrice.code` | string | See description | Service code; mainPrice.code becomes order.service; extraPrices[].code becomes order.vas[]. | | `mainPrice.name` | string | See description | Service name. | | `mainPrice.price` | number | See description | Listed service price; extra service list prices include VAT. | | `mainPrice.priceAfterDiscount` | number | See description | Service amount after discount. | | `mainPrice.discount` | number | See description | Service-specific discount amount. | | `mainPrice.beforeVatPrice` | number | See description | Pre-VAT fee to map to detailedFees.serviceFee[].fee. | | `mainPrice.vatPrice` | number | See description | VAT amount. | | `mainPrice.field` | string | See description | Calculation classification: KL/TH/CC/KG/KC/CV/CV_KL. | | `mainPrice.calculatedWeight` | number | See description | Weight used by the main service. | | `mainPrice.extraService` | array | See description | Automatically included extra services. | | `mainPrice.specialService` | array | See description | Automatically included special services. | | `mainPrice.isCalculatedAutomatically` | boolean | See description | true: automatic Drools calculation; false: provide manualPrice to get-all. | | `mainPrice.isShow` | boolean | See description | Extra service visible for selection; false means internal and need not be submitted. | | `mainPrice.isChosen` | boolean | See description | Extra service selected by default. | | `mainPrice.isChangeable` | boolean | See description | Whether the user may toggle the extra service; otherwise locked by orderService. | | `mainPrice.isCalculated` | boolean | See description | Whether the extra service is priced or only displayed. | | `extraPrices` | array | See description | Extra-service prices. | | `extraPrices[].code` | string | See description | Service code; mainPrice.code becomes order.service; extraPrices[].code becomes order.vas[]. | | `extraPrices[].name` | string | See description | Service name. | | `extraPrices[].price` | number | See description | Listed service price; extra service list prices include VAT. | | `extraPrices[].priceAfterDiscount` | number | See description | Service amount after discount. | | `extraPrices[].discount` | number | See description | Service-specific discount amount. | | `extraPrices[].beforeVatPrice` | number | See description | Pre-VAT fee to map to detailedFees.serviceFee[].fee. | | `extraPrices[].vatPrice` | number | See description | VAT amount. | | `extraPrices[].field` | string | See description | Calculation classification: KL/TH/CC/KG/KC/CV/CV_KL. | | `extraPrices[].calculatedWeight` | number | See description | Weight used by the main service. | | `extraPrices[].extraService` | array | See description | Automatically included extra services. | | `extraPrices[].specialService` | array | See description | Automatically included special services. | | `extraPrices[].isCalculatedAutomatically` | boolean | See description | true: automatic Drools calculation; false: provide manualPrice to get-all. | | `extraPrices[].isShow` | boolean | See description | Extra service visible for selection; false means internal and need not be submitted. | | `extraPrices[].isChosen` | boolean | See description | Extra service selected by default. | | `extraPrices[].isChangeable` | boolean | See description | Whether the user may toggle the extra service; otherwise locked by orderService. | | `extraPrices[].isCalculated` | boolean | See description | Whether the extra service is priced or only displayed. | | `specialPrices` | array | See description | Special-service prices. | | `specialPrices[].code` | string | See description | Service code; mainPrice.code becomes order.service; extraPrices[].code becomes order.vas[]. | | `specialPrices[].name` | string | See description | Service name. | | `specialPrices[].price` | number | See description | Listed service price; extra service list prices include VAT. | | `specialPrices[].priceAfterDiscount` | number | See description | Service amount after discount. | | `specialPrices[].discount` | number | See description | Service-specific discount amount. | | `specialPrices[].beforeVatPrice` | number | See description | Pre-VAT fee to map to detailedFees.serviceFee[].fee. | | `specialPrices[].vatPrice` | number | See description | VAT amount. | | `specialPrices[].field` | string | See description | Calculation classification: KL/TH/CC/KG/KC/CV/CV_KL. | | `specialPrices[].calculatedWeight` | number | See description | Weight used by the main service. | | `specialPrices[].extraService` | array | See description | Automatically included extra services. | | `specialPrices[].specialService` | array | See description | Automatically included special services. | | `specialPrices[].isCalculatedAutomatically` | boolean | See description | true: automatic Drools calculation; false: provide manualPrice to get-all. | | `specialPrices[].isShow` | boolean | See description | Extra service visible for selection; false means internal and need not be submitted. | | `specialPrices[].isChosen` | boolean | See description | Extra service selected by default. | | `specialPrices[].isChangeable` | boolean | See description | Whether the user may toggle the extra service; otherwise locked by orderService. | | `specialPrices[].isCalculated` | boolean | See description | Whether the extra service is priced or only displayed. | | `discount` | number | See description | Discount value. | | `totalDiscountAmount` | number | See description | Total discount amount. | | `discountType` | string | See description | AMOUNT: fixed amount; PERCENT: percentage. Values: `AMOUNT`, `PERCENT`. | ## Error handling See [HTTP codes and source limitations](reference). HTTP success does not prove COD, fees, or cancellation are correct; inspect the documented business fields. Do not assume create-order retries are safe. --- # Create an order Create a shipment, explicitly persist COD, and retain the complete response. Company: Metfone Express API version: v1 Requested language: en Content language: en Source: https://mfexpress-partner-docs.zhst.app/mfexpress/en/create-order Endpoint: POST /order ```http POST /tms-partner/api/v1/order ``` ## Usage and important notes Send top-level `moneyCollection` on this request. Pricing and creation are independent. If it is absent or null, the recorded response `cod` becomes 0 even if the quote included COD. Negative COD produces `INVALID_PRICE`. Do not send the obsolete request field `cod`: it is not read. Compare the returned `cod` against your intended collection amount. Provide sender and recipient names, international-format phones, and addresses. The source describes `seller` as the fallback when shipper is empty, `buyer` when consignee is empty, and `returnContact` defaulting to shipper. Each parcel needs weight/weightUnit, dimensions, type, and at least one item with a name and positive weight. `measurementUnit` defaults to the literal `CENTIMET`. Only `payerType=CONSIGNEE` or `SELLER` is accepted; other values produce `PAYER_INVALID`. `vas` containing GTN/NTN/VTT may override the submitted `receiveType`. `deliveryPostcode` is required unless `vas` contains GTN or VTT; with GTN it is forced to null. Source names and sender/receiver service labels are retained rather than reinterpreted. `txId` supports partner-side reconciliation/deduplication; the source does **not** establish server-enforced idempotency, a deduplication window, or safe repeat-create behavior. Retain the initial response and reconcile an ambiguous timeout before submitting another create. Do not assume a repeated txId prevents a duplicate shipment. **Response:** save `id` for label creation, status lookup, and cancellation. Creation uses textual `status=CONFIRMED` and `statusName`; status lookup/webhooks use numeric codes. `totalFees` includes VAT after discounts and `totalTax` is VAT. `estDeliveryTime.date` is DD/MM/YYYY. `partnerInfo` uses snake_case keys inside an otherwise camelCase order response. Addresses are reduced to `components` and `formattedAddress`; no address-level id/code/geometry or component typeName/typeLabel is echoed. ## Authentication headers | Header | Requirement | Value / meaning | | --- | --- | --- | | X-Api-Token | Required | `${API_TOKEN}`: environment-specific token issued by TMS | | X-Client-Source | Required | `${PARTNER_CODE}`: partner code paired with the token | | Content-Type | With a JSON body | application/json | | Accept | Required by source | application/json | | Accept-Language | Optional | `vn`, `en`, `my`, `km`, `lo` | ## Request fields Provide sender/recipient or documented seller/buyer fallback. Requiredness describes source business requirements, not guaranteed validation responses. | Field | Type | Required | Description | | --- | --- | --- | --- | | `receiveType` | string | See description | AT_HOME or AT_STORE; GTN/NTN/VTT in vas can cause recalculation and override. Values: `AT_HOME`, `AT_STORE`. | | `shipper` | object | See description | ContactInfo. Sender and recipient need name, phone and address, subject to seller/buyer fallback. | | `shipper.name` | string | See description | Contact name. Examples use synthetic people. | | `shipper.phone` | string | See description | International format, e.g. Cambodia +855; replace the environment placeholder. | | `shipper.type` | string | See description | Examples: PARTNER_WAREHOUSE, PERSON, HOME, OFFICE; complete enum not supplied. | | `shipper.postOffice` | string | See description | Post-office code for drop-off at a branch. | | `shipper.address` | object | See description | Address supplied by the partner. | | `shipper.address.components` | array | When parent is present | Use current administrative API IDs and codes. | | `shipper.address.components[].id` | string | See description | Use the identifier returned by the API, not the illustrative value. | | `shipper.address.components[].code` | string | See description | Use the identifier returned by the API, not the illustrative value. | | `shipper.address.components[].name` | string | See description | Location name or street address. | | `shipper.address.components[].type` | string | See description | NATIONAL, PROVINCE, DISTRICT, VILLAGE or OTHER according to level. | | `shipper.address.components[].level` | number | See description | 0 country; 1 province; 2 district; 3 village; 999 street/detail. | | `consignee` | object | See description | ContactInfo. Sender and recipient need name, phone and address, subject to seller/buyer fallback. | | `consignee.name` | string | See description | Contact name. Examples use synthetic people. | | `consignee.phone` | string | See description | International format, e.g. Cambodia +855; replace the environment placeholder. | | `consignee.type` | string | See description | Examples: PARTNER_WAREHOUSE, PERSON, HOME, OFFICE; complete enum not supplied. | | `consignee.postOffice` | string | See description | Post-office code for drop-off at a branch. | | `consignee.address` | object | See description | Address supplied by the partner. | | `consignee.address.components` | array | When parent is present | Use current administrative API IDs and codes. | | `consignee.address.components[].id` | string | See description | Use the identifier returned by the API, not the illustrative value. | | `consignee.address.components[].code` | string | See description | Use the identifier returned by the API, not the illustrative value. | | `consignee.address.components[].name` | string | See description | Location name or street address. | | `consignee.address.components[].type` | string | See description | NATIONAL, PROVINCE, DISTRICT, VILLAGE or OTHER according to level. | | `consignee.address.components[].level` | number | See description | 0 country; 1 province; 2 district; 3 village; 999 street/detail. | | `seller` | object | See description | Optional seller, used if shipper is empty. | | `seller.name` | string | See description | Contact name. Examples use synthetic people. | | `seller.phone` | string | See description | International format, e.g. Cambodia +855; replace the environment placeholder. | | `seller.type` | string | See description | Examples: PARTNER_WAREHOUSE, PERSON, HOME, OFFICE; complete enum not supplied. | | `seller.postOffice` | string | See description | Post-office code for drop-off at a branch. | | `seller.address` | object | See description | Address supplied by the partner. | | `seller.address.components` | array | When parent is present | Use current administrative API IDs and codes. | | `seller.address.components[].id` | string | See description | Use the identifier returned by the API, not the illustrative value. | | `seller.address.components[].code` | string | See description | Use the identifier returned by the API, not the illustrative value. | | `seller.address.components[].name` | string | See description | Location name or street address. | | `seller.address.components[].type` | string | See description | NATIONAL, PROVINCE, DISTRICT, VILLAGE or OTHER according to level. | | `seller.address.components[].level` | number | See description | 0 country; 1 province; 2 district; 3 village; 999 street/detail. | | `buyer` | object | See description | Optional buyer, used if consignee is empty. | | `buyer.name` | string | See description | Contact name. Examples use synthetic people. | | `buyer.phone` | string | See description | International format, e.g. Cambodia +855; replace the environment placeholder. | | `buyer.type` | string | See description | Examples: PARTNER_WAREHOUSE, PERSON, HOME, OFFICE; complete enum not supplied. | | `buyer.postOffice` | string | See description | Post-office code for drop-off at a branch. | | `buyer.address` | object | See description | Address supplied by the partner. | | `buyer.address.components` | array | When parent is present | Use current administrative API IDs and codes. | | `buyer.address.components[].id` | string | See description | Use the identifier returned by the API, not the illustrative value. | | `buyer.address.components[].code` | string | See description | Use the identifier returned by the API, not the illustrative value. | | `buyer.address.components[].name` | string | See description | Location name or street address. | | `buyer.address.components[].type` | string | See description | NATIONAL, PROVINCE, DISTRICT, VILLAGE or OTHER according to level. | | `buyer.address.components[].level` | number | See description | 0 country; 1 province; 2 district; 3 village; 999 street/detail. | | `returnContact` | object | See description | Return contact; defaults to shipper. | | `returnContact.name` | string | See description | Contact name. Examples use synthetic people. | | `returnContact.phone` | string | See description | International format, e.g. Cambodia +855; replace the environment placeholder. | | `returnContact.type` | string | See description | Examples: PARTNER_WAREHOUSE, PERSON, HOME, OFFICE; complete enum not supplied. | | `returnContact.postOffice` | string | See description | Post-office code for drop-off at a branch. | | `returnContact.address` | object | See description | Address supplied by the partner. | | `returnContact.address.components` | array | When parent is present | Use current administrative API IDs and codes. | | `returnContact.address.components[].id` | string | See description | Use the identifier returned by the API, not the illustrative value. | | `returnContact.address.components[].code` | string | See description | Use the identifier returned by the API, not the illustrative value. | | `returnContact.address.components[].name` | string | See description | Location name or street address. | | `returnContact.address.components[].type` | string | See description | NATIONAL, PROVINCE, DISTRICT, VILLAGE or OTHER according to level. | | `returnContact.address.components[].level` | number | See description | 0 country; 1 province; 2 district; 3 village; 999 street/detail. | | `parcel` | object | Yes | Parcel dimensions and goods. | | `parcel.weight` | number | Yes | Parcel weight. | | `parcel.weightUnit` | string | Yes | Weight unit documented as GRAM. | | `parcel.length` | number | Yes | Length, cm. | | `parcel.width` | number | Yes | Width, cm. | | `parcel.height` | number | Yes | Height, cm. | | `parcel.measurementUnit` | string | See description | Defaults to CENTIMET (source spelling). Default: `CENTIMET`. | | `parcel.type` | string | Yes | Parcel type. Values: `GOODS`, `DOCUMENT`. | | `parcel.items` | array | Yes | At least one item. | | `parcel.items[].name` | string | When parent is present | Item name. | | `parcel.items[].type` | string | See description | Item type shown in the example. | | `parcel.items[].value` | number | See description | Declared item value. | | `parcel.items[].weight` | number | When parent is present | Item weight; must be greater than zero. | | `parcel.items[].quantity` | number | See description | Item quantity. | | `payerType` | string | Yes | Only CONSIGNEE and SELLER accepted; otherwise PAYER_INVALID. Values: `CONSIGNEE`, `SELLER`. | | `service` | string | Yes | Main service code from get-service. | | `vas` | array | See description | Extra service codes selected from available services. | | `moneyCollection` | number | See description | COD must be resent here. Omitted/null becomes cod=0. Negative values produce INVALID_PRICE. | | `detailedFees` | object | See description | Use the results returned by get-all. | | `detailedFees.serviceFee` | array | See description | Recommended pricing details. | | `detailedFees.serviceFee[].service` | string | See description | Service code from pricing. | | `detailedFees.serviceFee[].name` | string | See description | Service name shown in the example. | | `detailedFees.serviceFee[].fee` | number | See description | Use main/extra beforeVatPrice, not VAT-inclusive price. | | `detailedFees.serviceFee[].surchargeType` | string | See description | Only valid values are retained, but the source does not list those values. Ask TMS. | | `detailedFees.serviceFee[].type` | string | See description | Source example says VAS, conflicting with the prose surchargeType field. Not a confirmed substitute. | | `voucher` | string | See description | Voucher code. | | `reference` | string | See description | Partner internal reference. | | `pickupAppointment` | object | See description | Optional appointment. Confirm day/time encoding before use. | | `pickupAppointment.dayOfWeek` | array | See description | Days of week; item encoding not specified by the source. | | `pickupAppointment.hours` | array | See description | Time slots; item encoding not specified by the source. | | `pickupAppointment.note` | string | See description | Appointment note. | | `deliveryAppointment` | object | See description | Optional appointment. Confirm day/time encoding before use. | | `deliveryAppointment.dayOfWeek` | array | See description | Days of week; item encoding not specified by the source. | | `deliveryAppointment.hours` | array | See description | Time slots; item encoding not specified by the source. | | `deliveryAppointment.note` | string | See description | Appointment note. | | `note` | string | See description | Order note. | | `invoice` | string | See description | Invoice requested from sender or receiver. Values: `NONE`, `BY_SENDER`, `BY_RECEIVER`. Default: `NONE`. | | `orderScope` | string | See description | Order scope. Values: `DOMESTIC`, `INTERNATIONAL`. Default: `DOMESTIC`. | | `deliveryPostcode` | string | See description | Required unless vas contains GTN or VTT. GTN forces this field to null. | | `txId` | string | See description | Partner transaction ID for reconciliation/client-side deduplication. Server idempotency is NOT established. | ## Example JSON body Save as `request.json`. Replace every `${…}` string and example ID with actual environment values before sending. cURL with `--data @request.json` **does not interpolate variables inside JSON files**. Identities are synthetic. ```json { "receiveType": "AT_HOME", "shipper": { "name": "Example Warehouse", "phone": "${SHIPPER_PHONE}", "type": "PARTNER_WAREHOUSE", "address": { "components": [ { "name": "Example warehouse, Street A", "type": "OTHER", "level": 999 }, { "id": "5025963", "code": "120211", "name": "Voat Phnum", "type": "VILLAGE", "level": 3 }, { "id": "4059553", "code": "1202", "name": "Doun Penh", "type": "DISTRICT", "level": 2 }, { "id": "4025120", "code": "PNP", "name": "Phnom Penh", "type": "PROVINCE", "level": 1 }, { "type": "NATIONAL", "name": "Cambodia", "code": "KH", "level": 0 } ] } }, "consignee": { "name": "Example Recipient", "phone": "${CONSIGNEE_PHONE}", "address": { "components": [ { "name": "Example recipient, Street B", "type": "OTHER", "level": 999 }, { "id": "5025109", "code": "010201", "name": "បន្ទាយនាង", "type": "VILLAGE", "level": 3 }, { "id": "4059452", "code": "0102", "name": "មង្គលបូរី", "type": "DISTRICT", "level": 2 }, { "id": "4025109", "code": "BAN", "name": "Banteay Meanchey", "type": "PROVINCE", "level": 1 }, { "type": "NATIONAL", "name": "Cambodia", "code": "KH", "level": 0 } ] } }, "parcel": { "weight": 100, "weightUnit": "GRAM", "length": 10, "width": 10, "height": 10, "measurementUnit": "CENTIMET", "type": "GOODS", "items": [ { "name": "Example product", "type": "GOODS", "value": 50, "weight": 10, "quantity": 8 } ] }, "payerType": "CONSIGNEE", "service": "CCN", "vas": [ "NTN", "VTT" ], "moneyCollection": 50, "invoice": "NONE", "reference": "EXAMPLE-REFERENCE-001", "txId": "00000000-0000-4000-8000-000000000001", "note": "" } ``` ## cURL example `BASE_URL` must include `/tms-partner/api/v1`. Load the token and partner code from your private environment; never paste secrets into documentation or AI prompts. ```bash curl --request POST "${BASE_URL}/order" \ --header "X-Api-Token: ${API_TOKEN}" \ --header "X-Client-Source: ${PARTNER_CODE}" \ --header "Accept-Language: en" \ --header "Accept: application/json" \ --header "Content-Type: application/json" \ --data @request.json ``` ## Example response ```json { "id": "EXAMPLE-ORDER-001", "status": "CONFIRMED", "statusName": "Awaiting acceptance", "service": "CCN", "serviceName": "Express", "orderScope": "DOMESTIC", "shipper": { "name": "Example Warehouse", "address": { "components": [ { "name": "Example warehouse, Street A", "type": "OTHER", "level": 999 }, { "id": "5025963", "code": "120211", "name": "Voat Phnum", "type": "VILLAGE", "level": 3 }, { "id": "4059553", "code": "1202", "name": "Doun Penh", "type": "DISTRICT", "level": 2 }, { "id": "4025120", "code": "PNP", "name": "Phnom Penh", "type": "PROVINCE", "level": 1 }, { "type": "NATIONAL", "name": "Cambodia", "code": "KH", "level": 0 } ], "formattedAddress": "Example warehouse, Phnom Penh, Cambodia" } }, "consignee": { "name": "Example Recipient", "phone": "${CONSIGNEE_PHONE}", "address": { "components": [ { "name": "Example recipient, Street B", "type": "OTHER", "level": 999 }, { "id": "5025109", "code": "010201", "name": "បន្ទាយនាង", "type": "VILLAGE", "level": 3 }, { "id": "4059452", "code": "0102", "name": "មង្គលបូរី", "type": "DISTRICT", "level": 2 }, { "id": "4025109", "code": "BAN", "name": "Banteay Meanchey", "type": "PROVINCE", "level": 1 }, { "type": "NATIONAL", "name": "Cambodia", "code": "KH", "level": 0 } ], "formattedAddress": "Example recipient, Banteay Meanchey, Cambodia" } }, "parcel": { "weight": 100, "weightUnit": "GRAM", "length": 10, "width": 10, "height": 10, "measurementUnit": "CENTIMET", "type": "GOODS", "items": [ { "name": "Example product", "type": "GOODS", "value": 50, "weight": 10, "quantity": 8 } ] }, "cod": 50, "totalFees": 1.75, "totalTax": 0.16, "originalPrice": 1.59, "detailedFees": { "currency": "USD", "serviceFee": [ { "service": "CCN", "fee": 1.44, "feeAfterVat": 1.59, "vatPrice": 0.14 } ] }, "payerType": "CONSIGNEE", "vas": [ "NTN", "VTT" ], "estDeliveryTime": { "days": 1.5, "date": "15/10/2026" }, "createdAt": "2026-10-14T09:00:00+00:00", "txId": "00000000-0000-4000-8000-000000000001", "invoice": "NONE", "receiveType": "AT_HOME", "partnerInfo": { "id": 900001, "partner_code": "EXAMPLE-PARTNER", "partner_name": "Example Partner", "type": "PARTNER" } } ``` ## Response fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | See description | Use the identifier returned by the API, not the illustrative value. | | `status` | string | See description | Creation example uses CONFIRMED, unlike numeric lookup/webhook status. | | `statusName` | string | See description | Creation status label: awaiting acceptance. | | `service` | string | See description | Use the identifier returned by the API, not the illustrative value. | | `serviceName` | string | See description | Main service name. | | `orderScope` | string | See description | Order scope. | | `shipper` | object | See description | Sender snapshot. | | `shipper.name` | string | See description | Sender name. | | `shipper.phone` | string | See description | Sender phone when returned. | | `shipper.address` | object | See description | Simplified response address; components do not echo typeName/typeLabel. | | `shipper.address.components` | array | See description | Use current administrative API IDs and codes. | | `shipper.address.components[].id` | string | See description | Use the identifier returned by the API, not the illustrative value. | | `shipper.address.components[].code` | string | See description | Use the identifier returned by the API, not the illustrative value. | | `shipper.address.components[].name` | string | See description | Location name or street address. | | `shipper.address.components[].type` | string | See description | NATIONAL, PROVINCE, DISTRICT, VILLAGE or OTHER according to level. | | `shipper.address.components[].level` | number | See description | 0 country; 1 province; 2 district; 3 village; 999 street/detail. | | `shipper.address.formattedAddress` | string | See description | Formatted address; no address-level id, code or geometry. | | `consignee` | object | See description | Recipient snapshot. | | `consignee.name` | string | See description | Recipient name. | | `consignee.phone` | string | See description | Recipient phone. | | `consignee.address` | object | See description | Simplified response address; components do not echo typeName/typeLabel. | | `consignee.address.components` | array | See description | Use current administrative API IDs and codes. | | `consignee.address.components[].id` | string | See description | Use the identifier returned by the API, not the illustrative value. | | `consignee.address.components[].code` | string | See description | Use the identifier returned by the API, not the illustrative value. | | `consignee.address.components[].name` | string | See description | Location name or street address. | | `consignee.address.components[].type` | string | See description | NATIONAL, PROVINCE, DISTRICT, VILLAGE or OTHER according to level. | | `consignee.address.components[].level` | number | See description | 0 country; 1 province; 2 district; 3 village; 999 street/detail. | | `consignee.address.formattedAddress` | string | See description | Formatted address; no address-level id, code or geometry. | | `parcel` | object | See description | Returned parcel may be partial; request requiredness is not a response guarantee. | | `parcel.weight` | number | See description | Parcel weight. | | `parcel.weightUnit` | string | See description | Weight unit documented as GRAM. | | `parcel.length` | number | See description | Length, cm. | | `parcel.width` | number | See description | Width, cm. | | `parcel.height` | number | See description | Height, cm. | | `parcel.measurementUnit` | string | See description | Defaults to CENTIMET (source spelling). Default: `CENTIMET`. | | `parcel.type` | string | See description | Parcel type. Values: `GOODS`, `DOCUMENT`. | | `parcel.items` | array | See description | Returned goods items. | | `parcel.items[].name` | string | See description | Item name. | | `parcel.items[].type` | string | See description | Item type shown in the example. | | `parcel.items[].value` | number | See description | Declared item value. | | `parcel.items[].weight` | number | See description | Item weight; must be greater than zero. | | `parcel.items[].quantity` | number | See description | Item quantity. | | `cod` | number | See description | Persisted COD; compare with request.moneyCollection. | | `totalFees` | number | See description | Total fees after discounts including VAT. | | `totalTax` | number | See description | Total VAT. | | `originalPrice` | number | See description | Original price shown in the source example; calculation not specified. | | `detailedFees` | object | See description | Recorded detailed fees. | | `detailedFees.currency` | string | See description | Example uses USD; do not assume every pricing response uses this currency. | | `detailedFees.serviceFee` | array | See description | Recorded fees. | | `detailedFees.serviceFee[].service` | string | See description | Use the identifier returned by the API, not the illustrative value. | | `detailedFees.serviceFee[].fee` | number | See description | Pre-VAT fee. | | `detailedFees.serviceFee[].feeAfterVat` | number | See description | Fee after VAT. | | `detailedFees.serviceFee[].vatPrice` | number | See description | VAT amount. | | `payerType` | string | See description | Payer. | | `vas` | array | See description | Extra services. | | `estDeliveryTime` | object | See description | Delivery estimate. | | `estDeliveryTime.days` | number | See description | Estimated day count. | | `estDeliveryTime.date` | string | See description | Estimated delivery date, DD/MM/YYYY. | | `createdAt` | string | See description | Creation timestamp. | | `txId` | string | See description | Transaction identifier derived from supplied txId or generated by the system; no server deduplication guarantee. | | `invoice` | string | See description | Invoice mode. | | `receiveType` | string | See description | Resolved receive type. | | `partnerInfo` | object | See description | Optional discount identity; X-Client-Source normally suffices. Uses snake_case. | | `partnerInfo.id` | number | See description | Internal partner ID. | | `partnerInfo.partner_code` | string | See description | Partner code. | | `partnerInfo.partner_name` | string | See description | Partner name. | | `partnerInfo.email` | string | See description | Partner email. | | `partnerInfo.type` | string | See description | Partner type, e.g. PARTNER. | ## Address levels | Level | type | Order requirement | | --- | --- | --- | | 0 | NATIONAL | Optional; code KH, name Cambodia | | 1 | PROVINCE | Required; id/code from provinces | | 2 | DISTRICT | Required; id/code from level 2 | | 3 | VILLAGE | Required; id/code from level 3 | | 999 | OTHER | Optional; house number, street, building | ## COD persistence diagram ```mermaid flowchart TD A["product.moneyCollection in pricing"] --> B["Select COD service branch only"] B --> C["Create a separate order request"] C --> D{"Top-level moneyCollection supplied?"} D -->|Yes| E["Persist and compare response.cod"] D -->|Missing or null| F["Order records cod = 0"] ``` ## Error handling See [HTTP codes and source limitations](reference). HTTP success does not prove COD, fees, or cancellation are correct; inspect the documented business fields. Do not assume create-order retries are safe. --- # Create a label template Request a temporary secure path for printing one or more orders. Company: Metfone Express API version: v1 Requested language: en Content language: en Source: https://mfexpress-partner-docs.zhst.app/mfexpress/en/create-label Endpoint: POST /order/label/presigned ```http POST /tms-partner/api/v1/order/label/presigned ``` ## Usage and important notes Use order IDs returned by creation. Both label endpoints use the **same TMS Partner base URL** as every other operation. Always provide `labelId` and `orderIds`, even though the current code lacks required-field validation and missing values can fail at runtime instead of returning a clear 400. `setting.maskContact`, if omitted, the system configuration applies. `sellerAsShipper=true` displays the seller rather than the warehouse. `securePath` is the next endpoint’s `source-path`; `exp` is a Unix millisecond timestamp. Fetch and print before it expires. ## Authentication headers | Header | Requirement | Value / meaning | | --- | --- | --- | | X-Api-Token | Required | `${API_TOKEN}`: environment-specific token issued by TMS | | X-Client-Source | Required | `${PARTNER_CODE}`: partner code paired with the token | | Content-Type | With a JSON body | application/json | | Accept | Required by source | application/json | | Accept-Language | Optional | `vn`, `en`, `my`, `km`, `lo` | ## Request fields Always supply labelId and orderIds. Current code does not enforce requiredness and omissions can cause runtime errors. | Field | Type | Required | Description | | --- | --- | --- | --- | | `labelId` | string | Yes | Print format examples: a5, a6, a7; full enum is not supplied. | | `orderIds` | array | Yes | IDs returned by create-order. | | `setting` | object | See description | Optional print settings. | | `setting.copy` | number | See description | Copies per order. | | `setting.hideFees` | boolean | See description | Hide fees on the label. | | `setting.sellerAsShipper` | boolean | See description | Show seller name instead of warehouse. | | `setting.maskContact` | boolean | See description | Partially hide phone/contact details; defaults to system configuration. | ## Example JSON body Save as `request.json`. Replace every `${…}` string and example ID with actual environment values before sending. cURL with `--data @request.json` **does not interpolate variables inside JSON files**. Identities are synthetic. ```json { "labelId": "a7", "orderIds": [ "EXAMPLE-ORDER-001" ], "setting": { "copy": 1, "hideFees": false, "sellerAsShipper": true, "maskContact": false } } ``` ## cURL example `BASE_URL` must include `/tms-partner/api/v1`. Load the token and partner code from your private environment; never paste secrets into documentation or AI prompts. ```bash curl --request POST "${BASE_URL}/order/label/presigned" \ --header "X-Api-Token: ${API_TOKEN}" \ --header "X-Client-Source: ${PARTNER_CODE}" \ --header "Accept-Language: en" \ --header "Accept: application/json" \ --header "Content-Type: application/json" \ --data @request.json ``` ## Example response ```json { "securePath": "00000000-0000-4000-8000-000000000002", "exp": 1791972000000 } ``` ## Response fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `securePath` | string | See description | Temporary secure token used as source-path. | | `exp` | number | See description | Expiry as Unix timestamp in milliseconds; fetch before expiry. | ## Error handling See [HTTP codes and source limitations](reference). HTTP success does not prove COD, fees, or cancellation are correct; inspect the documented business fields. Do not assume create-order retries are safe. --- # Retrieve label HTML Download the HTML print page before the secure path expires. Company: Metfone Express API version: v1 Requested language: en Content language: en Source: https://mfexpress-partner-docs.zhst.app/mfexpress/en/print-label Endpoint: GET /order/label/{source-path} ```http GET /tms-partner/api/v1/order/label/{source-path} ``` ## Usage and important notes **Always HTML, not PDF.** The response has `Content-Type: text/html`; the source explicitly says no binary PDF is generated and no download `Content-Disposition` is set. The HTML format follows the prior `labelId`. Open it in a browser or an appropriately isolated iframe, print from the browser, or perform HTML-to-PDF conversion in your own system if a PDF is needed. Treat returned HTML as external content, not trusted markup for your application. Use `Accept: text/html` and both authentication headers. The presigned path still has an expiry. Use the same TMS Partner server. ## Authentication headers | Header | Requirement | Value / meaning | | --- | --- | --- | | X-Api-Token | Required | `${API_TOKEN}`: environment-specific token issued by TMS | | X-Client-Source | Required | `${PARTNER_CODE}`: partner code paired with the token | | Content-Type | With a JSON body | application/json | | Accept | Required by source | text/html | | Accept-Language | Optional | `vn`, `en`, `my`, `km`, `lo` | ## Path and query parameters | Name | Location | Required | Description | | --- | --- | --- | --- | | source-path | path | Yes | securePath from create-label; use before exp. | ## cURL example `BASE_URL` must include `/tms-partner/api/v1`. Load the token and partner code from your private environment; never paste secrets into documentation or AI prompts. ```bash curl --request GET "${BASE_URL}/order/label/${SECURE_PATH}" \ --header "X-Api-Token: ${API_TOKEN}" \ --header "X-Client-Source: ${PARTNER_CODE}" \ --header "Accept-Language: en" \ --header "Accept: text/html" \ --output label.html ``` ## Example response ```html
Illustrative label HTML; layout is generated by TMS.
``` ## Response fields HTML label in the selected format. Not binary PDF; no download Content-Disposition is documented. ## Error handling See [HTTP codes and source limitations](reference). HTTP success does not prove COD, fees, or cancellation are correct; inspect the documented business fields. Do not assume create-order retries are safe. --- # Cancel an order Cancel an eligible pickup-stage order and verify the returned order code. Company: Metfone Express API version: v1 Requested language: en Content language: en Source: https://mfexpress-partner-docs.zhst.app/mfexpress/en/cancel-order Endpoint: DELETE /order/cancel ```http DELETE /tms-partner/api/v1/order/cancel ``` ## Usage and important notes Send a JSON body even though the method is DELETE. `order_code` is the `id` from create-order. Cancellation is permitted only for internal states **110, 120, 200, 220, 230**, the “pickup in progress” group in the webhook table. The guide also says an order already handed over to the courier or in transport cannot be canceled; use the explicit numeric eligibility list rather than guessing from translated labels. **Do not rely on HTTP success.** If no order matches for the partner, the source describes `{}` (order_code absent/null), not a clear error. Confirm that the returned `order_code` matches the requested one. ## Authentication headers | Header | Requirement | Value / meaning | | --- | --- | --- | | X-Api-Token | Required | `${API_TOKEN}`: environment-specific token issued by TMS | | X-Client-Source | Required | `${PARTNER_CODE}`: partner code paired with the token | | Content-Type | With a JSON body | application/json | | Accept | Required by source | application/json | | Accept-Language | Optional | `vn`, `en`, `my`, `km`, `lo` | ## Request fields Cancellation request with a JSON body. | Field | Type | Required | Description | | --- | --- | --- | --- | | `order_code` | string | Yes | The id returned by create-order. | ## Example JSON body Save as `request.json`. Replace every `${…}` string and example ID with actual environment values before sending. cURL with `--data @request.json` **does not interpolate variables inside JSON files**. Identities are synthetic. ```json { "order_code": "EXAMPLE-ORDER-001" } ``` ## cURL example `BASE_URL` must include `/tms-partner/api/v1`. Load the token and partner code from your private environment; never paste secrets into documentation or AI prompts. ```bash curl --request DELETE "${BASE_URL}/order/cancel" \ --header "X-Api-Token: ${API_TOKEN}" \ --header "X-Client-Source: ${PARTNER_CODE}" \ --header "Accept-Language: en" \ --header "Accept: application/json" \ --header "Content-Type: application/json" \ --data @request.json ``` ## Example response ```json { "order_code": "EXAMPLE-ORDER-001" } ``` ## Response fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `order_code` | string / null | See description | Must match the requested order to confirm cancellation. Absent/null when no partner order matches. | ## Error handling See [HTTP codes and source limitations](reference). HTTP success does not prove COD, fees, or cancellation are correct; inspect the documented business fields. Do not assume create-order retries are safe. --- # Receive status webhooks Authenticate incoming journey updates, acknowledge receipt, and handle duplicate states. Company: Metfone Express API version: v1 Requested language: en Content language: en Source: https://mfexpress-partner-docs.zhst.app/mfexpress/en/webhook-status ```http POST ${WEBHOOK_URL} ``` ## Delivery contract The webhook is the official journey-update source for pickup, transport, delivery success/failure, return requests, and cancellation. It updates the journey only; it does not create, edit, or cancel orders. Each partner configures **one webhook**. The source says changes are pushed sequentially in update order, but also warns that older statuses can be resent. Handle duplicates and prevent older events from regressing a newer state. Do not infer exactly-once delivery or a total ordering across retries. ## Configure with TMS | Field | Required | Description | | --- | --- | --- | | partner_account | Yes | Partner account issued by the carrier | | token | Yes | Secret token supplied by the partner and sent with each request | | webhook_url | Yes | Partner HTTPS endpoint receiving updates | ## Incoming headers | Header | Value | | --- | --- | | Content-Type | application/json | | Authorization | Bearer ${WEBHOOK_TOKEN} | ## Payload fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `order_code` | string | Yes | Order code. | | `post_code` | string | Yes | Current branch/unit code. | | `province_code` | string | Yes | Province code of the current unit. | | `province_name` | string | Yes | Province name of the current unit. | | `department_address` | string | Yes | Current unit address. | | `status_code` | integer | Yes | Numeric journey status from the status table. | | `status_name` | string | Yes | Journey status label. | | `actor_name` | string | Yes | Actor name; personal data must be protected. | | `updated_at` | string | Yes | Current status update time, ISO 8601. | | `note` | string / null | See description | Current status name and optional note. | ## Successful delivery example: 410 ```json { "order_code": "EXAMPLE-ORDER-001", "post_code": "EXAMPLE-POST", "province_code": "SPE", "province_name": "Kampong Speu", "department_address": "Example branch, Kampong Speu", "status_code": 410, "status_name": "Delivered successfully", "actor_name": "Example Operator", "updated_at": "2026-10-14T14:30:00+07:00", "note": "Delivered successfully" } ``` ## Receipt and processing 1. Verify `Authorization` against the configured token. This is the partner-issued webhook token, not `X-Api-Token`. 2. Durably store the payload or enqueue it internally. 3. Return HTTP **200** to acknowledge receipt; business processing need not be complete. 4. Process order updates/notifications internally, deduplicate, and own internal retries. **Source warning retained:** the guide instructs partners to return 200 even on internal business errors, including an unknown order or database failure, to prevent repeated carrier deliveries. That guidance risks losing an event if nothing was persisted. Portal integration recommendation: acknowledge only after durable receipt; if persistence fails or authentication fails, agree an error policy with TMS rather than falsely acknowledging success. This is safety guidance, not an additional provider guarantee. The source does not specify retry intervals/counts, timeout duration, event IDs, HMAC signatures, or a deduplication-key algorithm. Do not invent those guarantees. Repeated known states should not repeat side effects; retain `updated_at` and enough reconciliation context for your business rules. | Result | Carrier action | | --- | --- | | 200 OK | Marks successful; does not retry | | Non-200 | Retries according to configured count; values unspecified | | Timeout | Retries after timeout; duration unspecified | ## Webhook diagram ```mermaid sequenceDiagram participant T as TMS participant W as Partner webhook participant Q as Durable inbox or queue participant B as Business processing T->>W: POST update with Bearer token W->>W: Authenticate and validate payload W->>Q: Durably accept event Q-->>W: Stored W-->>T: HTTP 200 Q->>B: Process with deduplication Note over T,W: Older states may be delivered again Note over Q,B: Partner owns business retries ``` ## Complete status table | Code | Group | Status | | --- | --- | --- | | N/A | New / draft | New | | 110 | Pickup in progress | Not assigned to a pickup courier | | 120 | Pickup in progress | Assigned to a pickup courier | | 200 | Pickup in progress | Courier traveling to pick up | | 220 | Pickup in progress | Customer requested another pickup time | | 230 | Pickup in progress | Customer did not hand over goods | | 210 | Collected / in transit | Goods collected | | 300 | Collected / in transit | Order added to a load | | 301 | Collected / in transit | Order removed from a load | | 302 | Collected / in transit | Load completed | | 306 | Collected / in transit | Handover received | | 309 | Collected / in transit | Postal item received | | 402 | Collected / in transit | Out for delivery | | 420 | Collected / in transit | Customer requested another delivery time | | 430 | Collected / in transit | Delivery unsuccessful | | 460 | Collected / in transit | Return notification | | 471 | Collected / in transit | Return refused | | 470 | Collected / in transit | Return approved | | 500 | Collected / in transit | Returning to sender | | 512 | Return delivery | Return out for delivery | | 530 | Return delivery | Customer requested a later return delivery | | 540 | Return delivery | Return delivery unsuccessful | | 410 | Final | Delivered successfully | | 520 | Final | Returned successfully | | 201 | Final | Order canceled | Only codes 110, 120, 200, 220, and 230 are eligible for API cancellation. “N/A” for new/draft is not a numeric code to submit. This is a status inventory, not a claim that every shipment follows every state in this order. ## OpenAPI callback The OpenAPI 3.1 export places this operation at `webhooks.orderStatus.post`. The URL is registered by the partner, not beneath the TMS base URL. The source describes no automatic webhook-registration endpoint. A 200 response body is unspecified; do not invent `{success:true}`. --- # Look up order status Read only the order ID, numeric status, and most recent update time. Company: Metfone Express API version: v1 Requested language: en Content language: en Source: https://mfexpress-partner-docs.zhst.app/mfexpress/en/order-status Endpoint: GET /order/{order_code} ```http GET /tms-partner/api/v1/order/{order_code} ``` ## Usage and important notes **Exactly three fields:** `id`, numeric `status`, and `updated_at`. This endpoint does not return the create-order response, pricing, full addresses, or parcel details. Preserve the create response for full order data; webhook messages provide journey updates, not a replacement full-order snapshot. Compare numeric `status` with the webhook status table. Do not confuse it with create-order’s textual `CONFIRMED`. `updated_at` records the latest status change. ## Authentication headers | Header | Requirement | Value / meaning | | --- | --- | --- | | X-Api-Token | Required | `${API_TOKEN}`: environment-specific token issued by TMS | | X-Client-Source | Required | `${PARTNER_CODE}`: partner code paired with the token | | Content-Type | With a JSON body | application/json | | Accept | Required by source | application/json | | Accept-Language | Optional | `vn`, `en`, `my`, `km`, `lo` | ## Path and query parameters | Name | Location | Required | Description | | --- | --- | --- | --- | | order_code | path | Yes | The id returned by create-order. | ## cURL example `BASE_URL` must include `/tms-partner/api/v1`. Load the token and partner code from your private environment; never paste secrets into documentation or AI prompts. ```bash curl --request GET "${BASE_URL}/order/${ORDER_CODE}" \ --header "X-Api-Token: ${API_TOKEN}" \ --header "X-Client-Source: ${PARTNER_CODE}" \ --header "Accept-Language: en" \ --header "Accept: application/json" ``` ## Example response ```json { "id": "EXAMPLE-ORDER-001", "status": 110, "updated_at": "2026-10-14T09:00:00.000+00:00" } ``` ## Response fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | See description | Use the identifier returned by the API, not the illustrative value. | | `status` | integer | See description | Current numeric status; use the webhook status table. | | `updated_at` | string | See description | Last status update time. | ## Error handling See [HTTP codes and source limitations](reference). HTTP success does not prove COD, fees, or cancellation are correct; inspect the documented business fields. Do not assume create-order retries are safe. --- # Reference and source notes Service codes, HTTP errors, and environment URLs. Company: Metfone Express API version: v1 Requested language: en Content language: en Source: https://mfexpress-partner-docs.zhst.app/mfexpress/en/reference ## Environments and complete base URLs | Environment | BASE_URL | | --- | --- | | Sandbox | https://dev-gw.metfone.com.kh/tms-partner/api/v1 | | Production | https://gw-express.metfone.com.kh/tms-partner/api/v1 | Each API path adds `/api/v1` to the host plus `/tms-partner`; the URLs above combine both parts so prefixes are neither missing nor duplicated. Template creation and label retrieval share this base, not `/oms-core/api/v1`. Do not call production before obtaining official environment credentials. ## Service codes | Code | Kind | Meaning | | --- | --- | --- | | CCN | mainService | Standard shipping; pricing examples label it Express, so confirm commercial naming. | | P2P | mainService | Fast point-to-point delivery. | | GTN | extraService / vas | Delivery to the recipient’s address. | | NTN | extraService / vas | Pickup at the sender’s address. | | VTT | extraService / vas | Address delivery/pickup according to senderService/receiverService. | | VCT | extraService | Forward to an alternate address after failed delivery. | | GCH | extraService | Return service. | | DISCOUNT | Adjustment | Partner-contract discount. | ## orderService | Field | Accepted values | | --- | --- | | orderService.senderService | `GTCH`, `VTT`, `GTN`, `UNKNOW` | | orderService.receiverService | `NTCH`, `NTN`, `VTT`, `UNKNOW` | Preserve `UNKNOW`; do not silently correct it to UNKNOWN. GTCH/NTCH are not fully defined, and the sender/receiver labels can be confusing relative to service names; confirm their use rather than swapping codes. At get-service, these preferences affect extra-service flags only, not price. ## Common HTTP errors | Code | Meaning | Action | | --- | --- | --- | | 400 | Bad Request | Check body, paths, required fields, and data types. | | 401 | Unauthorized | Token invalid/expired; contact TMS to refresh X-Api-Token. | | 403 | Forbidden | Check endpoint access and X-Client-Source/token pairing. | | 404 | Not Found | Check URL, location codes, and order IDs. | | 422 | Unprocessable | Business-invalid data; inspect the returned message. | | 500 | Internal Server Error | Retry later and report repeated failures to TMS. get-service/get-all/label may return 500 for missing fields. For an ambiguous create outcome, reconcile before retrying because idempotency is not established. | No common error schema, rate limit, SLA, or retry schedule is defined. `PAYER_INVALID` and `INVALID_PRICE` are create-order business errors without a specific HTTP status each. --- # Use with AI coding tools Give Claude, Codex, or Cursor scoped documentation and verifiable API contracts. Company: Metfone Express API version: v1 Requested language: en Content language: en Source: https://mfexpress-partner-docs.zhst.app/mfexpress/en/ai-tools ## Company- and language-scoped context These are real portal routes generated from published CMS content. `llms.txt` is a short index; `llms-full.txt` contains complete resolved documentation; `openapi.json` contains inline operation schemas and an OpenAPI 3.1 webhook; every page has a `.md` representation. Stay within the selected company/language rather than mixing providers. Record the actual document language when a translation falls back. | Resource | Path | | --- | --- | | Index | [llms.txt](/mfexpress/en/llms.txt) | | Full context | [llms-full.txt](/mfexpress/en/llms-full.txt) | | OpenAPI | [openapi.json](/mfexpress/en/openapi.json) | | Create-order Markdown | [create-order.md](/mfexpress/en/create-order.md) | ## Claude, Codex, and Cursor 1. **Claude:** give a web-capable session the index URL; if web access is unavailable, download and attach the Markdown/OpenAPI files. Pasting a URL does not prove the assistant read it. 2. **Codex:** ask it to read `llms.txt` first, then fetch relevant `.md` pages and `openapi.json` with an allowed network tool. For offline work, provide downloaded files. Require generated code to be checked against schemas and missing facts to be reported. 3. **Cursor:** add the documentation URL as a Docs source if supported by your installed version, or download files into an approved context location and reference them in chat. Do not assume Cursor’s index is fresh; inspect the cited content and endpoints. 4. Ask for a plan before changing business behavior; resolve uncertainties with TMS, never let an assistant invent surchargeType, idempotency, or an SLA. 5. Exercise sandbox only with credentials kept outside prompts; compare COD, fees, label HTML, cancellation results, and duplicate webhook behavior. ## Download context ```bash # Set PORTAL_ORIGIN to the actual deployed portal origin, without a trailing slash. curl --fail --show-error "${PORTAL_ORIGIN}/mfexpress/en/llms.txt" curl --fail --show-error "${PORTAL_ORIGIN}/mfexpress/en/openapi.json" --output cambodia-openapi.json curl --fail --show-error "${PORTAL_ORIGIN}/mfexpress/en/create-order.md" --output create-order.md ``` ## Suggested prompt ```text Read /mfexpress/en/llms.txt, the relevant Markdown pages, and /mfexpress/en/openapi.json. Implement only the Cambodia TMS Partner API described there. Cite page paths and field names for important decisions. Disclose actual document language and all uncertainties. Do not invent surchargeType enums or txId server idempotency. Send moneyCollection separately during creation and check cod. Labels are HTML; status lookup has only three fields. Propose sandbox checks and identify missing inputs, but do not request tokens or customer data in chat. Do not call production. ``` ## Safety boundaries Do not include real `API_TOKEN`, `PARTNER_CODE`, webhook tokens, personal addresses, phones, customer orders, or production data in prompts, public context files, or shared logs. Use synthetic examples and environment variables. AI is not authority for the provider contract; this OpenAPI is a bounded interpretation of the document, not a TMS-certified specification. Review [source details](reference) before accepting generated code.