Send orders
Sends orders from a store that is not on Shopify, or from your own backend, so MTA can attribute them.
https://mta.growthdept.ai/api/v1/ordersRequest#
curl -X POST "https://mta.growthdept.ai/api/v1/orders" \
-H "Authorization: $MTA_API_KEY" \
-H "Data-Client-ID: $MTA_BRAND_ID" \
-H "Content-Type: application/json" \
-d '[
{
"order_id": "1001",
"customer_id": "c-42",
"time_of_purchase": "2026-11-05T14:03:00-08:00",
"currency": "USD",
"purchase_total": 64.5,
"tax": 4.5,
"shipping_cost": 5,
"products": [
{ "id": "sku-1", "name": "Canvas tote", "quantity": 1, "price": 55 }
],
"order_tags": ["wholesale"],
"visitor_keys": ["gd_7f3a9c2e41b8"]
}
]'Response#
{
"message": "Orders successfully upserted",
"count": 1,
"created": 1,
"changed": 1
}Authentication#
Send the API key in Authorization and the brand id in Data-Client-ID. The key needs the orders:write scope.
| Header | Type | Description |
|---|---|---|
Authorizationrequired | string | The API key. See Authentication. |
Data-Client-IDrequired | uuid | The brand id. |
Content-Type | string | application/json. |
Body#
A JSON array of 1 to 1,000 orders, at most 20 MB. Each order_id may appear once per request, and a field this table does not list is refused.
| Field | Type | Description |
|---|---|---|
order_required | string | Your order id, up to 200 characters. Orders upsert on it. |
customer_required | string | Your customer id, up to 200 characters. It is stored only as a hash with the brand's key, and tells first-time customers from returning ones. |
time_required | string (date-time) | ISO 8601 with a time zone offset or Z. |
currencyrequired | string | Three capital letters, such as USD. |
purchase_required | number | The order's revenue, with tax, shipping and fees included. |
taxrequired | number | Tax on the order. |
shipping_ | number | Shipping charged. Counts as 0 when left out. |
discount_ | number | Discounts on the order. |
productsrequired | array | At least one product: id, name, quantity (0 or more) and price, and optionally variant_id and variant_name. |
refunds | array | Each refund's product_id, quantity, refund_amount, refund_cost and refund_made_at (date-time), and optionally variant_id. No two may be identical. |
discount_ | array of strings | Codes used on the order, each once. |
order_ | array of strings | Tags, each once. Tags the brand excludes in Settings > Exclude orders by tag keep the order out of attribution. |
is_ | boolean | Marks a subscription order. |
cancelled_ | string (date-time) or null | When the order was cancelled. A cancelled order keeps its attribution credit, as the store's own totals count it. |
visitor_ | array of strings | Up to 10 visitor ids that made the purchase: the pixel's gd_vid or Shopify's client id. 8 to 128 characters of letters, digits, ., _, : and -. They tie the order to its visits. |
customer_ | string | The buyer's email. It becomes a key for matching partner views and is not stored. |
hashed_ | string | The SHA-256 hex digest of the trimmed, lower-cased email, in place of customer_email. A value that is not 64 hex characters is ignored. |
customer_ | various | Accepted so existing clients keep working, and dropped unread. |
alternate_ | array | Objects with type and id. Accepted; not used today. |
Response fields#
| Field | Type | Description |
|---|---|---|
count | integer | Orders written. |
created | integer | Of those, orders MTA did not have before. |
changed | integer | Orders that are new or whose attributed values changed, which MTA will recompute. |
Status codes#
| Status | Meaning | Body |
|---|---|---|
| 200 | The orders were written. | |
| 400 | The body is not a JSON array of valid orders, or repeats an order_id. issues lists the first 50 problems, each with its path, such as 0.currency. | {
"detail": "invalid input",
"error": "invalid input",
"issues": [
{
"path": "0.currency",
"message": "Invalid string: must match pattern /^[A-Z]{3}$/"
}
]
} |
| 401 | No key, an unknown or revoked key, or a key that belongs to another brand than Data-Client-ID. A key with the right brand but without orders:write gets 403 instead. | {
"detail": "unauthenticated: send the API key in Authorization and the brand id in Data-Client-ID",
"error": "unauthenticated: send the API key in Authorization and the brand id in Data-Client-ID"
} |
| 403 | The key is valid but lacks orders:write. | {
"detail": "this API key lacks the orders:write scope",
"error": "this API key lacks the orders:write scope"
} |
| 409 | An order_id belongs to one of the brand's Shopify orders. Nothing in the request was written; order_ids lists the ids. | {
"detail": "these order ids belong to the brand's Shopify orders",
"error": "these order ids belong to the brand's Shopify orders",
"order_ids": ["1001"]
} |
| 413 | The body is over 20 MB. Send fewer orders per request. | {
"detail": "body over 20 MB; send fewer orders per request",
"error": "body over 20 MB; send fewer orders per request"
} |
| 422 | No exchange rate yet for an order's currency on its date. MTA has asked for the rate; send the same orders again later. Nothing was written. | {
"detail": "no ECB rate for CAD to USD on or up to 7 days before 2026-11-05; run fx_pull for that date or add the rate to ops.fx_rates",
"error": "no ECB rate for CAD to USD on or up to 7 days before 2026-11-05; run fx_pull for that date or add the rate to ops.fx_rates",
"retry": "the rate has been requested; send the orders again later"
} |
| 500 | Something failed on our side. Send the same request again later; orders upsert, so a retry is safe. | { "detail": "Internal server error.", "error": "Internal server error." } |
Sending an order again#
Each order upserts on order_id, so send the whole order every time it changes. The products you send replace the order's stored products, and the refunds you send replace its stored refunds: a refund you leave out is removed.
Sending an order without customer_email or hashed_customer_email removes any email key it had.
Revenue and currency#
Revenue is purchase_total. Gross sales are purchase_total - tax - shipping_cost + discount_amount. An order whose revenue is 0 is stored but takes no attribution credit.
Amounts are converted to the brand's currency at the European Central Bank rate for the order's date in the brand's time zone, or the latest rate up to 7 days before it. When there is none, the request gets 422 and MTA asks for the rate.
Personal data#
MTA keeps no contact details from this API. customer_id is stored as a hash with the brand's key. An email becomes a key only this brand's data can match, used to match partner views sent with an email (Send matched views), and the address is discarded. Phone, name, IP address and shipping address fields are accepted and dropped unread.
Shopify stores#
Orders from a connected Shopify store come from the Shopify connection. An order_id that belongs to one of those orders is refused with 409, so this API never overwrites them.
Failed requests#
When a request with a valid key fails, MTA keeps the status and error message for 30 days, and the brand's team can read them under Settings > Data Diagnostics. The error response carries the record's id in X-Request-Id; quote it when you ask us about a failure.
Questions about the API: support@growthdept.ai.