Offer Data
- Introduction
- How to create an offer?
- How to delete an offer?
- How to check your offers?
- Dictionary Fees
- SKU and Stock
- Error handling in POST /openapi/v2/offers
- Error handling in DELETE /openapi/v2/offers
Introduction
Via Offer Management API you can do different actions regarding your offer data: post, change, delete and check your offers.
⚠️ API Rate Limits
Starting Tuesday, August 6th, 2024, we will be introducing rate limits on the following API endpoints:
- POST /openapi/v2/offers: 5,500 requests per minute
- GET /openapi/v2/offers: 500 requests per minute
- DELETE /openapi/v2/offers: 1,500 requests per minute
🚨 If your requests exceed the specified rate limits, the excess requests will be rejected, and you may receive an HTTP 429 (Too Many Requests) status code. Consistently exceeding the limits may result in further action according to our API usage policies.
How to create an offer?
Using POST /openapi/v2/offers with request body (OfferV2PostItem).
Field name | Field type | Is required? | Requirements/validations | Comments |
---|---|---|---|---|
gtin | string | - | · max length = 14 · at least one of (gtin, mid, mpn+manufacturer) required | |
sku | string | - | · max length = 100 · at least one of (gtin, mid, mpn+manufacturer) required | · sku is case insensitive · it is possible to create an offer using only sku, if there is already an offer with specified sku · sku can be treated separately for origin and destination, allowing you to set up different quantity per market. To learn more about this, please click here. |
mpn | string | - | · max length = 100 ·at least one of (gtin, mid, mpn+manufacturer) required | |
manufacturer | string | - | · max length = 100 · at least one of (gtin, mid, mpn+manufacturer) required | |
mid | string | - | · max length = 13 ·at least one of (gtin, mid, mpn+manufacandturer) required | |
quantity | int | + | · min = 0 · max = 100000 | · see comments for sku · When you run out of stock you can put the quantity = 0 so the offer is taken out from the marketplace. Once the stock is available, you are able to bring the offer to marketplace just setting quantity > 0. |
netPrice | object | + | ||
netPrice.amount | float | + | · min = 0.01 · max = 100000.0 | Default math rounding will be applied to bring value to “%.2f” format |
netPrice.currency | string | + | · enum (“EUR”) | |
processingTime | int | + | · min = 0 · max = 100 | |
maxProcessingTime | int | - | · min = max(1, processingTime) · max = 100000.0 | greater than 0 and greater than or equal processingTime |
businessModel | string | - | · num (“”, “B2B”, “B2B/B2C”) | |
freightForwarding | bool | - | ||
netVolumePrices | array | - | ||
…netVolumePrices[i]… | ||||
netVolumePrice.price | object | |||
netVolumePrice.price.amount | float | · min = 0 · given netVolumePrices[i].quantity > netVolumePrices[j].quantity then netVolumePrices[i].price.amount < netVolumePrices[j].price.amount for any i,j < length(netVolumePrices) | · only decreasing prices, no repeated counts · default math rounding applied to bring value to “%.2f” format | |
netVolumePrice.price.currency | string | · num (“EUR”) | ||
netVolumePrice.quantity | int | · min = 2 · max = 100000 | ||
…netVolumePrices[i]… | ||||
destination | string | + | · enum(“DE_MAIN“, “ES_MAIN”, “IT_MAIN“, “PT_MAIN“, “NL_MAIN”, “FR_MAIN”) | |
origin | string | + | · enum(“DE_MAIN“, “ES_MAIN”, “IT_MAIN“, “PT_MAIN“, “NL_MAIN”, “FR_MAIN”) | |
services | array | - | You can ignore the service, as it´s only used for internal purposes (Example in the Swagger Documentation). | |
includedFees | array | - | · check /dictionary/included-fees · includedFee.type (string enum(“ECO_WEEE_HOUSEHOLD”, “ECO_WEEE_PROFESSIONAL”, “ECO_FURNITURE”, “ECO_BATTERIES”, “ECO_PACKAGING”, “ECO_TEXTILES”, “ECO_CHEMICALS”, “ECO_SPORT”, “ECO_TOYS”, “ECO_PAPER”, “ECO_DIY”)) · includedFee.amount (number, min = 0.01) | · Empty value or skipped allowed for all countries · Marketplace implies that includedFee.amount is already included into the netPrice. · Currency taken from the netPrice |
shippingGroupName | string | - | · Existing shipping group name ·Provide shippingGroupName OR shippingGroupId , if both are provided, http 400 Bad request will be returned. |
· You can set up Shipping Costs using Shipping Groups. More information here. · Shipping Group destination must match with the destination from the Offer. · If you don´t use this option, you need to send the price including the shipping costs. |
shippingGroupId | string | - | · Deprecated, please use shippingGroupName · Existing shipping group Id (should be valid UUID) |
Please, take into consideration the following:
- Send one offer per request, multiple offers in one request are not supported.
- In case any non mandatory fields are not provided, you can send them empty
""
,null
or just don´t include them in the request. - For updating the
quantity
of an existing offer you just need to send a newPOST
request with the same product data and updating quantity field. - If
netPrice
orbusinessModel
ornetVolumePrices
fields change, it will trigger new offer creation and a deactivation of the previous one. - If you know that offer for this item is not available in the nearest future, just deactivate the Offer sending a
DELETE
request with product identifier. - All prices must be net prices, there is no possibility to send gross prices (with taxes).
- If destination = FR_MAIN, you can use the
includedFees
parameter to indiciate the éco-participation. This is an infomative field to be shown on the marketplace. ThenetPrice
needs to be provided including this fee, as MM does not sum up to the price. Use/dictionary/included-fees
to check all fees per country. - Via API no .csv files for offer data are supported.
Example:
Here is an request example providing gtin
and not providing mpn
, manufacturer
or mid
.
{
"gtin": "4251143960263",
"sku": "8888",
"mpn": "",
"manufacturer": null,
"quantity": 20,
"netPrice": {
"amount": 50,
"currency": "EUR"
},
"processingTime": 5,
"maxProcessingTime": 10,
"businessModel": "B2B",
"freightForwarding": true,
"netVolumePrices": [
{
"price": {
"amount": 48,
"currency": "EUR"
},
"quantity": 2
}
],
"destination": "DE_MAIN",
"origin": "DE_MAIN",
"shippingGroupName": "2ManHandling"
}
In case of successful request, you will get a response (OfferV2GetItem):
Field name | Field type | Requirements/validations | Comments |
---|---|---|---|
offerNumber (deprecated) | string | Deprecated. Number for internal METRO Markets use. | |
gtin | string or null | ||
sku | string or null | ||
mpn | string or null | ||
manufacturer | string or null | Given in the response, because we have this product information in our database | |
mid | string | ||
quantity | int | ||
netPrice | object | ||
netPrice.amount | float | ||
netPrice.currency | string | enum (“EUR”) | |
processingTime | int or null | ||
maxProcessingTime | int or null | ||
businessModel | int | enum (1, 2) | 1 - “B2B/B2C”, 2 - “B2B” |
freightForwarding | bool | ||
offerStatus | object | ||
offerStatus.internalStatus | string | enum (“deactivated”, “active”, ‘paused”, “company_verification”, “company_rejected”, “product_incomplete”, “product_unpublished”, “product_blacklisted”, “inactive”) | |
offerStatus.readableStatus | string | This is the translation of the offersStatus.internalStatus. Per default is provided in German. To get this information in any other language, you need to specify the Accept-Language in the header of your request. | |
productStatus | object | This is how we map internalStatus to readableStatus and their meaning: · 1 - published -> product is published on the marketplace · 2 - incomplete -> product is not completed · 3 - blacklisted -> product was blacklisted from MM because a specific reason · 4 - unpublished -> product was unpublished from MM because a specific reason Note: only products with status: published and is_active=true will be available online on the marketplace and customers will be able to purchase them. For all other statuses, API will return success, as the offer is consumed, but the Product Data is not ready to go and needs attention. | |
productStatus.internalStatus | int | enum (1, 2, 3, 4) | |
productStatus.readableStatus | string | enum (“published”, “incomplete”, “blacklisted”, “unpublished”) | |
netVolumePrices | array | ||
…netVolumePrices[i]… | |||
netVolumePrice.price | object | ||
netVolumePrice.price.amount | float | ||
netVolumePrice.price.currency | string | enum (“EUR”) | |
netVolumePrice.quantity | int | ||
…netVolumePrices[i]… | |||
isActive | bool | · false: offer is not visible on the market place · true: together with offerStatus.internalStatus: active means that your offer is visible at the marketplace. | |
productKey | string | number for internal METRO Markets use | |
productName | string | displayed product name | |
destination | string | · enum(“DE_MAIN“, “ES_MAIN”, “IT_MAIN“, “PT_MAIN“, “FR_MAIN”) | |
origin | string | · enum(“DE_MAIN“, “ES_MAIN”, “IT_MAIN“, “PT_MAIN“, “FR_MAIN”) | |
services | array | An empty array delivered as it´s only used for internal purposes | |
includedFees | array | · It´s returned if it has been provided in the request. · Check /dictionary/included-fees to see applicable markets | |
…includedFees[i] | |||
includedFee.type | string | enum(“ECO_WEEE_HOUSEHOLD”, “ECO_WEEE_PROFESSIONAL”, “ECO_FURNITURE”, “ECO_BATTERIES”, “ECO_PACKAGING”, “ECO_TEXTILES”, “ECO_CHEMICALS”, “ECO_SPORT”, “ECO_TOYS”, “ECO_PAPER”, “ECO_DIY”) | |
includedFee.amount | number | includedFee.amount (min = 0.01) | |
shippingGroupId | object or null | ||
shippingGroup.shippingGroupId | string | ||
shippingGroup.shippingGroupName | string | ||
shippingGroup.createdAt | DateTime string | format: ISO 8601 |
Full detail list of errors here.
Response example (HTTP 200):
{
"offerNumber": "1234",
"gtin": "4251143960263",
"mid": "AAA0000057385",
"sku": "88888",
"mpn": "1",
"manufacturer": "Random company",
"netPrice": {
"amount": "50.00",
"currency": "EUR"
},
"processingTime": 5,
"maxProcessingTime": 10,
"businessModel": 2,
"quantity": 20,
"freightForwarding": true,
"offerStatus": {
"internalStatus": "active",
"readableStatus": "Aktiv"
},
"productStatus": {
"internalStatus": 1,
"readableStatus": "published"
},
"netVolumePrices": [
{
"price": {
"amount": "48.00",
"currency": "EUR"
},
"quantity": 2
}
],
"isActive": true,
"productKey": "ded54740-9d64-46fc-920d-0a982aa3391b",
"productName": "Motivknöpfe Karpfen Fb. Kupfer",
"services": [],
"destination": "DE_MAIN",
"origin": "DE_MAIN",
"shippingGroup": {
"shippingGroupId": "23855521-1902-4a90-acb7-81a4744c1759",
"shippingGroupName": "2ManHandling",
"createdAt": "2022-03-27T22:00:35+00:00"
}
}
How to delete an offer?
Using DELETE /openapi/v2/offers
with request params:
Param name | Is required | Requirements/validations | Comments |
---|---|---|---|
gtin | - | at least one of (gtin, mid, sku, mpn+manufacturer) required | |
sku | - | at least one of (gtin, mid, sku, mpn+manufacturer) required | sku is case insensitive |
mpn | - | at least one of (gtin, mid, sku, mpn+manufacturer) required | |
manufacturer | - | at least one of (gtin, mid, sku, mpn+manufacturer) required | |
mid | - | at least one of (gtin, mid, sku, mpn+manufacturer) required | |
destination | + | enum(“DE_MAIN“, “ES_MAIN”, “IT_MAIN“, “PT_MAIN“, “NL_MAIN”, “FR_MAIN”) | |
origin | + | enum(“DE_MAIN“, “ES_MAIN”, “IT_MAIN“, “PT_MAIN“, “NL_MAIN”, “FR_MAIN”) |
Use this method (DELETE /openapi/v2/offers
) when you want to deactivate the offer from the marketplace.
Note: if you want to remove the offer temporary from the marketplace, but you know that you will activate it again, you can use POST
request with quantity=0.
Offer Deactivation is used for short or long periods with removal it from the Marketplace in case:
- if a product is getting end of life
- if you don´t plan to sale this product anymore
- any other reason for which offer should be removed from the Marketplace.
Full detail list of errors here.
How to check your offers?
With GET
request to /openapi/v2/offers
you will get a list of all your offers and their statuses.
Param name | Is required | Default value | Requirements/validations | Comments |
---|---|---|---|---|
limit | - | 20 | integer | max value recommended 10000 |
offset | - | 0 | integer | |
sort[createdAt] | - | DESC | enum(“DESC”, “ASC”) | |
filter[gtin] | - | |||
filter[sku] | - | |||
filter[status] | - | active | enum (“deactivated”, “active”, “paused”, “inactive”) |
Filters can be concatenated.
Examples:
/openapi/v2/offers?limit=25&offset=10&sort%5BcreatedAt%5D=DESC
-> This will provide you the offers from 10 to 35 in descending order per creation date./openapi/v2/offers?filter%5Bsku%5D=111112359
/openapi/v2/offers?filter%5Bgtin%5D=44040000008686
Dictionary Fees
With GET
request to /openapi/v2/dictionary/included-fees
you will get a list of all fees per country/market that you can send in POST offer request (includedFee/type
).
Marketplace implies that includedFee.amount is already included into the netPrice.
SKU and Stock
You can manage different quantity per market (Origin / Destination) for a product using the combination of the following parameters: origin, destination, product (GTIN, MID or mpn + manufacturer) and SKU.
This is optional, if you prefer to keep quantity independent to the market, you can still do so.
Please have a look at the two possible options:
Option 1: Common stock -quantity- per Origin (having same SKU)
Offer A and B have the same sku and the same quantity.
- Offer A:
“gtin”: “123456”, “origin”: “DE_MAIN”, “destination”: “DE_MAIN”, “price”: 60, “sku”: 1111, “quantity”: 10
- Offer B:
“gtin”: “123456”, “origin”: “DE_MAIN”, “destination”: “ES_MAIN”, “price”: 55, “sku”: 1111, “quantity”: 10
If you send an update in quantity for sku: 1111, the quantity will be updated for both offers (A and B), regardless of origin and destination.
- In this case Offer A and Offer B will have quantity 20:
“gtin”: “123456”, “origin”: “DE_MAIN”, “destination”: “ES_MAIN”, “price”: 55, “sku”: 1111, “quantity”: 20
Option 2: Indpendent Stock -quantity- per Origin (when SKUs are different)
Offer A and B have different SKUs, same origin and different destination, therefore they can handle different quantity for the same product.
- Offer A:
“gtin”: “123456”, “origin”: “DE_MAIN”, “destination”: “DE_MAIN”, “price”: 60, “sku”: 1111, “quantity”: 10
- Offer B:
“gtin”: “123456”, “origin”: “DE_MAIN”, “destination”: “ES_MAIN”, “price”: 55, “sku”: 2222, “quantity”: 15
Error handling in POST /openapi/v2/offers
Here you can find a list of all possible error messages when status code is 400:
- Offer is rejected because the price has dropped by 50% or more:
- message: ‘Please check your price. Offer is rejected because the price has dropped by 50% or more. Offer price reduction not more than 50% at a time is allowed.’
- For existing active offer and for each next POST message it is allowed price drop (Net price+Shipping Cost) for not more than 50% at a time.
- If the price is correct, please DELETE the offer and send the POST request again.
- gtin:
- Regex: pattern: ‘/^\d*$/' message: ‘GTIN: Only numeric value is allowed’
- Length: max: 14 maxMessage: ‘GTIN exceeds max allowed length of characters {{ limit }}'
- message: ‘GTIN not found’
- sku:
- Type: type: string message: ‘SKU: Only {{ type }} value is allowed’
- Length: max: 100 maxMessage: ‘SKU exceeds max allowed length of characters {{ limit }}'
- Regex: pattern: ‘/^[_a-zA-ZÖöÄäÜüß0-9+./-\ ]*$/' message: ‘SKU: Only uppercase and lowercase latin letters, figures, underscore, space, hyphen, plus, slashes and dot allowed’
- message: ‘The provided SKU exists for another GTIN’.
- quantity:
- NotBlank: message: ‘Quantity: Field is required’
- Type: type: numeric message: ‘Quantity: Only {{ type }} value is allowed’
- Regex: pattern: ‘/^\d+$/' message: ‘Quantity: Only numeric value is allowed’
- GreaterThanOrEqual: value: 0 message: ‘Quantity: Value does not match the allowed range’
- LessThanOrEqual: value: 100000 message: ‘Quantity: Value does not match the allowed range’
- processingTime:
- NotBlank: message: ‘Minimum processing time: Field is required’
- Type: type: numeric message: ‘Minimum processing time: Only {{ type }} value is allowed’
- GreaterThanOrEqual: value: 0 message: ‘Minimum processing time: Only integer values from 0 to 100 is allowed’
- LessThanOrEqual: value: 100 message: ‘Minimum processing time: Only integer values from 0 to 100 is allowed’
- maxProcessingTime:
- Type: type: numeric message: ‘Maximum processing time: Only integer values from 1 to 100 is allowed’
- Regex: pattern: ‘/^\d+$/' message: ‘Maximum processing time: Only integer values from 1 to 100 is allowed’
- GreaterThanOrEqual: propertyPath: processingTime message: ‘The minimal processing time must not exceed the maximum processing time’
- GreaterThan: value: 0 message: ‘Maximum processing time: Only integer values from 1 to 100 is allowed’
- LessThanOrEqual: value: 100 message: ‘Maximum processing time: Only integer values from 1 to 100 is allowed’
- businessModel:
- Regex: pattern: ‘/^b2c$/i’ match: false message: ‘B2B/B2C: Offer upload for the B2C only is forbidden’
- Regex: pattern: ‘#^(b2b|b2b/b2c)$#i’ message: ‘B2B/B2C: Only “B2B”, “B2B/B2C” or empty value is allowed.'
- mid:
- Type: type: string message: ‘MID: Only {{ type }} value is allowed’
- Length: max: 13 maxMessage: ‘MID exceeds max allowed length of characters {{ limit }}'
- Regex: pattern: ‘/^(?:[a-z]{3}\d{10})*$/i’ message: ‘Wrong MID value format’
- freightForwarding:
- Type: type: boolean message: ‘Freight forwarding: wrong value type was provided’
- mpn:
- Type: type: string message: ‘MPN: Only {{ type }} value is allowed’
- Length: max: 100 maxMessage: ‘MPN exceeds max allowed length of characters {{ limit }}'
- Regex: pattern: ‘/^[a-zA-Z0-9_- \t\n.,+/]*$/' message: ‘Wrong MPN value format’
- manufacturer:
- Type: type: string message: ‘Manufacturer: Only {{ type }} value is allowed’
- Length: max: 100 maxMessage: ‘Manufacturer exceeds max allowed length of characters {{ limit }}'
- netPrice:
- NotBlank: message: ‘Net price: Field is required’
- PriceMoneyConstraint: requiredAmountRangeMessage: ‘Net price: Amount value does not match the allowed range’ requiredTypeMessage: ‘Net price: Only Money value is allowed’ requiredAmountTypeMessage: ‘Net price: Only Float amount value is allowed’ requiredCurrencyMessage: ‘Net price: currency not specified’ invalidCurrencyMessage: ‘Net price: Only {{ allowedCurrencies }} currency may be specified’ allowedCurrencies: - ‘EUR’
- Please check your price. Offer is rejected because the price has dropped by 50% or more. Offer price reduction not more than 50% at a time is allowed.
- destination:
- NotBlank: message: ‘Destination: Field is required’
- Type: type: string message: ‘Destination: Only {{ type }} value is allowed’
- Regex: message: ‘Destination: wrong value format’
- origin:
- NotBlank: message: ‘Origin: Field is required’
- Type: type: string message: ‘Origin: Only {{ type }} value is allowed’
- Case of wrong syntax:
{ "type": "validation", "title": "Malformed request: Syntax error", "status": 400, "detail": "", "instance": null }
Error handling in DELETE /openapi/v2/offers
Here you can find a list of all possible error messages when status code is 400:
- gtin:
- Regex: pattern: ‘/^\d*$/' message: ‘GTIN: Only numeric value is allowed’
- Length: max: 14 maxMessage: ‘GTIN exceeds max allowed length of characters {{ limit }}'
- sku:
- Type: type: string message: ‘SKU: Only {{ type }} value is allowed’
- Length: max: 100 maxMessage: ‘SKU exceeds max allowed length of characters {{ limit }}'
- Regex: pattern: ‘/^[_a-zA-ZÖöÄäÜüß0-9+./\-\ ]*$/' message: ‘SKU: Only uppercase and lowercase latin letters, figures, underscore, space, hyphen, plus, slashes and dot allowed’
- mid:
- Type: type: string message: ‘MID: Only {{ type }} value is allowed’
- Length: max: 13 maxMessage: ‘MID exceeds max allowed length of characters {{ limit }}'
- Regex: pattern: ‘/^(?:[a-z]{3}\d{10})*$/i’ message: ‘Wrong MID value format’
- mpn:
- Type: type: string message: ‘MPN: Only {{ type }} value is allowed’
- Length: max: 100 maxMessage: ‘MPN exceeds max allowed length of characters {{ limit }}'
- Regex: pattern: ‘/^[a-zA-Z0-9_- \t\n.,+\\/]*$/' message: ‘Wrong MPN value format’
- manufacturer:
- Type: type: string message: ‘Manufacturer: Only {{ type }} value is allowed’
- Length: max: 100 maxMessage: ‘Manufacturer exceeds max allowed length of characters {{ limit }}'
- destination:
- NotBlank: message: ‘Destination: Field is required’
- Type: type: string message: ‘Destination: Only {{ type }} value is allowed’
- Regex: message: ‘Destination: wrong value format’
- origin:
- NotBlank: message: ‘Origin: Field is required’
- Type: type: string message: ‘Origin: Only {{ type }} value is allowed’
- Regex: message: ‘Origin: wrong value format’