Offer Data

Link to swagger documentation

  1. Introduction
  2. How to create an offer?
  3. How to delete an offer?
  4. How to check your offers?
  5. Dictionary Fees
  6. SKU and Stock
  7. Error handling in POST /openapi/v2/offers
  8. 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 new POST request with the same product data and updating quantity field.
  • If netPrice or businessModel or netVolumePrices 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. The netPrice 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’