Originally posted at https://github.com/commerce-operations-foundation/mcp-reference-server/pull/24
orderTax and orderDuty are proposed as separate numeric fields at order header level. The reviewer noted that a dedicated TaxesAndDuties node would be more coherent, and that duties in particular can be multiple and typed. The author's position is that carriers such as DHL require tax and duty to be declared separately, as collected by the seller at checkout, and that the header-level placement is standard. Decision needed on whether to introduce a dedicated financial sub-node and how to handle multiple duty types.
What the existing schema tells us:
- orderTax (number) already exists as a flat header field
- shippingPrice (number), giftNote (string), incoterms (string) are all flat header fields
- The PR adds orderDuty (number) at the same level
- The schema uses additionalProperties: false on objects, meaning any new node needs to be deliberate
Suggested resolution: A single orderDuty number cannot represent multiple duty types. The author is also right that DHL and other carriers expect tax and duty declared separately at order header level for their API payloads. These are not mutually exclusive requirements: the schema can hold structured data internally, and the TMS/carrier adapter translates to the flat carrier format on output.
"crossBorderCharges": {
"type": "object",
"description": "Taxes and duties collected by the seller at checkout, required for customs declaration on cross-border shipments. Values must originate from the shopping cart. Currency follows the order-level currency field.",
"properties": {
"taxCollected": {
"description": "Total tax amount collected by the seller (VAT, GST, sales tax, or equivalent). Passed as a single header-level value to carrier APIs.",
"type": "number"
},
"dutyCollected": {
"description": "Total duty amount collected by the seller. Passed as a single header-level value to carrier APIs (e.g. DHL duty field).",
"type": "number"
},
"duties": {
"description": "Itemised duty breakdown. Used where carriers or customs authorities require duty typed by category. The sum should equal dutyCollected.",
"type": "array",
"items": {
"type": "object",
"properties": {
"dutyType": {
"description": "Classification of the duty.",
"type": "string",
"enum": ["IMPORT_DUTY", "EXCISE", "ANTI_DUMPING", "COUNTERVAILING", "OTHER"]
},
"amount": {
"description": "Duty amount for this type.",
"type": "number"
},
"description": {
"description": "Optional free-text clarification, used when dutyType is OTHER.",
"type": "string"
}
},
"required": ["dutyType", "amount"],
"additionalProperties": false
}
},
"taxScheme": {
"description": "Tax regime applied (VAT, GST, HST, SALES_TAX). Informs customs and downstream tax reporting.",
"type": "string",
"enum": ["VAT", "GST", "HST", "SALES_TAX", "OTHER"]
},
"iossNumber": {
"description": "EU Import One-Stop-Shop registration number. Required for B2C shipments into the EU where VAT was collected at point of sale.",
"type": "string"
},
"ddpElected": {
"description": "True if the seller has elected Delivered Duty Paid (DDP) Incoterm, meaning all taxes and duties are pre-collected. Aligns with the incoterms field at order header level.",
"type": "boolean"
}
},
"additionalProperties": false
}
taxCollected and dutyCollected as flat numbers satisfy the carrier API requirement. DHL, UPS, and FedEx all expect a single duty value and a single tax value in their shipment request payloads. The adapter reads these two fields directly.
duties as a typed array satisfies the reviewer's concern about multiple duty types without replacing the scalar. For most ecommerce orders this array will be empty or absent. For complex goods (electronics with anti-dumping duties, alcohol with excise) it provides the breakdown without forcing every implementation to parse it.
taxScheme is operationally necessary. Whether the collected amount is VAT, GST, or US sales tax changes how it is declared to customs and how the TMS handles the carrier mapping.
iossNumber belongs here rather than in crossBorderParties because it is a transaction-level registration tied to the tax collected, not a standing party identifier. An IOSS number appears on the customs declaration alongside the tax amount.
ddpElected creates an explicit link to the incoterms field already in the schema. Rather than inferring DDP from the incoterms string, a boolean here gives the WMS and TMS an unambiguous signal to include pre-collected charges in the customs declaration.
Relationship to crossBorderParties
order (header)
├── crossBorderParties ← who the parties are and their regulatory IDs
├── crossBorderCharges ← what was collected and how it is classified
├── incoterms ← existing field, trade term governing liability
└── reasonForExport ← existing/proposed enum
The two nodes are deliberately separate. crossBorderParties is identity data; crossBorderCharges is financial data. Merging them would recreate the original TaxesAndDuties scope ambiguity the reviewer flagged, just under a different name.
Originally posted at https://github.com/commerce-operations-foundation/mcp-reference-server/pull/24
What the existing schema tells us:
Suggested resolution: A single orderDuty number cannot represent multiple duty types. The author is also right that DHL and other carriers expect tax and duty declared separately at order header level for their API payloads. These are not mutually exclusive requirements: the schema can hold structured data internally, and the TMS/carrier adapter translates to the flat carrier format on output.
taxCollected and dutyCollected as flat numbers satisfy the carrier API requirement. DHL, UPS, and FedEx all expect a single duty value and a single tax value in their shipment request payloads. The adapter reads these two fields directly.
duties as a typed array satisfies the reviewer's concern about multiple duty types without replacing the scalar. For most ecommerce orders this array will be empty or absent. For complex goods (electronics with anti-dumping duties, alcohol with excise) it provides the breakdown without forcing every implementation to parse it.
taxScheme is operationally necessary. Whether the collected amount is VAT, GST, or US sales tax changes how it is declared to customs and how the TMS handles the carrier mapping.
iossNumber belongs here rather than in crossBorderParties because it is a transaction-level registration tied to the tax collected, not a standing party identifier. An IOSS number appears on the customs declaration alongside the tax amount.
ddpElected creates an explicit link to the incoterms field already in the schema. Rather than inferring DDP from the incoterms string, a boolean here gives the WMS and TMS an unambiguous signal to include pre-collected charges in the customs declaration.
Relationship to crossBorderParties
The two nodes are deliberately separate. crossBorderParties is identity data; crossBorderCharges is financial data. Merging them would recreate the original TaxesAndDuties scope ambiguity the reviewer flagged, just under a different name.