<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Spryker Documentation</title>
        <description>Spryker documentation center.</description>
        <link>https://docs.spryker.com/</link>
        <atom:link href="https://docs.spryker.com/feed.xml" rel="self" type="application/rss+xml"/>
        <lastBuildDate>Tue, 15 Sep 2026 14:54:12 +0000</lastBuildDate>
        <generator>Jekyll v4.2.2</generator>
        
        
        <item>
            <title>Create discounts</title>
            <description>&lt;p&gt;This document describes how to create discounts in the Back Office.&lt;/p&gt;
&lt;h2 id=&quot;prerequisites&quot;&gt;Prerequisites&lt;/h2&gt;
&lt;p&gt;If you are new to discounts, you might want to start with &lt;a href=&quot;/docs/pbc/all/discount-management/latest/base-shop/manage-in-the-back-office/best-practices-promote-products-with-discounts.html&quot;&gt;Best practices: Promote products with discounts&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;There is a &lt;a href=&quot;#reference-information-define-general-settings-of-the-discount&quot;&gt;reference information&lt;/a&gt; for each section. Review it before you start, or look up the necessary information as you go through the process.&lt;/p&gt;
&lt;p&gt;To create a discount, do the following:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;Merchandising &lt;span aria-label=&quot;and then&quot;&gt;&amp;gt;&lt;/span&gt; Discount&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;On the &lt;strong&gt;Discount&lt;/strong&gt; page, click &lt;strong&gt;Create new discount&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;define-general-settings-of-the-discount&quot;&gt;1. Define general settings of the discount&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;On the &lt;strong&gt;Create Discount&lt;/strong&gt; page, click the &lt;strong&gt;General information&lt;/strong&gt; tab.&lt;/li&gt;
&lt;li&gt;For &lt;strong&gt;STORE RELATION&lt;/strong&gt;, select one or more stores you want the discount to be displayed in.&lt;/li&gt;
&lt;li&gt;Select a &lt;strong&gt;DISCOUNT TYPE&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Enter a &lt;strong&gt;NAME&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Optional: Enter a &lt;strong&gt;DESCRIPTION&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Optional: Enter a &lt;strong&gt;PRIORITY&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Optional: To make the discount exclusive, select &lt;strong&gt;EXCLUSIVE&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Select a &lt;strong&gt;VALID FROM&lt;/strong&gt; date.&lt;/li&gt;
&lt;li&gt;Select a &lt;strong&gt;VALID TO&lt;/strong&gt; date.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Next&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;define-discount-calculation-and-the-products-to-apply-the-discount-to&quot;&gt;2. Define discount calculation and the products to apply the discount to&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Click the &lt;strong&gt;Discount calculation&lt;/strong&gt; tab.&lt;/li&gt;
&lt;li&gt;Select a &lt;strong&gt;CALCULATOR TYPE&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Based on the calculator type you’ve selected, do one of the following:
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Fixed amount&lt;/strong&gt;: Enter the needed discounted prices.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Percentage&lt;/strong&gt;: For &lt;strong&gt;VALUE&lt;/strong&gt;, enter a percentage to be discounted.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Select a &lt;strong&gt;DISCOUNT APPLICATION TYPE&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Depending on the discount application type you’ve selected, do the following:
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;QUERY STRING&lt;/strong&gt;: Add a query using the query builder or by entering a plain query.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PROMOTIONAL PRODUCT&lt;/strong&gt;:
&lt;ol&gt;
&lt;li&gt;Enter &lt;strong&gt;ABSTRACT PRODUCT SKU&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Enter a &lt;strong&gt;QUANTITY&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Next&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;define-on-what-conditions-the-discount-can-be-applied&quot;&gt;3. Define on what conditions the discount can be applied&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Click the &lt;strong&gt;Conditions&lt;/strong&gt; tab.&lt;/li&gt;
&lt;li&gt;For &lt;strong&gt;APPLY WHEN&lt;/strong&gt;, add a query using the query builder or by entering a plain query.&lt;/li&gt;
&lt;li&gt;For &lt;strong&gt;THE DISCOUNT CAN BE APPLIED IF THE QUERY APPLIES FOR AT LEAST X ITEM(S).&lt;/strong&gt;, enter a number.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Save&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This refreshes the page with a success message displayed.&lt;/p&gt;
&lt;h2 id=&quot;generate-voucher-codes&quot;&gt;4. Generate voucher codes&lt;/h2&gt;
&lt;p&gt;If you are creating a cart rule, proceed to step &lt;a href=&quot;#activate-the-discount&quot;&gt;5. Activate the discount&lt;/a&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Click the &lt;strong&gt;Voucher codes&lt;/strong&gt; tab.&lt;/li&gt;
&lt;li&gt;Enter a &lt;strong&gt;QUANTITY&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Optional: Enter a &lt;strong&gt;CUSTOM CODE&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Optional: For &lt;strong&gt;ADD RANDOM GENERATED CODE LENGTH&lt;/strong&gt;, select a number.&lt;/li&gt;
&lt;li&gt;Enter a &lt;strong&gt;MAX NUMBER OF USES&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Generate&lt;/strong&gt;.
This refreshes the page with a success message displayed. The created voucher codes are displayed in the &lt;strong&gt;Generated Discount Codes&lt;/strong&gt; section.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;activate-the-discount&quot;&gt;5. Activate the discount&lt;/h2&gt;
&lt;p&gt;Optional: To make the discount redeemable on the Storefront, click &lt;strong&gt;Activate&lt;/strong&gt; in the top-right corner.&lt;/p&gt;
&lt;p&gt;This refreshes the page with a success message displayed.&lt;/p&gt;
&lt;h2 id=&quot;reference-information-define-general-settings-of-the-discount&quot;&gt;Reference information: Define general settings of the discount&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ATTRIBUTE&lt;/th&gt;
&lt;th&gt;DESCRIPTION&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;STORE RELATION&lt;/td&gt;
&lt;td&gt;The stores to display the discount in.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DISCOUNT TYPE&lt;/td&gt;
&lt;td&gt;Defines how the discount is applied: &lt;ul&gt;&lt;li&gt;&lt;strong&gt;Cart rule&lt;/strong&gt;: the discount is applied automatically to the products defined in &lt;a href=&quot;#define-discount-calculation-and-the-products-to-apply-the-discount-to&quot;&gt;Define discount calculation and the products to apply the discount to&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Voucher codes&lt;/strong&gt;: the discount is applied to the products defined in &lt;a href=&quot;#define-discount-calculation-and-the-products-to-apply-the-discount-to&quot;&gt;Define discount calculation and the products to apply the discount to&lt;/a&gt; when a customer enters a voucher code generated in &lt;a href=&quot;#generate-voucher-codes&quot;&gt;Generate voucher codes&lt;/a&gt;.&lt;/li&gt;&lt;/ul&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NAME&lt;/td&gt;
&lt;td&gt;The unique ID of the discount that is displayed in the Back Office and on the Storefront.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DESCRIPTION&lt;/td&gt;
&lt;td&gt;The description is displayed only in the Back Office.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PRIORITY&lt;/td&gt;
&lt;td&gt;Defines the order of discounts being applied. Accepts integers from &lt;code&gt;1&lt;/code&gt; to &lt;code&gt;9999&lt;/code&gt; with &lt;code&gt;1&lt;/code&gt; being the highest priority.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NON-EXCLUSIVE&lt;/td&gt;
&lt;td&gt;Defines that this discount can be applied to a cart together with other non-exclusive discounts.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EXCLUSIVE&lt;/td&gt;
&lt;td&gt;Defines that this discount, when applied to a cart, discards all non-exclusive discounts. If multiple exclusive discounts are applied to a cart, only the discount with the bigger discount value is applied.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VALID FROM and VALID TO&lt;/td&gt;
&lt;td&gt;Inclusively, define the dates between which the discount can be applied.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&quot;reference-information-define-discount-calculation-and-the-products-to-apply-the-discount-to&quot;&gt;Reference information: Define discount calculation and the products to apply the discount to&lt;/h2&gt;
&lt;p&gt;This section contains information for defining discount calculation and the product to apply the discount to.&lt;/p&gt;
&lt;h3 id=&quot;calculator-type&quot;&gt;CALCULATOR TYPE&lt;/h3&gt;
&lt;p&gt;The discount can be calculated in two ways:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Percentage&lt;/strong&gt;: The discount is calculated as a percentage of the discounted items.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fixed amount&lt;/strong&gt;: A fixed amount is discounted.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;PRODUCT PRICE&lt;/th&gt;
&lt;th&gt;CALCULATOR TYPE&lt;/th&gt;
&lt;th&gt;AMOUNT&lt;/th&gt;
&lt;th&gt;DISCOUNT APPLIED&lt;/th&gt;
&lt;th&gt;PRICE TO PAY&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;50 €&lt;/td&gt;
&lt;td&gt;Percentage&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;5 €&lt;/td&gt;
&lt;td&gt;45 €&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50 €&lt;/td&gt;
&lt;td&gt;Fixed amount&lt;/td&gt;
&lt;td&gt;10 €&lt;/td&gt;
&lt;td&gt;10 €&lt;/td&gt;
&lt;td&gt;40 €&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;discount-application-type-query-string&quot;&gt;DISCOUNT APPLICATION TYPE: QUERY STRING&lt;/h3&gt;
&lt;p&gt;A query string defines what products a discount applies to. A query string consists of decision rules. Only the products that fulfill all the decision rules are discountable. You can define a query string by entering a plain query or by using a query builder.&lt;/p&gt;
&lt;p&gt;Query builder:
&lt;img src=&quot;https://spryker.s3.eu-central-1.amazonaws.com/docs/pbc/all/discount-management/latest/marketplace/marketplace-promotions-discounts-feature-overview/image-1788795781771.png&quot; alt=&quot;Discount calculation tab showing the discount application type options and an Apply to query&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Plain query:
&lt;img src=&quot;https://spryker.s3.eu-central-1.amazonaws.com/docs/User+Guides/Back+Office+User+Guides/Discount/Discount+Calculation:+Reference+Information/discount-calculation-plain-query.png&quot; alt=&quot;Discount_Calculation_Plain Query&quot; /&gt;&lt;/p&gt;
&lt;p&gt;A decision rule consists of the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Attribute. For example, &lt;em&gt;attribute.color&lt;/em&gt;. Attribute fields are named &lt;code&gt;attribute.&amp;lt;attribute_key&amp;gt;&lt;/code&gt; and are generated from the product attribute keys that exist in your database. If an attribute key does not exist in your database, the corresponding field is not offered. To add attribute keys, see &lt;a href=&quot;/docs/pbc/all/product-information-management/latest/base-shop/manage-in-the-back-office/attributes/create-product-attributes.html&quot;&gt;Create product attributes&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Relation operator. For example, &lt;em&gt;equal&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Value. For example, &lt;em&gt;black&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can find query examples in the following table.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;PLAIN QUERY&lt;/th&gt;
&lt;th&gt;EXPLANATION&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;day-of-week = ‘1’&lt;/td&gt;
&lt;td&gt;Discount applies to any orders that are placed on Monday.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;shipment-carrier != ‘1’ AND price-mode = ‘GROSS_MODE’&lt;/td&gt;
&lt;td&gt;Discount applies if the shipment carrier with the identifier &lt;code&gt;1&lt;/code&gt; is not chosen, and if gross pricing is selected.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;currency != ‘EUR’ OR price-mode = ‘GROSS_MODE’&lt;/td&gt;
&lt;td&gt;Discount applies if the selected currency is not the Euro, or if the pricing mode is gross.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;discount-application-type-promotional-product&quot;&gt;DISCOUNT APPLICATION TYPE: PROMOTIONAL PRODUCT&lt;/h3&gt;
&lt;p&gt;The promotional product lets you select a product that is to be displayed in a customer’s cart with a discount. The &lt;strong&gt;ABSTRACT PRODUCT SKU&lt;/strong&gt; defines the discounted product, and &lt;strong&gt;QUANTITY&lt;/strong&gt; defines how many products a customer can buy with a discount.
&lt;img src=&quot;https://spryker.s3.eu-central-1.amazonaws.com/docs/User+Guides/Back+Office+User+Guides/Discount/Discount+Calculation:+Reference+Information/Application+type.png&quot; alt=&quot;Application type&quot; /&gt;&lt;/p&gt;
&lt;p&gt;To give away a promotional product for free, select percentage calculator type and enter 100 percents.&lt;/p&gt;
&lt;h2 id=&quot;reference-information-define-on-what-conditions-the-discount-can-be-applied&quot;&gt;Reference information: Define on what conditions the discount can be applied&lt;/h2&gt;
&lt;p&gt;Similarly to &lt;a href=&quot;#discount-application-type-query-string&quot;&gt;defining discounted products&lt;/a&gt;, the conditions on which a discount is applied are defined using a query string. However, the two settings serve different purposes:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;SETTING&lt;/th&gt;
&lt;th&gt;TAB&lt;/th&gt;
&lt;th&gt;PURPOSE&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;APPLY TO&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Discount calculation&lt;/td&gt;
&lt;td&gt;Selects the cart items that receive the discount.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;APPLY WHEN&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Conditions&lt;/td&gt;
&lt;td&gt;Determines whether the discount applies to the cart at all.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;APPLY WHEN&lt;/strong&gt; acts as a gate: if its decision rules are not fulfilled, the discount is not applied. &lt;strong&gt;APPLY TO&lt;/strong&gt; then selects which of the items in the cart the discount value is distributed across. Because of this, &lt;strong&gt;APPLY WHEN&lt;/strong&gt; also offers cart-level and customer-level fields, such as &lt;code&gt;grand-total&lt;/code&gt;, &lt;code&gt;total-quantity&lt;/code&gt;, and &lt;code&gt;customer-group&lt;/code&gt;, while &lt;strong&gt;APPLY TO&lt;/strong&gt; offers only item-level fields, such as &lt;code&gt;sku&lt;/code&gt;, &lt;code&gt;item-price&lt;/code&gt;, and &lt;code&gt;attribute.&amp;lt;attribute_key&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The fields that appear in both places have a different meaning in each. For example, &lt;code&gt;sku&lt;/code&gt; under &lt;strong&gt;APPLY WHEN&lt;/strong&gt; checks whether the cart contains an item with a given SKU, while &lt;code&gt;sku&lt;/code&gt; under &lt;strong&gt;APPLY TO&lt;/strong&gt; restricts the discount to the items with that SKU.&lt;/p&gt;
&lt;p&gt;Example: The discount is applied if five or more items are in the cart, and if it’s Tuesday or Wednesday.
&lt;img src=&quot;https://spryker.s3.eu-central-1.amazonaws.com/docs/User+Guides/Back+Office+User+Guides/Discount/Discount+Conditions:+Reference+Information/discount-condition.png&quot; alt=&quot;Discount Condition&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;THE DISCOUNT CAN BE APPLIED IF THE QUERY APPLIES FOR AT LEAST X ITEM(S).&lt;/strong&gt; defines a minimum number of items that must fulfill the query for the discount to be applies. By default, the minimum order amount value is 1. It means that the discount is applied if there is one item in a cart the fulfills the query.&lt;/p&gt;
&lt;p&gt;Example: The discount is applied if 4 or more items with an  Intel Core processor are in the cart.
&lt;img src=&quot;https://spryker.s3.eu-central-1.amazonaws.com/docs/User+Guides/Back+Office+User+Guides/Discount/Discount+Conditions:+Reference+Information/threshold.png&quot; alt=&quot;Threshold&quot; /&gt;&lt;/p&gt;
&lt;h2 id=&quot;reference-information-generate-voucher-codes&quot;&gt;Reference information: Generate voucher codes&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ATTRIBUTE&lt;/th&gt;
&lt;th&gt;DESCRIPTION&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;QUANTITY&lt;/td&gt;
&lt;td&gt;The number of vouchers to generate.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CUSTOM CODE&lt;/td&gt;
&lt;td&gt;Any custom symbols you want the voucher codes to contain. If you are adding a single custom code, for &lt;strong&gt;ADD RANDOM GENERATED CODE LENGTH&lt;/strong&gt;, select &lt;strong&gt;No additional random characters&lt;/strong&gt;. If you add random characters, they are by default appended to the end of the custom code. To specify where to place the random characters, add &lt;code&gt;[code]&lt;/code&gt; to the custom code. For example, &lt;code&gt;black[code]friday&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ADD RANDOM GENERATED CODE LENGTH&lt;/td&gt;
&lt;td&gt;A number of random alphanumeric symbols to add to the code. If you entered a &lt;strong&gt;CUSTOM CODE&lt;/strong&gt;, the random characters are appended to the end of it. If you are generating more than one code, you must select &lt;strong&gt;3&lt;/strong&gt; or more.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MAX NUMBER OF USES&lt;/td&gt;
&lt;td&gt;Defines the maximum number of times a voucher code can be redeemed.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;section class=&apos;info-block &apos;&gt;&lt;i class=&apos;info-block__icon icon-info&apos;&gt;&lt;/i&gt;&lt;div class=&apos;info-block__content&apos;&gt;&lt;div class=&quot;info-block__title&quot;&gt;Voucher code collections&lt;/div&gt;
&lt;p&gt;The voucher codes of a discount belong to a voucher code collection. One customer may only redeem one voucher code per collection per cart. At the same time, a customer can redeem two vouchers in one cart if they belong to different voucher code collections.&lt;/p&gt;
&lt;/div&gt;&lt;/section&gt;
&lt;h2 id=&quot;decision-rules-attributes-and-operators&quot;&gt;Decision rules: Attributes and operators&lt;/h2&gt;
&lt;p&gt;This section describes complex decision rule attributes:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ATTRIBUTE&lt;/th&gt;
&lt;th&gt;VALUE TYPE&lt;/th&gt;
&lt;th&gt;DESCRIPTION&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;calendar-week&lt;/td&gt;
&lt;td&gt;Number&lt;/td&gt;
&lt;td&gt;The number of a week in a year: 1-52.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;grand-total&lt;/td&gt;
&lt;td&gt;Decimal number&lt;/td&gt;
&lt;td&gt;The sum of all totals.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sub-total&lt;/td&gt;
&lt;td&gt;Decimal number&lt;/td&gt;
&lt;td&gt;The sum of item prices without shipment expenses and discounts.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;item-price&lt;/td&gt;
&lt;td&gt;Decimal number&lt;/td&gt;
&lt;td&gt;The price of one item.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;item-quantity&lt;/td&gt;
&lt;td&gt;Number&lt;/td&gt;
&lt;td&gt;The number of items.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;month&lt;/td&gt;
&lt;td&gt;Number&lt;/td&gt;
&lt;td&gt;The month of the year: 1-12.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;time&lt;/td&gt;
&lt;td&gt;hour:minute&lt;/td&gt;
&lt;td&gt;The time of the day.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;total-quantity&lt;/td&gt;
&lt;td&gt;Number&lt;/td&gt;
&lt;td&gt;The total cart quantity.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;attribute.*&lt;/td&gt;
&lt;td&gt;String, number, list&lt;/td&gt;
&lt;td&gt;One field per product attribute key in your database, named &lt;code&gt;attribute.&amp;lt;attribute_key&amp;gt;&lt;/code&gt;. If no attribute keys exist, no attribute fields are available.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;customer-group&lt;/td&gt;
&lt;td&gt;String&lt;/td&gt;
&lt;td&gt;Any value, use a customer group name for an exact match.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;customer-order-count&lt;/td&gt;
&lt;td&gt;Number&lt;/td&gt;
&lt;td&gt;Any value. Checked against the number of placed orders in a customer account.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;product-offer-reference&lt;/td&gt;
&lt;td&gt;String&lt;/td&gt;
&lt;td&gt;Unique identifier of a &lt;a href=&quot;/docs/pbc/all/offer-management/latest/marketplace/marketplace-product-offer-feature-overview.html&quot;&gt;product offer&lt;/a&gt;. Marketplace only.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;RELATION OPERATOR&lt;/th&gt;
&lt;th&gt;IN PLAIN QUERY&lt;/th&gt;
&lt;th&gt;VALUE TYPE&lt;/th&gt;
&lt;th&gt;DESCRIPTION&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Contains&lt;/td&gt;
&lt;td&gt;CONTAINS&lt;/td&gt;
&lt;td&gt;String, Number&lt;/td&gt;
&lt;td&gt;Checks if the value is contained in the field.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Doesn’t contain&lt;/td&gt;
&lt;td&gt;DOES NOT CONTAIN&lt;/td&gt;
&lt;td&gt;String, Number&lt;/td&gt;
&lt;td&gt;Checks if the value is not contained in the field.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Equal&lt;/td&gt;
&lt;td&gt;=&lt;/td&gt;
&lt;td&gt;String, Number&lt;/td&gt;
&lt;td&gt;Checks if the value is equal to the value of the right operand&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Not Equal&lt;/td&gt;
&lt;td&gt;!=&lt;/td&gt;
&lt;td&gt;String, Number&lt;/td&gt;
&lt;td&gt;Checks if the value is not equal to the value of the right operand&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;In&lt;/td&gt;
&lt;td&gt;IS IN&lt;/td&gt;
&lt;td&gt;List&lt;/td&gt;
&lt;td&gt;Values need to be semicolon-separated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Not In&lt;/td&gt;
&lt;td&gt;IS NOT IN&lt;/td&gt;
&lt;td&gt;List&lt;/td&gt;
&lt;td&gt;Values need to be semicolon-separated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Less&lt;/td&gt;
&lt;td&gt;&amp;lt;&lt;/td&gt;
&lt;td&gt;Number&lt;/td&gt;
&lt;td&gt;Checks if the value is less than the value of the right operand&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Less or equal&lt;/td&gt;
&lt;td&gt;&amp;lt;=&lt;/td&gt;
&lt;td&gt;Number&lt;/td&gt;
&lt;td&gt;Checks if the value is less than or equal to the value of the right operand&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Greater&lt;/td&gt;
&lt;td&gt;&amp;gt;&lt;/td&gt;
&lt;td&gt;Number&lt;/td&gt;
&lt;td&gt;Checks if the value is greater than the value of the right operand&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Greater or equal&lt;/td&gt;
&lt;td&gt;&amp;gt;=&lt;/td&gt;
&lt;td&gt;Number&lt;/td&gt;
&lt;td&gt;Checks if the value is greater than or equal to the value of the right operand&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The operators offered for a field depend on the value types that the field accepts, so not every field offers all of the operators above. For example:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;FIELD&lt;/th&gt;
&lt;th&gt;OPERATORS OFFERED&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;attribute.&amp;lt;attribute_key&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Equal, Not equal, Contains, Doesn’t contain, In, Not in, Less, Less or equal, Greater, Greater or equal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sku&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Equal, Not equal, Contains, Doesn’t contain, In, Not in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;item-price&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Equal, Not equal, Contains, Doesn’t contain, Less, Less or equal, Greater, Greater or equal&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;section class=&apos;info-block &apos;&gt;&lt;i class=&apos;info-block__icon icon-info&apos;&gt;&lt;/i&gt;&lt;div class=&apos;info-block__content&apos;&gt;&lt;div class=&quot;info-block__title&quot;&gt;Missing operators&lt;/div&gt;
&lt;p&gt;If a field offers fewer operators than you expect, the field accepts fewer value types. Attribute fields accept strings, numbers, and lists, so they offer all of the operators. A field that accepts only lists offers &lt;strong&gt;In&lt;/strong&gt; and &lt;strong&gt;Not in&lt;/strong&gt; only. Operators that are costly to evaluate for a value type are left out for performance reasons.&lt;/p&gt;
&lt;/div&gt;&lt;/section&gt;
</description>
            <pubDate>Tue, 15 Sep 2026 14:49:42 +0000</pubDate>
            <link>https://docs.spryker.com/docs/pbc/all/discount-management/latest/base-shop/manage-in-the-back-office/create-discounts.html</link>
            <guid isPermaLink="true">https://docs.spryker.com/docs/pbc/all/discount-management/latest/base-shop/manage-in-the-back-office/create-discounts.html</guid>
            
            
        </item>
        
        <item>
            <title>Promotions &amp; Discounts feature overview</title>
            <description>The *Discount Management* feature lets shop owners provide free value to their customers by discounting the percentage or fixed sum of an order&apos;s subtotal, or an item&apos;s price on predefined conditions.

## Discount types

There are two discount types:
- Voucher
- Cart rule

A Back Office user selects a discount type when [creating a discount](/docs/pbc/all/discount-management/latest/base-shop/manage-in-the-back-office/create-discounts.html).

## Voucher

A *voucher* is a discount that applies when a customer enters an active voucher code on the **Cart** page.

![Cart voucher](https://spryker.s3.eu-central-1.amazonaws.com/docs/Features/Promotions+&amp;+Discounts/Discount/Discount+Feature+Overview/cart_voucher.png)

Once the customer clicks **Redeem code**, the page refreshes to show the discount name, discount value, and available actions: **Remove** and **Clear all**. The **Clear all** action disables all the applied discounts. The **Remove** action disables a single discount.
![Cart voucher applied](https://spryker.s3.eu-central-1.amazonaws.com/docs/Features/Promotions+&amp;+Discounts/Discount/Discount+Feature+Overview/cart_voucher_applied.png)

You can generate multiple voucher codes for a single voucher. The code has a **Max number of uses** value which defines how many times the code can be redeemed.

You can enter codes manually or use the code generator in the Back Office.

![Generate codes](https://spryker.s3.eu-central-1.amazonaws.com/docs/Features/Promotions+%26+Discounts/Discount/Discount+Feature+Overview/generate_codes.png)

To learn how a Back Office user can create a voucher in the Back Office, see [Create discounts](/docs/pbc/all/discount-management/latest/base-shop/manage-in-the-back-office/create-discounts.html)

## Cart rule

A *cart rule* is a discount that applies to a cart once all the [decision rules](#decision-rule) linked to the cart rule are fulfilled.

A cart rule is applied automatically once its conditions are met. If the decision rules of a discount are fulfilled, the customer can see the discount upon entering the cart. Unlike with [vouchers](#voucher), the **Clear all** and **Remove** actions are not displayed for cart rules.

![Cart rule](https://spryker.s3.eu-central-1.amazonaws.com/docs/Features/Promotions+&amp;+Discounts/Discount/Discount+Feature+Overview/cart-cart-rule.png)

To learn how a Back Office user can create a cart rule in the Back Office, see [Create discounts](/docs/pbc/all/discount-management/latest/base-shop/manage-in-the-back-office/create-discounts.html)

### Decision rule

In the Back Office, you define discounts by building a query. A query consists of one or more rules, and each rule compares a parameter to a value using a relation operator. Such a rule is called a *decision rule*.

A query serves one of two purposes, depending on where you build it:

- Under **Apply when**, the query is a condition: it determines whether the discount applies to the cart at all.
- Under **Apply to**, the query is a filter: it selects which items in the cart receive the discount.

The Back Office does not use the term *decision rule*. You build queries in the **Apply when** and **Apply to** fields, which are described in [Apply to compared to Apply when](#apply-to-compared-to-apply-when).

A query can contain one or more rules. Find an example combination below:

| PARAMETER | RELATION OPERATOR | VALUE |
| --- | --- | --- |
| total-quantity | equal |  3 |
| day-of-week | equal | 5  |

In this case, the discount is applied if the cart contains three items and the purchase is made on the fifth day of the week (Friday).

Multiple rules form a single query. A query is a request for information based on the defined parameters. In the Discount Management capability, a query requests information from a cart, either to check whether the cart is eligible for the discount or to select the items to discount. By specifying rules, you define the parameters of the query.

In the Back Office, you create the rules in a Query Builder. Query Builder transforms the rules into a single query.

The rules from the previous example look as follows in the Query Builder:

![Query Builder showing the rules total-quantity equal 3 and day-of-week equal 5 combined with AND](https://spryker.s3.eu-central-1.amazonaws.com/docs/pbc/all/discount-management/latest/marketplace/marketplace-promotions-discounts-feature-overview/image-1788795776069.png)

A Back Office user can enter a query manually as well.

The same rules look as follows as a plain query:

`total-quantity = &apos;3&apos; AND day-of-week = &apos;5&apos;`

You can switch between Query Builder and Plain query modes to see how the specified rules look in either of them.  

Decision rules are combined with *AND* and *OR*  combination operators. With the AND operator, all the rules must be fulfilled for the discount to be applied. With the OR operator, at least one must be fulfilled for the discount to be applied.

In the following example, for the discount to apply the cart must contain three items, and the purchase must be made on Wednesday.

![AND operator](https://spryker.s3.eu-central-1.amazonaws.com/docs/Features/Promotions+%26+Discounts/Discount/Discount+Feature+Overview/and-operator.png)

In the following example, for the discount to apply, either the cart must contain three items, or the purchase must be made on Wednesday.

![OR operator](https://spryker.s3.eu-central-1.amazonaws.com/docs/Features/Promotions+%26+Discounts/Discount/Discount+Feature+Overview/or-operator.png)

{% info_block infoBox &quot;Info&quot; %}

When rules are combined by the OR operator, they do not exclude each other. If a cart fulfills both such rules, the discount is still applied.

{% endinfo_block %}

#### Product attributes in decision rules

To use a product attribute in a decision rule, reference it as `attribute.&lt;attribute_key&gt;` — for example, `attribute.color`. Spryker does not predefine these fields: it generates one field per product attribute key that exists in your database.

{% info_block warningBox &quot;Add attribute keys before you use them&quot; %}

If you do not see the attribute you expect in the Query Builder, the corresponding attribute key does not exist in your database. Add it in one of the following ways:

- [Create product attributes](/docs/pbc/all/product-information-management/latest/base-shop/manage-in-the-back-office/attributes/create-product-attributes.html) in the Back Office.
- Import attribute keys with the `product-attribute-key` data entity. For details, see [Import file details: product_attribute_key.csv](/docs/pbc/all/product-information-management/latest/base-shop/import-and-export-data/products-data-import/import-file-details-product-attribute-key.csv.html).

{% endinfo_block %}

The examples in this document use attribute keys such as `attribute.color` and `attribute.processor_cores`. Depending on the demo shop you started with, these keys may not exist in your database. Replace them with the attribute keys that exist in your own database.

#### Decision rule group

A *rule group* is a separate set of rules with its own combination operator.

![Decision rule group](https://spryker.s3.eu-central-1.amazonaws.com/docs/Features/Promotions+%26+Discounts/Discount/Discount+Feature+Overview/decision-rule-group.png)

With the rule groups, you can build multiple levels of rule hierarchy. When a cart is evaluated against the rules, it&apos;s evaluated on all levels of the hierarchy. At each level, there can be both rules and rule groups.

![Decision rule hierarchy](https://spryker.s3.eu-central-1.amazonaws.com/docs/Features/Promotions+%26+Discounts/Discount/Discount+Feature+Overview/decision-rule-hierarchy.png)

When a cart is evaluated on a level that has both a rule and a rule group, the rule group is treated as a single rule. The following diagram shows how a cart is evaluated against the rules from the previous screenshot.

### Discount threshold

A *threshold* is the minimum number of items in a cart that must fulfill all the specified decision rules for the discount to apply.
The default value is *1*. This means that a discount is applied if at least one item fulfills the discount&apos;s decision rules.

In the following example, the discount is applied if there are four items with an Intel Core processor in the cart.

![Threshold](https://spryker.s3.eu-central-1.amazonaws.com/docs/Features/Promotions+&amp;+Discounts/Discount/Discount+Feature+Overview/threshold.png)

## Discount application

*Discount application* is a discount configuration option that defines the products to which a discount is applied.

There are two types of the discount application:
- Query string
- Promotional product

### Apply to compared to Apply when

A discount is configured in two separate places that serve different purposes:

| CONFIGURATION | BACK OFFICE TAB | SECTION HEADING | PURPOSE |
| --- | --- | --- | --- |
| **APPLY TO** | Discount calculation | Define what to apply the discount to | Selects the cart items that receive the discount. |
| **APPLY WHEN** | Conditions | Define when to apply the discount | Determines whether the discount applies to the cart at all. |

*Apply when* acts as a gate: if its decision rules are not fulfilled, the discount is not applied to the cart. *Apply to* then selects which of the items in the cart the discount value is distributed across.

Because the two settings serve different purposes, they offer different fields. *Apply when* evaluates the cart as a whole, so it also offers cart-level and customer-level fields, such as `grand-total`, `sub-total`, `total-quantity`, `currency`, `day-of-week`, and `customer-group`. *Apply to* selects individual items, so it offers only item-level fields, such as `sku`, `item-quantity`, `item-price`, `category`, and `attribute.&lt;attribute_key&gt;`.

Some fields, such as `sku`, `item-quantity`, and `item-price`, are available in both places, where they have a different meaning:

| FIELD | UNDER APPLY WHEN | UNDER APPLY TO |
| --- | --- | --- |
| `sku` | Checks whether the cart contains an item with this SKU. | Restricts the discount to the items with this SKU. |
| `item-quantity` | Checks whether an item with this quantity is in the cart. | Restricts the discount to the items with this quantity. |
| `item-price` | Checks whether an item with this price is in the cart. | Restricts the discount to the items with this price. |

The fields under *Apply to* do not discount an SKU or a quantity by themselves. They narrow down the set of items that the discount is applied to.

The following screenshots show the same rule in both places. The field list offered under *Apply to* is a subset of the one offered under *Apply when*, because *Apply to* only offers item-level fields. The attribute key in these screenshots comes from the demo data, so the key you see in your own project differs.

On the **Discount calculation** tab, *Apply to* selects the items that receive the discount:

![Apply to on the Discount calculation tab, with the rule attribute.farbe equal](https://spryker.s3.eu-central-1.amazonaws.com/docs/pbc/all/discount-management/latest/marketplace/marketplace-promotions-discounts-feature-overview/image-1788793722162.png)

On the **Conditions** tab, *Apply when* determines whether the discount applies to the cart at all:

![Apply when on the Conditions tab, with the rule attribute.farbe equal](https://spryker.s3.eu-central-1.amazonaws.com/docs/pbc/all/discount-management/latest/marketplace/marketplace-promotions-discounts-feature-overview/image-1788793729064.png)

{% info_block infoBox &quot;Example&quot; %}

A discount with *Apply when* set to `total-quantity &gt;= 10` and *Apply to* set to `attribute.color = &apos;white&apos;` first checks the total number of items in the cart, regardless of their color. If the cart contains at least ten items, the discount then reduces the price of the white items only.

{% endinfo_block %}


### Query string

A *query string* is a discount application type that uses [decision rules](#decision-rule) to dynamically determine which products qualify for discounts.

The discount in the example below applies to white color products.

![Query collection](https://spryker.s3.eu-central-1.amazonaws.com/docs/Features/Promotions+&amp;+Discounts/Discount/Discount+Feature+Overview/collection-query.png)

The product selection based on the query string is dynamic:
- If at some point the color attribute of a product changes from white to anything else, the product is no longer eligible for a discount.
- If at some point a product receives the white color attribute, it becomes eligible for a discount.

{% info_block infoBox &quot;Attribute keys in the examples&quot; %}

This example uses the `attribute.color` field. Attribute fields are generated from the product attribute keys in your own database, so replace `attribute.color` with an attribute key that exists in your database. For details, see [Product attributes in decision rules](#product-attributes-in-decision-rules).

{% endinfo_block %}

### Promotional product

*Promotional product* is a discount application type that discounts particular products at a set quantity, enabling &quot;buy X, get Y&quot; promotions.

When a customer meets conditions for a promotional product discount, the **Promotional products** section is displayed in the cart and lets customers add the available quantity of the discounted products. The section consists of the product name, SKU, original and discounted price, and a description of the discount.

![Promotional product](https://spryker.s3.eu-central-1.amazonaws.com/docs/scos/user/features/promotions-discounts-feature-overview.md/202200.0/promotional-product-storefront.png)
&lt;!--
old image: (https://spryker.s3.eu-central-1.amazonaws.com/docs/Features/Promotions+&amp;+Discounts/Discount/Discount+Feature+Overview/promotional-product-frontend.png)
--&gt;

A Back Office user selects promotional products by entering comma-separated abstract product SKUs in the Back Office. They also define the maximum quantity of the products to be sold with a discount. For example, there are 10 SKUs in **ABSTRACT PRODUCT SKU(S)**, and **MAXIMUM QUANTITY** is set to `1`. If a customer fulfills the discount conditions, they are eligible for one unit of any of the 10 promotional products, not one of each. Likewise, if **MAXIMUM QUANTITY** is set to `3`, they can select 3 units of any of the promotional products in any combination. The promotional products are automatically merchandised below the items in a customer&apos;s cart. The customer can add one of these products to the cart from this widget, and the discount will apply.

{% info_block infoBox &quot;Note&quot; %}

The promotional product discount only applies if the product is added to the cart from the **Promotional Products cart** widget. If the product is already in the cart before the discount conditions are met, the customer needs to first remove it, then re-add it to the widget.

{% endinfo_block %}

A Back Office user can either give away promotional products for free or provide a discount for these products by specifying the percentage value, or a fixed amount, to be discounted from their price. When giving a product for free, the percentage value must be 100%. Using a fixed amount discount for a free product is also possible, where the amount is equal to a product&apos;s price, but it&apos;s not recommended because of the possibility of price fluctuations and differences across multiple products.

![Collection - promotional product](https://spryker.s3.eu-central-1.amazonaws.com/docs/scos/user/features/promotions-discounts-feature-overview.md/202200.0/collection-promotional-product.png)

## Discount calculation types

Calculation defines the value to be deducted from a product&apos;s default price. There are two types of discount calculations:
- Percentage
- Fixed amount

{% info_block infoBox %}

With the fixed amount type, the currency of the respective shop is used for calculation.

{% endinfo_block %}

See examples in the table below.

| PRODUCT PRICE | CALCULATION TYPE | AMOUNT | DISCOUNT APPLIED | PRICE TO PAY |
| --- | --- | --- | --- | --- |
| €50 | Percentage | 10 | €5 | €45 |
| €50 | Fixed amount | 10 | €10 | €40 |

A Back Office user defines calculation when [creating discounts](/docs/pbc/all/discount-management/latest/base-shop/manage-in-the-back-office/create-discounts.html)

![Discount calculation](https://spryker.s3.eu-central-1.amazonaws.com/docs/Features/Promotions+&amp;+Discounts/Discount/Discount+Feature+Overview/discount_calculation.png)

## Discount exclusiveness

*Discount exclusiveness* defines if the discount value of a discount can be combined with the discount value of other discounts in a single order.

A Back Office user defines a calculation when [creating discounts](/docs/pbc/all/discount-management/latest/base-shop/manage-in-the-back-office/create-discounts.html)

![Exclusive discount](https://spryker.s3.eu-central-1.amazonaws.com/docs/scos/user/features/promotions-discounts-feature-overview.md/exclusivity.png)


### Exclusive discount

An *exclusive discount* is a discount that when applied to a cart, discards all the other discounts applied to it.

{% info_block infoBox &quot;Promotional products and query string&quot; %}

[Promotional product](#promotional-product) discounts and [query string](#query-string) discounts are separate when it comes to exclusivity. These types of discounts exclude all other discounts only among each other. Promotional product discounts are not affected by exclusive query string discounts and vice versa.

{% endinfo_block %}

If a cart is eligible for multiple exclusive discounts, you can [prioritize](#discount-priority) the discounts to define which of the exclusive discounts prevail over the others. For details on how a Back Office user can set priorities for discounts, see [Create discounts](/docs/pbc/all/discount-management/latest/base-shop/manage-in-the-back-office/create-discounts.html)

If the exclusive discounts are not prioritized or have the same priorities, the highest-value discount is applied. For details and examples on how the discounts are calculated, see [Discount calculation logic](#discount-calculation-logic).

### Non-exclusive discount

A *non-exclusive discount* is a discount that can be combined with other non-exclusive discounts in a single order. If all the discounts are non-exclusive, they are applied together. However, a Back Office user can [prioritize](#discount-priority) the non-exclusive discounts to set the order in which they apply. For details on how a Back Office user can prioritize a discount, see [Create discounts](/docs/pbc/all/discount-management/latest/base-shop/manage-in-the-back-office/create-discounts.html) For details and examples of how discounts are calculated based on their priorities, see [Discount calculation logic](#discount-calculation-logic).

## Discount validity interval

A *validity interval* is a period of time during which a discount is active and can be applied.

If a cart is eligible for a discount outside of its validity interval, the cart rule is not applied. If a customer enters a voucher code outside of its validity interval, they get a &quot;Your voucher code is invalid.&quot; message.

A Back Office user defines calculation when [creating a discount](/docs/pbc/all/discount-management/latest/base-shop/manage-in-the-back-office/create-discounts.html)

![Validity interval](https://spryker.s3.eu-central-1.amazonaws.com/docs/scos/user/features/promotions-discounts-feature-overview.md/validity-interval.png)


## Discount priority

In cases when several discounts can apply to a customer&apos;s order, a Back Office user can set the discount priorities. A *discount priority* is an integer value that defines the order in which the discounts are applied. For details on how discounts are calculated based on their priorities, see [Discount calculation logic](#discount-calculation-logic).

Discount prioritization behavior does not depend on the [discount type](#discount-types). For example, you might have the following priorities for different types:

- Cart rule discount with priority 1
- Voucher code discount with priority 2
- Voucher code discount with priority 3
- Cart rule discount with priority 4

In this case, discounts are applied based on their priority, regardless of type.

{% info_block infoBox &quot;Info&quot; %}

If you have a voucher code and a cart rule with the same priority, both discounts apply and use the same cart subtotal. For an example, see [Scenario 5: Non-exclusive, fixed amount and percentage discounts with the same priority values](#scenario-4-non-exclusive-fixed-amount-and-percentage-discounts-with-the-same-priority-values).

{% endinfo_block %}

## Discount calculation logic

The discount calculation logic follows these rules:

- All discounts are calculated sequentially based on their [priority value](#discount-priority). Thus, discounts with the highest priority value are considered first.
- Discounts without priority values are treated with the last possible priority.
- Discounts with the same priority value are calculated independently of each other.
- If there is a set of discounts with [exclusive](#exclusive-discount) and [non-exclusive](#non-exclusive-discount) discounts, the non-exclusive discounts are ignored. The remaining exclusive discounts are handled according to their priority values as shown in the schema:

![discounts-calculation-logic](https://confluence-connect.gliffy.net/embed/image/ad6c6c4c-9ccb-42ae-a9dc-5944300bdf91.png?utm_medium=live&amp;utm_source=custom)

&lt;!---source: https://spryker.atlassian.net/wiki/spaces/DOCS/pages/3019079947/CC-14560+-+Enable+prioritization+for+discounts --&gt;

### Example discount calculation scenarios

The following scenarios illustrate how the discount calculation logic works.

#### Scenario 1: Multiple non-exclusive discounts, the mix of fixed amount and percentage discount calculation types

Cart subtotal: €500

| DISCOUNT NAME|  DESCRIPTION|  DISCOUNT TYPE| DISCOUNT AMOUNT| DISCOUNT PRIORITY| EXCLUSIVENESS|
| --- | --- | --- | --- | --- |--- |
| HOCKEY10| 10% off your order| Percentage| 10%|  300|  No|
| HELMET20| Save €20 on helmets| Fixed amount| €20|  200|  No|
| STICK50| €50 off all carbon sticks| Fixed amount| €50|  500|  No|

**Discounts applied in priority order:**

1. HELMET20 (200 priority): €500 - €20 = €480
2. HOCKEY10 (300 priority): €480 - €480*0.10 = €432
3. STICK50 (500 priority): €432 - €50 = €382

**Calculation as displayed in cart:**

Subtotal: €500.00
HELMET20: -€20.00
HOCKEY10: -€48.00
STICK50: -€50.00

Grand total: €382.00

{% info_block infoBox &quot;Info&quot; %}

As customers are not aware of the logic behind discount prioritization, they may be confused about the assigned priorities. For the scenario above, they might wonder why they get 10% off the pre-discount price of the hockey stick and not the helmet. In cases like this one, instead of creating prioritization logic for separate discount types, consider adjusting the discount priority accordingly to avoid customer confusion.

{% endinfo_block %}

#### Scenario 2: Multiple non-exclusive discounts, percentage discount calculation types

Cart subtotal: $100

| DISCOUNT NAME| DESCRIPTION| DISCOUNT TYPE| DISCOUNT AMOUNT| DISCOUNT PRIORITY| EXCLUSIVENESS| NOTES|
| --- | --- | --- | --- | --- |--- |--- |
| BUY4GET1| Buy 4 baguettes, get one free| Percentage| 100%| 100|  No| One baguette costs $3|
| SPICE10| 10% off spices| Percentage| 10%| 100| No| Spices cost $30|
| MEMBER5| 5% off for members| Percentage| 5%| 5000| No| |
| STORE5| 5% off storewide if you spend $50 or more| Percentage| 5%| 5000|No| |

**Discounts applied in priority order:**

1. BUY4GET1 (100 priority): $100 - $3*1.00 or $3 = $97
2. SPICE10 (100 priority): $97 - $30*0.10 or $3.00 = $94
3. MEMBER5 (5000 priority): $94 - $94*0.05 or $4.70 = $89.30
4. STORE5 (5000 priority): $89.30 - $94*0.05 or $4.70 = $84.60

**Calculation as displayed in cart:**

Subtotal: $100.00

BUY5GET10: -$3.00
SPICE10: -$3.00
MEMBER5: -$4.70
STORE5: -$4.70

Grand total: $84.60

#### Scenario 3: Multiple discounts, both exclusive and non-exclusive

Cart subtotal: $100

| DISCOUNT NAME|  DESCRIPTION|  DISCOUNT TYPE| DISCOUNT AMOUNT| DISCOUNT PRIORITY| EXCLUSIVENESS| NOTES|
| --- | --- | --- | --- | --- |--- |--- |
| BUY4GET1| Buy 4 baguettes, get one free|  Percentage| 100%| 100|  No| One baguette costs $3|
| SPICE10| 10% off spices| Percentage| 10%|  100|  No| $30 of spices|
| MEMBER5| 5% off for members| Percentage| 5%| 5000| Yes| |
| STORE5| 5% off storewide if you spend $50 or more|  Percentage| 5%| 9000| Yes| |

**Discounts applied in priority order:**

MEMBER5 (5000): $100 - $100*0.05 = $95

In the presence of exclusive discounts, all non-exclusive discounts are excluded.
Between the exclusive discounts, the discount with the higher priority is chosen.

#### Scenario 4: Non-exclusive, fixed amount and percentage discounts with the same priority values

Cart subtotal: €100

| DISCOUNT NAME|  DESCRIPTION|  DISCOUNT TYPE| DISCOUNT AMOUNT| DISCOUNT PRIORITY| EXCLUSIVENESS| NOTES |
| --- | --- | --- | --- | --- |--- | --- |
| 10SOCKS|  10% off Nike socks| Percentage| 10%|  100|  No| Nike socks cost €40|
| 20PANTS|  Save €20 on all white pants|  Fixed amount| €20|100|  No| White pants cost €60|

**Discounts applied in priority order:**

1. 10SOCKS (100 priority): €100 - €40*0.1 = €96
2. 20PANTS (100 priority): €96 - €20 = €76


**Calculation as displayed in cart:**

Subtotal: €100.00

10SOCKS: -€4
20PANTS: -€20.00

Grand total: €76.00

#### Scenario 5: Exclusive and non-exclusive discounts without the priorities

Cart subtotal: €100

| DISCOUNT NAME  | DESCRIPTION| DISCOUNT TYPE | DISCOUNT AMOUNT | DISCOUNT PRIORITY|  EXCLUSIVENESS | NOTES |
| --- | --- | --- | --- | --- |--- |--- |
|10SOCKS | 10% off Nike socks | Percentage | 10% | |Exclusive | Nike socks cost €40 |
|5PANTS|Save €5 on all pants| Fixed amount | €5 | | Exclusive |  |
|SITE10|10% off everything in the store| Percentage | €10 | |  Non-exclusive | |

In the presence of exclusive discounts, all non-exclusive discounts are excluded.
Since the remaining exclusive discounts do not have priorities and the discount 5PANTS provides more free value than the discount 10SOCKS, this discount is applied.

**Calculation as displayed in cart:**

Subtotal: €100.00

15PANTS: -€5

Grand total: €95.00

&lt;!-- THIS SCENARIO IS NOT SUPPORTED YET. IT WILL BE SUPPORTED ONCE CC-15011 IS FIXED #### Scenario 6: Multiple non-exclusive, percentage discount calculation types. Executing discounts in priority order results in cart subtotal no longer satisfying conditions for one or more subsequent discounts.

Cart subtotal: €100

| DISCOUNT NAME|  DESCRIPTION|  DISCOUNT TYPE| DISCOUNT AMOUNT| DISCOUNT PRIORITY| EXCLUSIVENESS| NOTES|
| --- | --- | --- | --- | --- |--- |--- |
| 50SOCKS|  50% off Nike socks| Percentage| 50%|  100|  No| Nike socks cost €40|
| 15PANTS|  15% off all white pants|  Percentage| 15%|100|  No| White pants cost €60|
| SAVE20| 20% off purchases over €100|  Percentage| 20%|  300|  No| |
| SITE5|  5% off everything in the store| Percentage| 5%| 9999| No| |

**Discounts applied in priority order:**

1. 50SOCKS (100 priority): €100 - €40*0.5 = €80
2. 15PANTS (100 priority): €80 - €60*0.15 = €71
3. SAVE20 (300 priority): No longer applies, because discounts with higher priorities have reduced the subtotal below the discount condition threshold of €100.
4. SITE5: (9999 priority): €71 - €3.55 = €67.45
--&gt;

## Related Business User articles

|BACK OFFICE USER GUIDES|
|---|
| [Create a discount](/docs/pbc/all/discount-management/latest/base-shop/manage-in-the-back-office/create-discounts.html)  |
| [Edit discounts](/docs/pbc/all/discount-management/latest/base-shop/manage-in-the-back-office/edit-discounts.html)  |

## Related Developer articles

| INSTALLATION GUIDES  | UPGRADE GUIDES | GLUE API GUIDES | DATA IMPORT | TUTORIALS AND HOWTOS |
|---|---|---|---|---|
| [Integrate the Promotions &amp; Discounts feature](/docs/pbc/all/discount-management/latest/base-shop/install-and-upgrade/install-features/install-the-promotions-and-discounts-feature.html) | [Upgrade the Discount module](/docs/pbc/all/discount-management/latest/base-shop/install-and-upgrade/upgrade-modules/upgrade-the-discount-module.html) | [Add items with discounts to carts of registered users](/docs/pbc/all/discount-management/latest/base-shop/manage-using-glue-api/glue-api-add-items-with-discounts-to-carts-of-registered-users.html) | [File details: discount.csv](/docs/pbc/all/discount-management/latest/base-shop/import-and-export-data/import-file-details-discount.csv.html) | [HowTo: Create discounts based on shipment](/docs/pbc/all/discount-management/latest/base-shop/create-discounts-based-on-shipment.html) |
| [Integrate the Category Management + Promotions &amp; Discounts feature](/docs/pbc/all/discount-management/latest/base-shop/install-and-upgrade/install-features/install-the-promotions-and-discounts-category-management-feature.html) | [Upgrade the DiscountCalculatorConnector module](/docs/pbc/all/discount-management/latest/base-shop/install-and-upgrade/upgrade-modules/upgrade-the-discountcalculatorconnector-module.html) |  [Retrieve discounts in carts of registered users](/docs/pbc/all/discount-management/latest/base-shop/manage-using-glue-api/glue-api-retrieve-discounts-in-carts-of-registered-users.html)   | [&quot;Import file details: discount_amount.csv&quot;](/docs/pbc/all/discount-management/latest/base-shop/import-and-export-data/import-file-details-discount-amount.csv.html) | |
| [Integrate the Product labels + Promotions &amp; Discounts feature](/docs/pbc/all/discount-management/latest/base-shop/install-and-upgrade/install-features/install-the-promotions-and-discounts-product-labels-feature.html) | [Upgrade the DiscountPromotion module](/docs/pbc/all/discount-management/latest/base-shop/install-and-upgrade/upgrade-modules/upgrade-the-discountpromotion-module.html) |  [Manage discount vouchers in carts of registered users](/docs/pbc/all/discount-management/latest/base-shop/manage-using-glue-api/glue-api-manage-discount-vouchers-in-carts-of-registered-users.html) | [&quot;Import file details: discount_store.csv&quot;](/docs/pbc/all/discount-management/latest/base-shop/import-and-export-data/import-file-details-discount-store.csv.html) | |
| [Integrate the Promotions &amp; Discounts Glue API](/docs/pbc/all/discount-management/latest/base-shop/install-and-upgrade/install-features/install-the-promotions-and-discounts-glue-api.html) | [Upgrade the DiscountPromotionWidget module](/docs/pbc/all/discount-management/latest/base-shop/install-and-upgrade/upgrade-modules/upgrade-the-discountpromotionwidget-module.html) | [Add items with discounts to guest carts](/docs/pbc/all/discount-management/latest/base-shop/manage-using-glue-api/glue-api-add-items-with-discounts-to-guest-carts.html) | [&quot;Import file details: discount_voucher.csv&quot;](/docs/pbc/all/discount-management/latest/base-shop/import-and-export-data/import-file-details-discount-voucher.csv.html) |  |
|  | [Upgrade the DiscountSalesAggregatorConnector module](/docs/pbc/all/discount-management/latest/base-shop/install-and-upgrade/upgrade-modules/upgrade-the-discountsalesaggregatorconnector-module.html) | [Retrieve discounts in guest carts](/docs/pbc/all/discount-management/latest/base-shop/manage-using-glue-api/glue-api-retrieve-discounts-in-guest-carts.html) | | |
|  |  |  [Manage discount vouchers in guest carts](/docs/pbc/all/discount-management/latest/base-shop/manage-using-glue-api/glue-api-manage-discount-vouchers-in-guest-carts.html)   |  | |
|  |  | [Retrieve discounts in customer carts](/docs/pbc/all/discount-management/latest/base-shop/manage-using-glue-api/glue-api-retrieve-discounts-in-customer-carts.html) |  | |
</description>
            <pubDate>Tue, 15 Sep 2026 09:26:21 +0000</pubDate>
            <link>https://docs.spryker.com/docs/pbc/all/discount-management/latest/base-shop/promotions-discounts-feature-overview.html</link>
            <guid isPermaLink="true">https://docs.spryker.com/docs/pbc/all/discount-management/latest/base-shop/promotions-discounts-feature-overview.html</guid>
            
            
        </item>
        
        <item>
            <title>Marketplace Promotions &amp; Discounts feature overview</title>
            <description>The *Marketplace Promotions and Discounts* feature ensures that discounts are applied to orders.

There are two discount types:

- Voucher
- Cart rule

A product catalog manager selects a discount type when [creating a discount](/docs/pbc/all/discount-management/latest/base-shop/manage-in-the-back-office/create-discounts.html).


Based on the business logic, discounts can be applied in the following ways:

- The discount is applied to the whole Marketplace order. In such a scenario, the discount is distributed among all the merchant orders and calculated according to the total volume of each of the items.

![Merchant discount 1](https://spryker.s3.eu-central-1.amazonaws.com/docs/Marketplace/user+guides/Features/Marketplace+Order+Management/mp-discount-1.png)

- The discount is related to a single product item in the Marketplace order. In this case, the whole discount is assigned only to the merchant order that contains the discounted item.

![Merchant discount 2](https://spryker.s3.eu-central-1.amazonaws.com/docs/Marketplace/user+guides/Features/Marketplace+Order+Management/mp-discount-2.png)


## Voucher

A *Voucher* is a discount that applies when a customer enters an active voucher code on the *Cart* page.

![Cart voucher](https://spryker.s3.eu-central-1.amazonaws.com/docs/Marketplace/user+guides/Features/Marketplace+Promotions+and+Discounts+feature+overview/voucher-storefront.png)

Once the customer clicks **Redeem code**, the page refreshes to show the discount name, discount value, and available actions: **Remove** and **Clear all**. The **Clear all** action disables all the applied discounts. The **Remove** action disables a single discount.
![Cart voucher applied](https://spryker.s3.eu-central-1.amazonaws.com/docs/Marketplace/user+guides/Features/Marketplace+Promotions+and+Discounts+feature+overview/voucher-cart.png)

Multiple voucher codes can be generated for a single voucher. The code has a **Max number of uses** value which defines how many times the code can be redeemed.

You can enter codes manually or use the code generator in the Back Office.

![Generate codes](https://spryker.s3.eu-central-1.amazonaws.com/docs/Features/Promotions+&amp;+Discounts/Discount/Discount+Feature+Overview/generate_codes.png)

To learn how a product catalog manager can create a voucher in the Back Office, see [Creating a voucher](/docs/pbc/all/discount-management/latest/base-shop/manage-in-the-back-office/create-discounts.html).

## Cart Rule

A *cart rule* is a discount that applies to the cart once all the [decision rules](#decision-rule) linked to the cart rule are fulfilled.

The cart rule is applied automatically. If the decision rules of a discount are fulfilled, the customer can see the discount upon entering the cart. Unlike with [vouchers](#voucher), the **Clear all** and **Remove** actions are not displayed.
![Cart rule](https://spryker.s3.eu-central-1.amazonaws.com/docs/Marketplace/user+guides/Features/Marketplace+Promotions+and+Discounts+feature+overview/cart-rule-storefront.png)

To learn how a product catalog manager can create a cart rule in the Back Office, see [Create discounts](/docs/pbc/all/discount-management/latest/base-shop/manage-in-the-back-office/create-discounts.html).

### Decision rule

In the Back Office, you define discounts by building a query. A query consists of one or more rules, and each rule compares a parameter to a value using a relation operator. Such a rule is called a *decision rule*.

A query serves one of two purposes, depending on where you build it:

- Under **Apply when**, the query is a condition: it determines whether the discount applies to the cart at all.
- Under **Apply to**, the query is a filter: it selects which items in the cart receive the discount.

The Back Office does not use the term *decision rule*. You build queries in the **Apply when** and **Apply to** fields, which are described in [Apply to compared to Apply when](#apply-to-compared-to-apply-when).

A query can contain one or more rules. Find an exemplary combination below:

| PARAMETER | RELATION OPERATOR | VALUE |
| --- | --- | --- |
| total-quantity | equal |  3 |
|  day-of-week| equal | 5  |

In this case, the discount is applied if the cart contains three items and the purchase is made on the fifth day of the week (Friday).

Multiple rules form a single query. A query is a request for information based on the defined parameters. In the Discount feature, a query requests information from a cart, either to check whether the cart is eligible for the discount or to select the items to discount. By specifying rules, you define the parameters of the query.

In the Back Office, a product catalog manager creates the rules in a Query Builder. The rules created in the Query Builder are transformed into a single query.

The rules from the previous example look as follows in the Query Builder:

&lt;div class=&quot;width-100&quot;&gt;

![Query Builder showing the rules total-quantity equal 3 and day-of-week equal 5 combined with AND](https://spryker.s3.eu-central-1.amazonaws.com/docs/pbc/all/discount-management/latest/marketplace/marketplace-promotions-discounts-feature-overview/image-1788795776069.png)

&lt;/div&gt;

A product catalog manager can enter the query manually as well.

The same rules look as follows as a plain query:

&lt;div class=&quot;width-100&quot;&gt;

![Plain query](https://spryker.s3.eu-central-1.amazonaws.com/docs/Features/Promotions+&amp;+Discounts/Discount/Discount+Feature+Overview/plain-query.png)
&lt;/div&gt;

You can switch between Query Builder and Plain query modes to see how the specified rules look in either of them.  


Decision rules are combined with *AND* and *OR*  combination operators. With the AND operator, all the rules should be fulfilled for the discount to be applied. With the OR operator, at least one of them should be fulfilled for the discount to be applied.


In the following example, for the discount to be applied, a cart should contain three items, and the purchase should be made on Wednesday.

![AND operator](https://spryker.s3.eu-central-1.amazonaws.com/docs/Features/Promotions+&amp;+Discounts/Discount/Discount+Feature+Overview/and-operator.png)

In the following example, for the discount to be applied, a cart should contain three items, or the purchase should be made on Wednesday.

![OR operator](https://spryker.s3.eu-central-1.amazonaws.com/docs/Features/Promotions+&amp;+Discounts/Discount/Discount+Feature+Overview/or-operator.png)

{% info_block infoBox &quot;Info&quot; %}

When rules are combined by the OR operator, they do not exclude each other. If a cart fulfills both such rules, the discount is still applied.

{% endinfo_block %}


#### Product attributes in decision rules

To use a product attribute in a decision rule, reference it as `attribute.&lt;attribute_key&gt;` — for example, `attribute.color`. Spryker does not predefine these fields: it generates one field per product attribute key that exists in your database.

{% info_block warningBox &quot;Add attribute keys before you use them&quot; %}

If you do not see the attribute you expect in the Query Builder, the corresponding attribute key does not exist in your database yet. Add it in one of the following ways:

- [Create product attributes](/docs/pbc/all/product-information-management/latest/base-shop/manage-in-the-back-office/attributes/create-product-attributes.html) in the Back Office.
- Import attribute keys with the `product-attribute-key` data entity. For details, see [Import file details: product_attribute_key.csv](/docs/pbc/all/product-information-management/latest/base-shop/import-and-export-data/products-data-import/import-file-details-product-attribute-key.csv.html).

{% endinfo_block %}

The examples in this document use attribute keys such as `attribute.color` and `attribute.processor_cores`. Depending on the demo shop you started with, these keys may not exist in your database. Replace them with the attribute keys that exist in your own database.

#### Decision rule group

A rule group is a separate set of rules with its own combination operator.

![Decision rule group](https://spryker.s3.eu-central-1.amazonaws.com/docs/Features/Promotions+%26+Discounts/Discount/Discount+Feature+Overview/decision-rule-group.png)

With the rule groups, you can build multiple levels of rule hierarchy. When a cart is evaluated against the rules, it is evaluated on all levels of the hierarchy. On each level, there can be both rules and rule groups.

![Decision rule hierarchy](https://spryker.s3.eu-central-1.amazonaws.com/docs/Features/Promotions+%26+Discounts/Discount/Discount+Feature+Overview/decision-rule-hierarchy.png)

When a cart is evaluated on a level that has a rule and a rule group, the rule group is treated as a single rule. The following diagram shows how a cart is evaluated against the rules in the previous screenshot.

### Discount threshold

A *threshold* is a minimum number of items in the cart that should fulfill all the specified decision rules for the discount to be applied.
The default value is *1*. It means that a discount is applied if at least one item fulfills the discount&apos;s decision rules.

In the following example, the discount is applied if there are four items with the Intel Core processor in the cart.
![Threshold](https://spryker.s3.eu-central-1.amazonaws.com/docs/Features/Promotions+&amp;+Discounts/Discount/Discount+Feature+Overview/threshold.png)


## Discount application

Discount application is a discount configuration option that defines the products to which a discount is applied.

The Marketplace discounts are applied based on the query string.

The *query string* is a discount application type that uses [decision rules](#decision-rule) to dynamically define what products a discount applies to.

The discount in the following example applies to white products.

![Query collection](https://spryker.s3.eu-central-1.amazonaws.com/docs/Features/Promotions+&amp;+Discounts/Discount/Discount+Feature+Overview/collection-query.png)

The product selection based on the query string is dynamic:
- If at some point, the color attribute of a product changes from white to anything else, the product is no longer eligible to be discounted.
- If at some point, a product receives the white color attribute, it becomes eligible for the discount.

{% info_block infoBox &quot;Attribute keys in the examples&quot; %}

This example uses the `attribute.color` field. Attribute fields are generated from the product attribute keys in your own database, so replace `attribute.color` with an attribute key that exists in your database. For details, see [Product attributes in decision rules](#product-attributes-in-decision-rules).

{% endinfo_block %}

### Apply to compared to Apply when

A discount is configured in two separate places that serve different purposes:

| CONFIGURATION | BACK OFFICE TAB | SECTION HEADING | PURPOSE |
| --- | --- | --- | --- |
| **APPLY TO** | Discount calculation | Define what to apply the discount to | Selects the cart items that receive the discount. |
| **APPLY WHEN** | Conditions | Define when to apply the discount | Determines whether the discount applies to the cart at all. |

*Apply when* acts as a gate: if its decision rules are not fulfilled, the discount is not applied to the cart. *Apply to* then selects which of the items in the cart the discount value is distributed across.

Because the two settings serve different purposes, they offer different fields. *Apply when* evaluates the cart as a whole, so it also offers cart-level and customer-level fields, such as `grand-total`, `sub-total`, `total-quantity`, `currency`, `day-of-week`, and `customer-group`. *Apply to* selects individual items, so it offers only item-level fields, such as `sku`, `item-quantity`, `item-price`, `category`, and `attribute.&lt;attribute_key&gt;`.

Some fields, such as `sku`, `item-quantity`, and `item-price`, are available in both places, where they have a different meaning:

| FIELD | UNDER APPLY WHEN | UNDER APPLY TO |
| --- | --- | --- |
| `sku` | Checks whether the cart contains an item with this SKU. | Restricts the discount to the items with this SKU. |
| `item-quantity` | Checks whether an item with this quantity is in the cart. | Restricts the discount to the items with this quantity. |
| `item-price` | Checks whether an item with this price is in the cart. | Restricts the discount to the items with this price. |

The fields under *Apply to* do not discount an SKU or a quantity by themselves. They narrow down the set of items that the discount is applied to.

The following screenshots show the same rule in both places. The field list offered under *Apply to* is a subset of the one offered under *Apply when*, because *Apply to* only offers item-level fields. The attribute key in these screenshots comes from the demo data, so the key you see in your own project differs.

On the **Discount calculation** tab, *Apply to* selects the items that receive the discount:

![Apply to on the Discount calculation tab, with the rule attribute.farbe equal](https://spryker.s3.eu-central-1.amazonaws.com/docs/pbc/all/discount-management/latest/marketplace/marketplace-promotions-discounts-feature-overview/image-1788793722162.png)

On the **Conditions** tab, *Apply when* determines whether the discount applies to the cart at all:

![Apply when on the Conditions tab, with the rule attribute.farbe equal](https://spryker.s3.eu-central-1.amazonaws.com/docs/pbc/all/discount-management/latest/marketplace/marketplace-promotions-discounts-feature-overview/image-1788793729064.png)

{% info_block infoBox &quot;Example&quot; %}

A discount with *Apply when* set to `total-quantity &gt;= 10` and *Apply to* set to `attribute.color = &apos;white&apos;` first checks the total number of items in the cart, regardless of their color. If the cart contains at least ten items, the discount then reduces the price of the white items only.

{% endinfo_block %}

## Discount calculation

Calculation defines the value to be deducted from a product&apos;s original price. There are two types of discount calculation:

- Calculator percentage
- Calculator fixed

{% info_block infoBox &quot;Info&quot; %}

With the calculator fixed type, the currency of the respective shop is used for calculation.

{% endinfo_block %}


See examples in the following table.

| PRODUCT PRICE | CALCULATION TYPE | AMOUNT | DISCOUNT APPLIED | PRICE TO PAY |
| --- | --- | --- | --- | --- |
| €50 |  Calculator percentage | 10 | €5 | €45 |
| €50 | Calculator fixed | 10 | €10 | €40 |

A product catalog manager defines the calculation when [creating a voucher or a cart rule](/docs/pbc/all/discount-management/latest/base-shop/manage-in-the-back-office/create-discounts.html).
![Discount calculation](https://spryker.s3.eu-central-1.amazonaws.com/docs/Features/Promotions+&amp;+Discounts/Discount/Discount+Feature+Overview/discount_calculation.png)

## Discount exclusiveness

Discount exclusiveness defines if a discount value of a discount can be combined with the discount value of other discounts in a single order.

A product catalog manager defines exclusiveness when [creating a voucher or a cart rule](/docs/pbc/all/discount-management/latest/base-shop/manage-in-the-back-office/create-discounts.html).
![Exclusive discount](https://spryker.s3.eu-central-1.amazonaws.com/docs/Features/Promotions+&amp;+Discounts/Discount/Discount+Feature+Overview/exclusivity.png)

### Exclusive discount

An exclusive discount is a discount that, when applied to a cart, discards all the other discounts applied to it. If a cart is eligible for multiple exclusive discounts, the highest-value discount is applied.

In the following example, a cart with the order total amount of €100 contains the following discounts.

| DISCOUNT NAME  | DISCOUNT AMOUNT | DISCOUNT TYPE | EXCLUSIVENESS | DISCOUNTED AMOUNT |
| --- | --- | --- | --- | --- |
| D1 | 15 | Calculator percentage | Exclusive | €15 |
|D2|5| Calculator fixed | Exclusive | €5 |
|D3|10| Calculator percentage | Non-exclusive | €10 |

The discount exclusivity is resolved as follows:
1. The discounts D1 and D2 are exclusive, so the non-exclusive discount D3 is discarded.
2. The discount D1 provides more free value than the discount D2.
3. As a result, the discount D1 is applied.


### Non-exclusive discount

A non-exclusive discount is a discount that can be combined with other non-exclusive discounts in a single order.

In the following example, a cart with the order total amount of €30 contains the following discounts.

| DISCOUNT NAME  | DISCOUNT AMOUNT | DISCOUNT TYPE | EXCLUSIVENESS | DISCOUNTED AMOUNT |
| --- | --- | --- | --- | --- |
| D1 | 15 | Calculator percentage | Non-exclusive | €15 |
| D2 | 5 | Calculator fixed | Non-exclusive | €5 |
| D3 | 10 |Calculator percentage | Non-exclusive | €10 |

As all the discounts are non-exclusive, they are applied together.

## Discount validity interval

A *validity interval* is a time period during which a discount is active and can be applied.

If a cart is eligible for a discount outside of its validity interval, the cart rule is not applied. If a customer enters a voucher code outside of its validity interval, they get a &quot;Your voucher code is invalid.&quot; message.

A product catalog manager defines the validity interval when [creating a discount](/docs/pbc/all/discount-management/latest/base-shop/manage-in-the-back-office/create-discounts.html).
![Validity interval](https://spryker.s3.eu-central-1.amazonaws.com/docs/Features/Promotions+&amp;+Discounts/Discount/Discount+Feature+Overview/validity-interval.png)

## Related Developer articles

| INSTALLATION GUIDES|
| -------------- |
| [Install the Marketplace Promotions &amp; Discounts feature](/docs/pbc/all/discount-management/latest/marketplace/install-the-marketplace-promotions-discounts-feature.html) |
| [Install the Marketplace Merchant + Promotions &amp; Discounts feature](/docs/pbc/all/merchant-management/latest/marketplace/install-and-upgrade/install-features/install-the-marketplace-merchant-promotions-and-discounts-feature.html) |
| [Install the Marketplace Product Offer + Promotions &amp; Discounts feature](/docs/pbc/all/offer-management/latest/marketplace/install-and-upgrade/install-features/install-the-marketplace-product-offer-promotions-and-discounts-feature.html) |
</description>
            <pubDate>Tue, 15 Sep 2026 09:26:21 +0000</pubDate>
            <link>https://docs.spryker.com/docs/pbc/all/discount-management/latest/marketplace/marketplace-promotions-discounts-feature-overview.html</link>
            <guid isPermaLink="true">https://docs.spryker.com/docs/pbc/all/discount-management/latest/marketplace/marketplace-promotions-discounts-feature-overview.html</guid>
            
            
        </item>
        
        <item>
            <title>Workflows feature overview</title>
            <description>{% info_block warningBox &quot;Early Access&quot; %}

This feature is in Early Access. We&apos;d love for you to try it out and share feedback as we work toward general availability.

{% endinfo_block %}

The Workflows feature lets Back Office users design, adjust, and monitor multi-step business processes — such as B2B company onboarding, order escalations, or approval flows — directly in the Back Office, without a code deployment.

![The Workflows list in the Back Office, showing the 3-step setup guide and the CompanyOnboarding workflow with its status, active version, and triggers](https://spryker.s3.eu-central-1.amazonaws.com/docs/pbc/all/back-office/base-shop/workflow-feature-overview/workflows-1.png)

## Business problems it solves

Multi-step business processes are often hard-coded into a project, which creates friction:

- **Every process change needs a developer and a deployment.** Adding a step, changing who approves it, or adjusting a timeout normally means a code change and a release.
- **Process logic is invisible to the business.** Without reading source code or asking a developer, business teams cannot see or explain how a process actually behaves.
- **Ad hoc tracking does not scale.** Teams fall back to spreadsheets, shared inboxes, or manual reminders to track where a company, order, or customer stands in a process, with no single source of truth.

The Workflows feature removes the deployment dependency. Unlike a classic state machine, which lives in a deployed code file and requires a release to change, a workflow is designed, versioned, and activated directly in the Back Office. A visual builder lets you draw the process—adding steps and linking them—on a canvas, so process design is in the hands of the people who own the process, without writing state-machine XML by hand.

## How a workflow works

A workflow describes how a subject — which can be anything: a company, a user, a product, or any custom entity — moves through a sequence of steps as events occur, conditions are met, or timeouts elapse. A workflow starts when its subject reaches a specific application event, defined in a trigger: for example a company being created, a user registering, or a merchant being updated.

Consider onboarding a new B2B company. When a company is created, a workflow starts and walks it through a series of steps: *business verification*, then *contract agreement*, then *customer group assignment*, and finally *approved*. Some steps advance on their own once a condition is met (for example, the business has been verified); others wait for a Back Office user to confirm; and a step can time out if nothing happens. The whole process — its steps, their order, and the rules between them — is defined and adjusted in the Back Office, not in code.

The Back Office renders this as a diagram, so you can follow the flow without reading the underlying definition:

![The CompanyOnboarding workflow graph in the Back Office, showing the created, business verification, contract agreement, and customer group assignment steps, with a branch to denied if the business is not verified](https://spryker.s3.eu-central-1.amazonaws.com/docs/pbc/all/back-office/base-shop/workflow-feature-overview/workflows-3.png)

The example below simplifies this to its first decision point — verifying the business, then approving or denying — to show the underlying definition format from start to finish: a new company moves from `created` to `approved`, passing a verification check and an automatic waiting step along the way.

A workflow definition is an XML document in the `state-machine-01` format. It has three parts: `states`, `transitions`, and `events`:

```xml
{% raw %}&lt;statemachine xmlns=&quot;spryker:state-machine-01&quot;&gt;
    &lt;process name=&quot;CompanyOnboarding&quot; main=&quot;true&quot;&gt;

        &lt;states&gt;
            &lt;state name=&quot;created&quot;/&gt;
            &lt;state name=&quot;business verification&quot;/&gt;
            &lt;state name=&quot;approved&quot;/&gt;
            &lt;state name=&quot;denied&quot;/&gt;
        &lt;/states&gt;

        &lt;transitions&gt;
            &lt;!-- A plain event transition: the &quot;initiate&quot; event moves the subject forward. --&gt;
            &lt;transition happy=&quot;true&quot;&gt;
                &lt;source&gt;created&lt;/source&gt;
                &lt;target&gt;business verification&lt;/target&gt;
                &lt;event&gt;initiate&lt;/event&gt;
            &lt;/transition&gt;

            &lt;!-- A guarded transition: it fires only if the condition returns true. --&gt;
            &lt;transition happy=&quot;true&quot; condition=&quot;CompanyOnboarding/IsBusinessVerified&quot;&gt;
                &lt;source&gt;business verification&lt;/source&gt;
                &lt;target&gt;approved&lt;/target&gt;
                &lt;event&gt;verify business&lt;/event&gt;
            &lt;/transition&gt;

            &lt;!-- The same event with no condition sends the subject the other way. --&gt;
            &lt;transition&gt;
                &lt;source&gt;business verification&lt;/source&gt;
                &lt;target&gt;denied&lt;/target&gt;
                &lt;event&gt;verify business&lt;/event&gt;
            &lt;/transition&gt;
        &lt;/transitions&gt;

        &lt;events&gt;
            &lt;event name=&quot;initiate&quot; onEnter=&quot;true&quot;/&gt;
            &lt;event name=&quot;verify business&quot; timeout=&quot;3 second&quot;/&gt;
        &lt;/events&gt;

    &lt;/process&gt;
&lt;/statemachine&gt;{% endraw %}
```

You do not write this XML by hand. On the **Create Version** screen, a visual builder lets you draw the states and transitions on a canvas and set each one&apos;s properties in an inspector. The raw XML above is still available through the builder&apos;s advanced panel for review or direct editing.

![The Create Version screen in the Back Office, with the visual builder showing the workflow states on a canvas and the properties inspector on the right](https://spryker.s3.eu-central-1.amazonaws.com/docs/pbc/all/back-office/base-shop/workflow-feature-overview/workflows-2.png)

Here is how a company travels through this definition:

1. **The instance starts.** A trigger (see [Triggers](#triggers)) starts an instance for a new company. The instance begins in the initial state you set when you author the version — here, `created`.
2. **An `onEnter` event fires automatically.** The `initiate` event is marked `onEnter=&quot;true&quot;`, so the engine fires it as soon as the instance is in `created`, moving the company to `business verification`.
3. **A timeout waits, then a condition decides.** The `verify business` event has a `timeout=&quot;3 second&quot;`. After the timeout elapses, the engine evaluates the two `verify business` transitions in order. The first is guarded by the `CompanyOnboarding/IsBusinessVerified` condition: if it returns `true`, the company moves to `approved`. If not, the unguarded transition sends it to `denied`.

Three ideas in that trace do the heavy lifting, and each maps to a piece of project code a developer can plug in:

| In the definition | What it means | You provide |
|-------------------|---------------|-------------|
| `event` | A named step. It can fire automatically (`onEnter`), after a delay (`timeout`), on a user action (`manual`), or from application code. | Nothing — events are declared in the XML. |
| `condition=&quot;…&quot;` | A guard that lets a transition fire only when a business rule is true. | A [condition plugin](#conditions). |
| `command=&quot;…&quot;` | Project logic to run during a transition (not shown above — for example, &quot;mark the company approved&quot;). | A [command plugin](#commands). |

## Core concepts

A workflow is built from four entities. The example above is a *version* of a *process*; running it for one company creates an *instance*; and what starts that instance is a *trigger*. You manage all of them in the Back Office under **Administration &gt; Workflows** (shown at the top of this page).

| Concept | What it is |
|---------|------------|
| Process | The workflow itself: a named process bound to a subject type (for example `Company`). It is created once and never replaced — all versions and instances belong to it. |
| Version | One complete definition of the process at a point in time (the XML above): its states, transitions, and events, plus the initial state. A process can have many versions, but only one is *active* at a time. |
| Trigger | Connects an application event (for example &quot;a company was created&quot;) to the process, so that event automatically starts a new instance. |
| Instance | One subject running through the workflow — for example one specific company being onboarded. It tracks the subject&apos;s current state. |

### Why versions matter

Every instance is pinned to the version it started on. When you activate a newer version, running instances do **not** jump to it — they finish on the version they began with. Only *new* instances start on the newly active version.

This is what makes editing a live process safe. You publish an improved version for future subjects, while in-flight subjects complete on the exact rules they started with — no half-migrated instances and no transitions that suddenly point at states that no longer exist.

## Subjects

A workflow is attached to a *subject type*, which you choose when you create the process. It is not limited to companies: the subject type is a label you define — `Company`, `Customer`, `Product`, or anything your project needs. The trigger you configure decides which application event starts an instance for that subject.

For example, a process with subject type `Company` and a trigger on company creation starts one instance per created company, and each company then runs its own instance independently.

## Instances

An *instance* is a single subject running through a workflow — for example one specific company being onboarded. Each instance is pinned to the version it started on and tracks its current state. In the Back Office you can inspect instances, see their current state, and trigger any manual actions the workflow defines (for example, a step that a Back Office user must confirm before the workflow continues).

{% info_block infoBox &quot;Instance history retention&quot; %}

Each instance and its transition history are kept in the database. This MVP does not ship an automated cleanup or retention job, so plan for periodic housekeeping if you expect a high volume of instances.

{% endinfo_block %}

## Transitions

A workflow advances through three kinds of transitions:

- **Event transitions**: triggered by an application event or a manual action in the Back Office.
- **Condition transitions**: advance automatically once a business condition becomes true.
- **Timeout transitions**: advance automatically after a defined period elapses.

Condition and timeout transitions have no incoming event to push them, so two console commands advance them on a schedule:

- `workflow:check-condition` — advances every condition transition whose condition has become `true`.
- `workflow:check-timeout` — advances every timeout transition whose timeout has elapsed.

Without them, an instance that reaches a condition or timeout transition waits forever. These commands are **not** scheduled out of the box; the [installation guide](/docs/dg/dev/integrate-and-configure/integrate-workflow-feature.html) shows how to register them as recurring jobs so workflows progress on their own.

## Triggers

A *trigger* connects an application event to a workflow so that a new instance starts automatically. For example, creating a company can trigger a new onboarding instance. You select the trigger event in the Back Office when you configure the process; the available events are provided by trigger plugins (see [Extending a workflow](#extending-a-workflow)).

## Extending a workflow

The definition references project code by name in two places: `condition=&quot;…&quot;` and `command=&quot;…&quot;`. A developer implements each as a plugin and registers it in the project `WorkflowDependencyProvider` (see the [installation guide](/docs/dg/dev/integrate-and-configure/integrate-workflow-feature.html)). The engine matches a plugin to a definition by two values: its `getName()` (the string used in the XML) and its `getSubjectType()` (so the same name can behave differently for different subjects).

### Commands

A *command* runs project logic during a transition — for example, marking a company approved when it enters the `approved` state. Reference it in the definition as `command=&quot;CompanyOnboarding/MarkCompanyActiveAndApproved&quot;`, then implement `WorkflowCommandPluginInterface` so `getName()` returns that same string:

```php
interface WorkflowCommandPluginInterface extends CommandPluginInterface
{
    public function getName(): string;

    public function getSubjectType(): string;
}
```

### Conditions

A *condition* guards a transition: the workflow takes that transition only when the condition returns `true` — for example, &quot;the business is verified.&quot; Reference it as `condition=&quot;CompanyOnboarding/IsBusinessVerified&quot;`, then implement `WorkflowConditionPluginInterface`:

```php
interface WorkflowConditionPluginInterface extends ConditionPluginInterface
{
    public function getName(): string;

    public function getSubjectType(): string;
}
```

### Trigger plugins

A trigger plugin provides an application event that can start an instance. Unlike commands and conditions — which are named inside the definition XML — a trigger is chosen in the Back Office when you configure the process. A trigger plugin binds an application event (`getEventName()`, for example `Entity.spy_company.create`) to a subject type, so that whenever that event fires for that subject, a new instance starts. Implement `StateMachineProcessTriggerPluginInterface`:

```php
interface StateMachineProcessTriggerPluginInterface
{
    public function getEventName(): string;

    public function getName(): string;

    public function getSubjectType(): string;

    public function getDescription(): string;
}
```

## Provisioning workflows on installation

Workflows can be shipped with a project and provisioned automatically during installation through data import. This means a demo or production workflow is available immediately after setup, and re-importing the same workflow does not create duplicates. See the [installation guide](/docs/dg/dev/integrate-and-configure/integrate-workflow-feature.html) for details.
</description>
            <pubDate>Mon, 14 Sep 2026 14:50:54 +0000</pubDate>
            <link>https://docs.spryker.com/docs/pbc/all/back-office/latest/base-shop/workflows-feature-overview.html</link>
            <guid isPermaLink="true">https://docs.spryker.com/docs/pbc/all/back-office/latest/base-shop/workflows-feature-overview.html</guid>
            
            
        </item>
        
        <item>
            <title>Keeping dependencies updated for performance</title>
            <description>Keeping your project&apos;s Spryker module dependencies up to date is critical for maintaining optimal performance, security, and reducing long-term upgrade efforts.

## Why keep dependencies updated

Updating dependencies regularly provides several practical benefits:

1. **Risk of security vulnerabilities**: Most recent versions contain necessary fixes to all known vulnerabilities.
2. **Performance and resource consumption optimizations**: Spryker continuously releases performance improvements based on real-life experiences and scenarios.
3. **Decreasing upgrade efforts**: Distributing upgrades across many smaller steps is much easier than doing one massive upgrade later.

## Key resources

The following resources provide information about performance-related module releases:

- [Security release notes 202512.0](https://docs.spryker.com/docs/about/all/releases/security-releases/security-release-notes-202512.0.html)
- [Release notes 202410.0](https://docs.spryker.com/docs/scos/user/intro-to-spryker/releases/release-notes/release-notes-202410.0/release-notes-202410.0.html)
- [Release notes 202507.0](https://docs.spryker.com/docs/scos/user/intro-to-spryker/releases/release-notes/release-notes-202507.0/release-notes-202507.0.html)
- [General performance guidelines](/docs/dg/dev/guidelines/performance-guidelines/general-performance-guidelines.html) - contains the list of recent module versions with known performance optimizations
- [Cart page performance configuration](https://docs.spryker.com/docs/pbc/all/cart-and-checkout/latest/cart-page-performance-configuration.html)

## Critical module updates

The following sections list important module updates that include performance improvements. It&apos;s recommended that each Spryker project evaluates and applies these updates.

### Product search performance

**spryker/product-page-search** - Recent versions (for example, `3.38.0` - `3.40.0`) include improvements such as:
- Caching logic adjustments for product images
- Potential bulk operation support to make search document writes more efficient
- Avoiding duplicate locale events

### Session management

**spryker/session** - at least `^4.17.0`
- Provides configurable session locking mechanism
- See [Redis session lock](/docs/dg/dev/troubleshooting/troubleshooting-performance-issues/redis-session-lock.html) for configuration details

**spryker/session-redis** - at least `^1.11.2`
- Includes configurable session locker
- See [Redis session lock](/docs/dg/dev/troubleshooting/troubleshooting-performance-issues/redis-session-lock.html) for setup instructions

### Merchant Portal and Back Office performance with ACL rules

- [spryker/acl:^3.26.0](https://github.com/spryker/acl/releases/tag/3.26.0)
- [spryker/acl-entity:^1.16.0](https://github.com/spryker/acl-entity/releases/tag/1.16.0)
- [spryker/gui:^4.11.0](https://github.com/spryker/gui/releases/tag/4.11.0)
- [spryker/merchant-profile:^1.9.0](https://github.com/spryker/merchant-profile/releases/tag/1.9.0)
- [spryker/merchant-user:^1.9.0](https://github.com/spryker/merchant-user/releases/tag/1.9.0)
- [spryker/multi-factor-auth-merchant-portal:^2.1.0](https://github.com/spryker/multi-factor-auth-merchant-portal/releases/tag/2.1.0)
- [spryker/product-attribute:^1.18.0](https://github.com/spryker/product-attribute/releases/tag/1.18.0)
- [spryker/zed-navigation:^1.15.0](https://github.com/spryker/zed-navigation/releases/tag/1.15.0)

### Back Office performance on category list page

- [spryker/product-category:&quot;^4.32.0&quot;](https://github.com/spryker/zed-navigation/releases/tag/4.32.0)

### Order placement performance

- [spryker/calculation:^4.14.0](https://github.com/spryker/calculation/releases/tag/4.14.0)
- [spryker/discount-calculation-connector:^5.4.0](https://github.com/spryker/discount-calculation-connector/releases/tag/5.4.0)
- [spryker/merchant:^3.15.0](https://github.com/spryker/merchant/releases/tag/3.15.0)
- [spryker/sales:^11.60.0](https://github.com/spryker/sales/releases/tag/11.60.0)
- [spryker/product:^6.49.0](https://github.com/spryker/product/releases/tag/6.49.0)
- [spryker/discount:^9.43.0](https://github.com/spryker/discount/releases/tag/9.43.0)
- [spryker/product-cart-connector:^4.13.0](https://github.com/spryker/product-cart-connector/releases/tag/4.13.0)
- [spryker/company-role:^1.9.1](https://github.com/spryker/company-role/releases/tag/1.9.1)
- [spryker/propel:^3.43.0](https://github.com/spryker/propel/releases/tag/3.43.0)
- [spryker/sales:^11.63.0](https://github.com/spryker/sales/releases/tag/11.63.0)
- [spryker/sales-product-connector:^1.11.1](https://github.com/spryker/sales-product-connector/releases/tag/1.11.1)
- [spryker/shipment:^8.24.0](https://github.com/spryker/shipment/releases/tag/8.24.0)

### OMS availability check and order item reservation

- [spryker/availability:^9.27.0](https://github.com/spryker/availability/releases/tag/9.27.0)
- [spryker/stock:^8.10.1](https://github.com/spryker/stock/releases/tag/8.10.1)
- [spryker/oms:^11.45.1](https://github.com/spryker/oms/releases/tag/11.45.1)
- [spryker/propel:^3.43.0](https://github.com/spryker/propel/releases/tag/3.43.0)
- [spryker/sales:^11.63.0](https://github.com/spryker/sales/releases/tag/11.63.0)

### Publish and synchronization (merchant-related)

- [spryker/merchant-product-offer-storage:^2.6.0](https://github.com/spryker/merchant-product-offer-storage/releases/tag/2.6.0)
- [spryker/product-offer-storage:^1.8.0](https://github.com/spryker/product-offer-storage/releases/tag/1.8.0)
- [spryker/propel:^3.45.0](https://github.com/spryker/propel/releases/tag/3.45.0)

### Publish and synchronization (merchant relationship prices)

- [spryker/merchant:^3.21.0](https://github.com/spryker/merchant/releases/tag/3.21.0)
- [spryker/price-product-merchant-relationship:^1.13.0](https://github.com/spryker/price-product-merchant-relationship/releases/tag/1.13.0)
- [spryker/price-product-merchant-relationship-storage:^1.22.0](https://github.com/spryker/price-product-merchant-relationship-storage/releases/tag/1.22.0)

Additionally, index the `fk_company_business_unit` column of both storage tables. Create `src/Pyz/Zed/PriceProductMerchantRelationshipStorage/Persistence/Propel/Schema/spy_price_product_merchant_relationship_storage.schema.xml` and run `vendor/bin/console propel:install`:

```xml
&lt;!--//--&gt;
    &lt;table name=&quot;spy_price_product_abstract_merchant_relationship_storage&quot;&gt;
        &lt;index name=&quot;spy_price_product_ab_m_r_storage-fk_company_business_unit&quot;&gt;
            &lt;index-column name=&quot;fk_company_business_unit&quot;/&gt;
        &lt;/index&gt;
    &lt;/table&gt;

    &lt;table name=&quot;spy_price_product_concrete_merchant_relationship_storage&quot;&gt;
        &lt;index name=&quot;spy_price_product_con_m_r_storage-fk_company_business_unit&quot;&gt;
            &lt;index-column name=&quot;fk_company_business_unit&quot;/&gt;
        &lt;/index&gt;
    &lt;/table&gt;
&lt;!--//--&gt;
```

### Publish and synchronization (product-related)

- [spryker/price-product:^4.48.0](https://github.com/spryker/price-product/releases/tag/4.48.0)
- [spryker/product-page-search:^3.40.0](https://github.com/spryker/product-page-search/releases/tag/3.40.0)
- [spryker/product-search:^5.24.1](https://github.com/spryker/product-search/releases/tag/5.24.1)
- [spryker/product-storage:^1.47.0](https://github.com/spryker/product-storage/releases/tag/1.47.0)
- [spryker/product-offer-storage:^1.10.0](https://github.com/spryker/product-offer-storage/releases/tag/1.10.0)
- [spryker/price-product-offer:^1.7.1](https://github.com/spryker/price-product-offer/releases/tag/1.7.1)
- [spryker/price-product-offer-storage:^1.5.1](https://github.com/spryker/price-product-offer-storage/releases/tag/1.5.1)
- [spryker/price-product-storage:^4.13.0](https://github.com/spryker/price-product-storage/releases/tag/4.13.0)
- [spryker/product-image:^3.20.1](https://github.com/spryker/product-image/releases/tag/3.20.1)
- [spryker/product-category-storage:^2.11.0](https://github.com/spryker/product-category-storage/releases/tag/2.11.0)
- [spryker/product-category-search:^1.2.1](https://github.com/spryker/product-category-search/releases/tag/1.2.1)
- [spryker/propel:^3.47.0](https://github.com/spryker/propel/releases/tag/3.47.0)
  - Note: If you still use destructive deployments, update the `config/install/destructive.yml` file. You can copy it from any demo shop.
- [spryker/event-behavior:^1.32.0](https://github.com/spryker/event-behavior/releases/tag/1.32.0)
- [spryker/synchronization-behavior:^1.13.0](https://github.com/spryker/synchronization-behavior/releases/tag/1.13.0)

### Publish and synchronization (Category tree build logic)

- [spryker/category:&quot;^5.23.0&quot;](https://github.com/spryker/category/releases/tag/5.23.0)
- [spryker/category-storage:&quot;^2.12.0&quot;](https://github.com/spryker/category-storage/releases/tag/2.12.0)
- [spryker/propel-orm:&quot;^1.22.0&quot;](https://github.com/spryker/propel-orm/releases/tag/1.22.0)
- [spryker/util-sanitize:&quot;^2.3.1&quot;](https://github.com/spryker/util-sanitize/releases/tag/2.3.1)

### Publish and synchronization (queue and event performance)

To update all required modules at once:

```bash
composer update spryker/rabbit-mq:&quot;^2.25.0&quot; spryker/availability-storage:&quot;^2.11.0&quot; spryker/event:&quot;^2.17.1&quot; spryker/glossary-storage:&quot;^1.14.0&quot; spryker/merchant-product:&quot;^1.12.0&quot; spryker/merchant-product-offer:&quot;^1.12.0&quot; spryker/merchant-product-offer-search:&quot;^1.9.0&quot; spryker/merchant-product-search:&quot;^1.6.0&quot; spryker/merchant-product-storage:&quot;^1.7.0&quot; spryker/price-product:&quot;^4.51.0&quot; spryker/price-product-merchant-relationship-storage:&quot;^1.21.0&quot; spryker/price-product-offer-storage:&quot;^1.8.0&quot; spryker/price-product-storage:&quot;^4.16.0&quot; spryker/product:&quot;^6.54.0&quot; spryker/product-category-search:&quot;^1.3.0&quot; spryker/product-category-storage:&quot;^2.13.0&quot; spryker/product-group-storage:&quot;^1.9.0&quot; spryker/product-image:&quot;^3.21.0&quot; spryker/product-image-storage:&quot;^1.21.0&quot; spryker/product-list:&quot;^1.10.0&quot; spryker/product-list-search:&quot;^2.11.0&quot; spryker/product-list-storage:&quot;^1.21.0&quot; spryker/product-offer:&quot;^1.17.0&quot; spryker/product-offer-availability-storage:&quot;^1.6.0&quot; spryker/product-offer-stock:&quot;^1.7.0&quot; spryker/product-page-search:&quot;^3.47.0&quot; spryker/product-page-search-extension:&quot;^1.7.0&quot; spryker/propel:&quot;^3.50.0&quot; spryker/queue:&quot;^1.28.0&quot; spryker/symfony-messenger:&quot;^1.6.0&quot; spryker/tax-product-storage:&quot;^1.8.0&quot; spryker/url-storage:&quot;^1.23.0&quot; spryker/synchronization-behavior:&quot;^1.14.0&quot;
```

- [spryker/rabbit-mq:^2.25.0](https://github.com/spryker/rabbit-mq/releases/tag/2.25.0)
- [spryker/availability-storage:^2.11.0](https://github.com/spryker/availability-storage/releases/tag/2.11.0)
- [spryker/event:^2.17.1](https://github.com/spryker/event/releases/tag/2.17.1)
- [spryker/event-behavior:^1.35.0](https://github.com/spryker/event-behavior/releases/tag/1.35.0)
- [spryker/glossary-storage:^1.14.0](https://github.com/spryker/glossary-storage/releases/tag/1.14.0)
- [spryker/merchant-product:^1.12.0](https://github.com/spryker/merchant-product/releases/tag/1.12.0)
- [spryker/merchant-product-offer:^1.12.0](https://github.com/spryker/merchant-product-offer/releases/tag/1.12.0)
- [spryker/merchant-product-offer-search:^1.9.0](https://github.com/spryker/merchant-product-offer-search/releases/tag/1.9.0)
- [spryker/merchant-product-search:^1.6.0](https://github.com/spryker/merchant-product-search/releases/tag/1.6.0)
- [spryker/merchant-product-storage:^1.7.0](https://github.com/spryker/merchant-product-storage/releases/tag/1.7.0)
- [spryker/price-product:^4.51.0](https://github.com/spryker/price-product/releases/tag/4.51.0)
- [spryker/price-product-merchant-relationship-storage:^1.21.0](https://github.com/spryker/price-product-merchant-relationship-storage/releases/tag/1.21.0)
- [spryker/price-product-offer-storage:^1.8.0](https://github.com/spryker/price-product-offer-storage/releases/tag/1.8.0)
- [spryker/price-product-storage:^4.16.0](https://github.com/spryker/price-product-storage/releases/tag/4.16.0)
- [spryker/product:^6.54.0](https://github.com/spryker/product/releases/tag/6.54.0)
- [spryker/product-category-search:^1.3.0](https://github.com/spryker/product-category-search/releases/tag/1.3.0)
- [spryker/product-category-storage:^2.13.0](https://github.com/spryker/product-category-storage/releases/tag/2.13.0)
- [spryker/product-group-storage:^1.9.0](https://github.com/spryker/product-group-storage/releases/tag/1.9.0)
- [spryker/product-image:^3.21.0](https://github.com/spryker/product-image/releases/tag/3.21.0)
- [spryker/product-image-storage:^1.21.0](https://github.com/spryker/product-image-storage/releases/tag/1.21.0)
- [spryker/product-list:^1.10.0](https://github.com/spryker/product-list/releases/tag/1.10.0)
- [spryker/product-list-search:^2.11.0](https://github.com/spryker/product-list-search/releases/tag/2.11.0)
- [spryker/product-list-storage:^1.21.0](https://github.com/spryker/product-list-storage/releases/tag/1.21.0)
- [spryker/product-offer:^1.17.0](https://github.com/spryker/product-offer/releases/tag/1.17.0)
- [spryker/product-offer-availability-storage:^1.6.0](https://github.com/spryker/product-offer-availability-storage/releases/tag/1.6.0)
- [spryker/product-offer-stock:^1.7.0](https://github.com/spryker/product-offer-stock/releases/tag/1.7.0)
- [spryker/product-page-search:^3.47.0](https://github.com/spryker/product-page-search/releases/tag/3.47.0)
- [spryker/product-page-search-extension:^1.7.0](https://github.com/spryker/product-page-search-extension/releases/tag/1.7.0)
- [spryker/propel:^3.50.0](https://github.com/spryker/propel/releases/tag/3.50.0)
- [spryker/queue:^1.28.0](https://github.com/spryker/queue/releases/tag/1.28.0)
- [spryker/symfony-messenger:^1.6.0](https://github.com/spryker/symfony-messenger/releases/tag/1.6.0)
- [spryker/tax-product-storage:^1.8.0](https://github.com/spryker/tax-product-storage/releases/tag/1.8.0)
- [spryker/url-storage:^1.23.0](https://github.com/spryker/url-storage/releases/tag/1.23.0)
- [spryker/synchronization-behavior:^1.14.0](https://github.com/spryker/synchronization-behavior/releases/tag/1.14.0)

### Data import (memory usage)

- [spryker/acl-entity:&quot;^1.17.0&quot;](https://github.com/spryker/acl-entity/releases/tag/1.17.0)
- [spryker/data-import:&quot;^1.33.0&quot;](https://github.com/spryker/data-import/releases/tag/1.33.0)
- [spryker/merchant-relationship-product-list-data-import:&quot;^0.1.3&quot;](https://github.com/spryker/merchant-relationship-product-list-data-import/releases/tag/0.1.3)
- [spryker/price-product-merchant-relationship-data-import:&quot;^0.2.5&quot;](https://github.com/spryker/price-product-merchant-relationship-data-import/releases/tag/0.2.5)

### Dynamic entity performance improvements

- [spryker/dynamic-entity:^1.21.0](https://github.com/spryker/dynamic-entity/releases/tag/1.21.0)
- [spryker/dynamic-entity-backend-api:^1.15.0](https://github.com/spryker/dynamic-entity-backend-api/releases/tag/1.15.0)
- [spryker/stock:^8.15.0](https://github.com/spryker/stock/releases/tag/8.15.0)

### Cart page and checkout for large carts (100+ items)

For comprehensive guidance on optimizing cart performance, see [Cart page performance configuration](https://docs.spryker.com/docs/pbc/all/cart-and-checkout/latest/cart-page-performance-configuration.html).

- [spryker-shop/checkout-page:^3.41.0](https://github.com/spryker-shop/checkout-page/releases/tag/3.41.0)
- [spryker-shop/session-customer-validation-page:^1.4.0](https://github.com/spryker-shop/session-customer-validation-page/releases/tag/1.4.0)

### Back Office product abstract list page performance

- [spryker/product-management:^0.20.17](https://github.com/spryker/product-management/releases/tag/0.20.17)

### Product detail page performance on Yves

- [spryker/currency:^4.9.0](https://github.com/spryker/currency/releases/tag/4.9.0)
- [spryker/glossary-storage:^1.15.0](https://github.com/spryker/glossary-storage/releases/tag/1.15.0)
- [spryker/kernel:^3.87.0](https://github.com/spryker/kernel/releases/tag/3.87.0)
- [spryker/price-product:^4.55.0](https://github.com/spryker/price-product/releases/tag/4.55.0)
- [spryker/price-product-storage:^4.18.0](https://github.com/spryker/price-product-storage/releases/tag/4.18.0)
- [spryker/product-image-storage:^1.22.0](https://github.com/spryker/product-image-storage/releases/tag/1.22.0)
- [spryker/product-storage:^1.58.0](https://github.com/spryker/product-storage/releases/tag/1.58.0)
- [spryker/quote:^2.27.0](https://github.com/spryker/quote/releases/tag/2.27.0)
- [spryker/redis:^2.14.0](https://github.com/spryker/redis/releases/tag/2.14.0)
- [spryker/router:^1.27.0](https://github.com/spryker/router/releases/tag/1.27.0)
- [spryker-shop/product-group-widget:^1.14.0](https://github.com/spryker-shop/product-group-widget/releases/tag/1.14.0)
- [spryker-shop/product-review-widget:^1.21.0](https://github.com/spryker-shop/product-review-widget/releases/tag/1.21.0)
- [spryker-shop/shop-ui:^2.2.0](https://github.com/spryker-shop/shop-ui/releases/tag/2.2.0)
- [spryker-shop/store-widget:^1.6.0](https://github.com/spryker-shop/store-widget/releases/tag/1.6.0)
- [spryker/store:^1.39.0](https://github.com/spryker/store/releases/tag/1.39.0)
- [spryker/synchronization:^1.20.0](https://github.com/spryker/synchronization/releases/tag/1.20.0)
- [spryker/url-storage:^1.26.0](https://github.com/spryker/url-storage/releases/tag/1.26.0)

## Update strategy

To effectively manage dependency updates:

1. **Monitor release notes**: Regularly check Spryker release notes for performance-related updates.
2. **Test in staging**: Always test module updates in a staging environment before production deployment.
3. **Prioritize performance modules**: Focus on modules that directly impact your application&apos;s performance bottlenecks.
4. **Use semantic versioning**: Understand the impact of major, minor, and patch updates.
5. **Batch related updates**: Group related module updates together for testing efficiency.

## Compatibility considerations

When updating modules:

- Check module compatibility with your current Spryker version
- Review breaking changes in major version updates
- Test all affected functionality after updates
- Monitor application performance metrics before and after updates
- Consider using Spryker&apos;s [Composer Dependency Manager](https://docs.spryker.com/docs/scos/dev/setup/managing-scos-dependencies-with-composer.html)
</description>
            <pubDate>Mon, 14 Sep 2026 13:49:37 +0000</pubDate>
            <link>https://docs.spryker.com/docs/dg/dev/guidelines/performance-guidelines/keeping-dependencies-updated.html</link>
            <guid isPermaLink="true">https://docs.spryker.com/docs/dg/dev/guidelines/performance-guidelines/keeping-dependencies-updated.html</guid>
            
            
        </item>
        
        <item>
            <title>Troubleshooting API Platform</title>
            <description>This document provides solutions to common issues when working with API Platform in Spryker.

## Generation issues

### Resources not generating

**Symptom:** Running `docker/sdk cli GLUE_APPLICATION=GLUE_BACKEND glue api:generate` completes but no resources are created.

**Possible causes:**

1. **Schema file location is incorrect**

   ```bash
   ❌ src/Pyz/Glue/Customer/api/customers.resource.yml
   ✅ src/Pyz/Glue/Customer/resources/api/backend/customers.resource.yml
   ```

2. **API type not configured**

   Check `config/{APPLICATION}/packages/spryker_api_platform.php`:

   ```php
   return static function (SprykerApiPlatformConfig $sprykerApiPlatform): void {
       $sprykerApiPlatform-&gt;apiTypes([
           &apos;backend&apos;, // Must match directory name
       ]);
   };
   ```

3. **Bundle not registered**

   Verify `config/{APPLICATION}/bundles.php` includes:

   ```php
   SprykerApiPlatformBundle::class =&gt; [&apos;all&apos; =&gt; true],
   ```

**Solution:**

```bash
# Debug to see what&apos;s being discovered
docker/sdk cli glue  api:debug --list

# Check schema validation
docker/sdk cli GLUE_APPLICATION=GLUE_BACKEND glue api:generate --validate-only

# Force regeneration
docker/sdk cli GLUE_APPLICATION=GLUE_BACKEND glue api:generate --force
```

### Schema validation errors

**Symptom:** Generation fails with schema validation errors.

**Common errors:**

```bash
# Error: Invalid operation type
❌ operations:
    - type: CREATE

✅ operations:
    - type: Post

# Error: Invalid property type
❌ type: int
✅ type: integer

# Error: Missing resource name
❌ resource:
    shortName: customers

✅ resource:
    name: Customers
    shortName: customers

# Error: Property declares both &quot;items&quot; and &quot;openapiContext.items&quot;
# openapiContext is merged on top of the derived schema, so the hand-written
# shape would win and the typed element schema would be discarded silently.
❌ categories:
    type: array
    items:
        type: object
        properties:
            categoryKey: { type: string }
    openapiContext:
        items:
            type: object
            properties:
                categoryKey: { type: string }

✅ categories:
    type: array
    items:
        type: object
        properties:
            categoryKey: { type: string }

# Error: Property is both a relationship and an inline object list
# A relationship property already receives a docblock describing its target
# resource, and only one docblock is emitted per property.
❌ includes:
    - relationshipName: addresses
      targetResource: CustomersAddresses
  properties:
    addresses:
        type: array
        items:
            type: object
            properties:
                city: { type: string }

✅ includes:
    - relationshipName: addresses
      targetResource: CustomersAddresses
```

**Solution:**

1. Check schema against examples in documentation
2. Use `--validate-only` flag for detailed validation:

   ```bash
   docker/sdk cli GLUE_APPLICATION=GLUE_BACKEND glue api:generate --validate-only
   ```

3. Inspect merged schema:

   ```bash
   docker/sdk cli glue  api:debug resource-name --show-merged
   ```

### A list property still publishes as an untyped array

**Symptom:** A `type: array` property has an `items` block, but the published contract shows
`&quot;type&quot;: &quot;array&quot;` with no `items` reference, and SDK generators produce an untyped collection.

**Possible causes:**

1. **`items` is nested under `openapiContext` instead of being a sibling of `type: array`**

   Only a sibling triggers typing. An `items` block under `openapiContext` is documentation
   passthrough: it generates no class and produces no reference.

   ```yaml
   ❌ categories:
       type: array
       openapiContext:
           items:
               type: object
               properties:
                   categoryKey: { type: string }

   ✅ categories:
       type: array
       items:
           type: object
           properties:
               categoryKey: { type: string }
   ```

2. **`items.type` is a scalar**

   A list of scalars generates no element class and no reference. This is expected — there is nothing
   to type.

3. **The resource was not regenerated after the schema change**

   ```bash
   docker/sdk cli GLUE_APPLICATION=GLUE_BACKEND glue api:generate
   ```

4. **Stale generated code is still being served**

   See [Inspecting generated code](#inspecting-generated-code) to confirm what is on disk.

**Solution:**

Confirm the generated resource class carries a `@var array&lt;\Generated\…&gt;` docblock on the property.
That docblock is what API Platform reads to build the element reference — if it is absent, the contract
falls back to an untyped array. See
[Typed collections in the published contract](/docs/integrations/spryker-api/api-platform/typed-collections.html).

## Runtime issues

### Provider/Processor not found

**Symptom:**

```bash
Error: Class &quot;Pyz\Glue\Customer\Api\Backend\Provider\CustomerBackendProvider&quot; not found
```

**Possible causes:**

1. Class doesn&apos;t exist or namespace is wrong
2. Not registered in the Dependency Injection container
3. Typo in the schema file

**Solution:**

1. Verify the class exists and namespace matches:

   ```php
   namespace Pyz\Glue\Customer\Api\Backend\Provider;

   class CustomerBackendProvider implements ProviderInterface
   ```

2. Ensure services are auto-discovered in `ApplicationServices.php`:

   ```php
   $services-&gt;load(&apos;Pyz\\Glue\\&apos;, &apos;../../../src/Pyz/Glue/&apos;);
   ```

3. Check class name in the resource schema file of the module matches exactly:

   ```yaml
   provider: &quot;Pyz\\Glue\\Customer\\Api\\Backend\\Provider\\CustomerBackendProvider&quot;
   ```

### Validation not working

**Symptom:** API accepts invalid data despite validation rules.

**Possible causes:**

1. Validation schema file not found
2. Wrong operation name in validation schema
3. Validation groups not matching

**Solution:**

1. Ensure validation file exists:

   ```bash
   ✅ resources/api/backend/customers.validation.yml
   ```

2. Match operation names to HTTP methods:

   ```yaml
   post:      # For POST /customers
     email:
       - NotBlank

   patch:     # For PATCH /customers/{id}
     email:
       - Optional:
           constraints:
             - Email
   ```

3. Check generated resource class (for example `Generated\Api\Storefront\CustomersStorefrontResource`) has validation attributes:

   ```php
   #[Assert\NotBlank(groups: [&apos;customers:create&apos;])]
   #[Assert\Email(groups: [&apos;customers:create&apos;])]
   public ?string $email = null;
   ```

### API documentation UI not displaying correctly

**Symptom:** When accessing the root URL of your API application, you see:
- Missing styles/CSS
- Broken JavaScript functionality
- Plain HTML without formatting
- &quot;Failed to load resource&quot; errors in the browser console

**Cause:** Assets were not installed after API Platform integration.

**Solution:**

Run the appropriate assets:install command for your application:

#### For Glue application

```bash
docker/sdk cli glue assets:install public/Glue/assets  --symlink
```

#### For GlueStorefront

```bash
docker/sdk cli GLUE_APPLICATION=GLUE_STOREFRONT glue assets:install public/GlueStorefront/assets/  --symlink
```

#### For GlueBackend

```bash
docker/sdk cli GLUE_APPLICATION=GLUE_BACKEND glue assets:install public/GlueBackend/assets/  --symlink
```

Then verify the documentation UI loads correctly by visiting the root URL:
- Storefront: `https://glue-storefront.mysprykershop.com/`
- Backend: `https://glue-backend.mysprykershop.com/`

{% info_block warningBox &quot;Required after integration&quot; %}

The `assets:install` command must be run after integrating API Platform and whenever API Platform assets are updated. This is a required step documented in [Integrate API Platform](/docs/integrations/spryker-api/migrate-from-glue-to-api-platform/integrate-api-platform.html).

{% endinfo_block %}

### 404 Not Found for API endpoints

**Symptom:** API requests return 404.

**Possible causes:**

1. Router not configured
2. Routes not loaded
3. Wrong URL format

**Solution:**

1. Verify `SymfonyFrameworkRouterPlugin` is registered:

   ```php
   // RouterDependencyProvider
   protected function getRouterPlugins(): array
   {
       return [
           new GlueRouterPlugin(),
           new SymfonyFrameworkRouterPlugin(), // Must be present
       ];
   }
   ```

2. Check API documentation for correct URLs:

   ```bash
   Storefront: https://glue-storefront.mysprykershop.com/
   Backend: https://glue-backend.mysprykershop.com/
   ```

   The interactive API documentation is available at the root URL of each application.

3. Use correct URL format:

   ```bash
   ❌ /api/v1/customers
   ✅ /customers
   ```

### Every request answers 404 with code 007 or a bare 500, and nothing is logged

**Symptom:** A resource that used to work suddenly answers `404` with `{&quot;errors&quot;:[{&quot;message&quot;:&quot;Not found&quot;,&quot;status&quot;:404,&quot;code&quot;:&quot;007&quot;}]}` or a plain `500 Internal Server Error`, the response carries no exception details, and `data/logs` stays empty.

**Cause:**

The Glue application routes a request to the API Platform kernel only after its own router answered `404`. If the kernel then throws before the router resolved an operation, for example in a `kernel.request` subscriber, an authenticator, or while instantiating one of their dependencies, no exception listener can build a response and the original Glue `404` is sent instead. An exception inside a provider or processor is caught by the API Platform stack and becomes a `500`.

In both cases the exception is logged to the container&apos;s standard error stream, not to a file. In debug mode the response also carries the exception class, message, file, line, and trace.

**Solution:**

1. Read the log of the Glue container, for example:

   ```bash
   docker logs spryker_glue_backend_eu_1 --since 5m 2&gt;&amp;1 | grep -i &quot;exception&quot;
   ```

2. Enable debug mode for local development. `SPRYKER_DEBUG_ENABLED=1` switches the Glue, Glue Backend, and Glue Storefront kernels into debug mode: the Symfony container is rebuilt when code changes, and exception details are rendered in the response. See [Enable debug mode for the Glue kernels](/docs/integrations/spryker-api/api-platform/configuration.html#enable-debug-mode-for-the-glue-kernels).

3. If the `404` persists in debug mode, the route really is unknown to the API Platform kernel. Regenerate the resources and clear the kernel cache:

   ```bash
   docker/sdk cli &quot;GLUE_APPLICATION=GLUE_BACKEND vendor/bin/glue api:generate&quot;
   rm -rf data/cache/GlueBackend/&lt;environment&gt;
   ```

### Requests without an `Accept` header are rejected or return the wrong format

**Symptom:** A client request that omits the `Accept` header — or sends only `Accept: */*` — returns `406 Not Acceptable`, or a response in a format other than the legacy `application/vnd.api+json`. The legacy Glue REST API silently accepted the same request and answered with `application/vnd.api+json`.

**Cause:** API Platform runs content negotiation that requires a satisfiable `Accept` header and does not assume the legacy Glue default. This is a behavioral difference from the legacy Glue REST stack.

**Solution:**

1. Upgrade `spryker/api-platform` to **1.15.0 or higher**. Its `AcceptHeaderFallbackSubscriber` restores the legacy behavior — a missing or `*/*` `Accept` header defaults to `application/vnd.api+json`:

   ```bash
   composer update spryker/api-platform --with-dependencies
   ```

2. If you cannot upgrade, send an explicit `Accept` header from the client:

   ```bash
   curl -H &quot;Accept: application/vnd.api+json&quot; https://glue-backend.mysprykershop.com/customers
   ```

### Pagination not working

**Symptom:** All results returned instead of paginated response.

**Solution:**

1. Enable pagination in the schema file of the defining module:

   ```yaml
   resource:
     paginationEnabled: true
     paginationItemsPerPage: 10
   ```

2. In the provider, read `page[limit]` and `page[offset]` with `buildPaginationTransfer()`, pass the transfer to the facade, and report the total number of results with `setCollectionPagination()`:

   ```php
   $paginationTransfer = $this-&gt;buildPaginationTransfer();
   $criteriaTransfer-&gt;setPagination($paginationTransfer);

   $collectionTransfer = $this-&gt;facade-&gt;getCollection($criteriaTransfer);

   $nbResults = $collectionTransfer-&gt;getPagination()?-&gt;getNbResults();
   if ($nbResults !== null) {
       $this-&gt;setCollectionPagination($paginationTransfer-&gt;getOffsetOrFail(), $paginationTransfer-&gt;getLimitOrFail(), $nbResults);
   }
   ```

3. Use the JSON:API pagination query parameters:

   ```bash
   GET /customers?page[limit]=20&amp;page[offset]=20
   ```

   A page number parameter such as `page=2` is not supported. If `meta.pagination` is missing from the response, the facade did not return `nbResults` in the collection&apos;s `pagination` transfer.

### Client cannot change items per page

**Symptom:** The `page[limit]` query parameter is ignored.

**Solution:**

Enable client-side items-per-page control and set a maximum limit in the resource schema:

```yaml
resource:
  paginationEnabled: true
  paginationItemsPerPage: 10
  paginationClientItemsPerPage: true
  paginationMaximumItemsPerPage: 100
```

Without `paginationClientItemsPerPage: true`, the `itemsPerPage` query parameter has no effect. The `paginationMaximumItemsPerPage` option prevents clients from requesting excessively large pages.

### Client cannot disable pagination

**Symptom:** The `pagination=false` query parameter is ignored and results are still paginated.

**Solution:**

Enable client-side pagination control in the resource schema:

```yaml
resource:
  paginationClientEnabled: true
```

Without `paginationClientEnabled: true`, the `pagination` query parameter has no effect.

For a full reference of all pagination options, see [Resource schemas — Pagination](/docs/integrations/spryker-api/api-platform/resource-schemas.html#pagination).

## Dependency Injection issues

### Services not autowired

**Symptom:**

```bash
Cannot autowire service &quot;CustomerBackendProvider&quot;: argument &quot;$customerFacade&quot;
references class &quot;CustomerFacadeInterface&quot; but no such service exists.
```

**Solution:**

1. Register facade in the respective applications `ApplicationServices.php`:

   ```php
   use Pyz\Zed\Customer\Business\CustomerFacadeInterface;
   use Pyz\Zed\Customer\Business\CustomerFacade;

   $services-&gt;set(CustomerFacadeInterface::class, CustomerFacade::class);
   ```

2. Ensure constructor uses interface type hints:

   ```php
   public function __construct(
       private CustomerFacadeInterface $customerFacade,  // ✅ Interface
   ) {}
   ```

## Performance issues

### Slow API responses

**Symptom:** API endpoints respond slowly.

**Solution:**

1. Verify that Opcache is enabled (`opcache.enable: 1`). Without it, PHP recompiles the whole application on every request, which adds a flat overhead of seconds to every endpoint regardless of the amount of data. See [Opcache activation](/docs/dg/dev/guidelines/performance-guidelines/general-performance-guidelines.html#opcache-activation).
2. Enable Symfony cache:

   ```bash
   docker/sdk cli glue  cache:warmup
   ```

3. Use pagination for collections
4. Optimize database queries in Provider
5. Use API Platform&apos;s built-in caching features

## Development tips

### Debugging schema merging

See which schemas contribute to final resource:

```bash
docker/sdk cli glue  api:debug customers --api-type=backend --show-sources
```

Output:

```bash
Source Files (priority order):
  ✓ vendor/spryker/customer/resources/api/backend/customers.resource.yml (CORE)
  ✓ src/SprykerFeature/CRM/resources/api/backend/customers.resource.yml (FEATURE)
  ✓ src/Pyz/Glue/Customer/resources/api/backend/customers.resource.yml (PROJECT)
```

### Inspecting generated code

View the generated resource class:

```bash
cat src/Generated/Api/Backend/CustomersBackendResource.php
```

Check for:
- Correct property types
- Validation attributes
- API Platform metadata

### Testing with dry-run

Preview generation without writing files:

```bash
docker/sdk cli GLUE_APPLICATION=GLUE_BACKEND glue api:generate --dry-run
```

## Getting help

If you encounter issues not covered here:

1. **Check logs:**

   ```bash
   tail -f var/log/application.log
   tail -f var/log/exception.log
   ```

2. **Enable debug mode:**

   ```php
   &lt;?php

   // config/{APPLICATION}/packages/spryker_api_platform.php

   declare(strict_types = 1);

   use Symfony\Config\SprykerApiPlatformConfig;

   return static function (SprykerApiPlatformConfig $sprykerApiPlatform): void {
       $sprykerApiPlatform-&gt;debug(true);
   };
   ```

3. **Validate environment:**

   ```bash
   php -v  # Check PHP version (8.3+)
   composer show | grep api-platform
   docker/sdk cli glue  debug:container | grep -i api
   ```

4. **Common error patterns:**

| Error | Likely cause | Solution |
|-------|--------------|----------|
| `Class not found` | Autoloading issue | Run `composer dump-autoload` |
| `Service not found` | DI configuration | Check `ApplicationServices.php` |
| `Route not found` | Router not configured | Add `SymfonyFrameworkRouterPlugin` |
| `Validation failed` | Schema mismatch | Regenerate with `--force` |
| `Cache is stale` | Outdated cache | Run `cache:clear` |
| API docs UI broken/unstyled | Assets not installed | Run `docker/sdk cli glue assets:install` |
</description>
            <pubDate>Mon, 14 Sep 2026 09:31:15 +0000</pubDate>
            <link>https://docs.spryker.com/docs/integrations/spryker-api/api-platform/troubleshooting.html</link>
            <guid isPermaLink="true">https://docs.spryker.com/docs/integrations/spryker-api/api-platform/troubleshooting.html</guid>
            
            
        </item>
        
        <item>
            <title>API Platform configuration</title>
            <description>&lt;p&gt;This page is the canonical reference for all API Platform configuration in Spryker. API Platform is configured through &lt;strong&gt;two&lt;/strong&gt; PHP config files per application: the native &lt;code&gt;api_platform.php&lt;/code&gt; (API Platform’s own options, with Spryker adaptations for PHP-based configuration and environment control) and Spryker’s &lt;code&gt;spryker_api_platform.php&lt;/code&gt; (which drives schema generation). Both are documented below.&lt;/p&gt;
&lt;h2 id=&quot;configuration-file-locations&quot;&gt;Configuration file locations&lt;/h2&gt;
&lt;p&gt;Each application layer carries two configuration files:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Application&lt;/th&gt;
&lt;th&gt;Native API Platform config&lt;/th&gt;
&lt;th&gt;Spryker generator config&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Glue&lt;/td&gt;
&lt;td&gt;&lt;code&gt;config/Glue/packages/api_platform.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;config/Glue/packages/spryker_api_platform.php&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GlueStorefront&lt;/td&gt;
&lt;td&gt;&lt;code&gt;config/GlueStorefront/packages/api_platform.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;config/GlueStorefront/packages/spryker_api_platform.php&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GlueBackend&lt;/td&gt;
&lt;td&gt;&lt;code&gt;config/GlueBackend/packages/api_platform.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;config/GlueBackend/packages/spryker_api_platform.php&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;api_platform.php&lt;/code&gt;&lt;/strong&gt; configures API Platform itself (Swagger, formats, pagination defaults, resource mapping paths). Documented under &lt;a href=&quot;#native-api-platform-configuration-apiplatformphp&quot;&gt;Native API Platform configuration&lt;/a&gt; below.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;spryker_api_platform.php&lt;/code&gt;&lt;/strong&gt; configures Spryker’s schema generator (which API types an application serves, where schemas are scanned, which modules stay on Glue). Documented under &lt;a href=&quot;#resource-generation-configuration-sprykerapiplatformphp&quot;&gt;Resource generation configuration&lt;/a&gt; below.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;released-configuration-reference&quot;&gt;Released configuration reference&lt;/h2&gt;
&lt;p&gt;The simplest starting point is a real, released configuration. The links below point to the B2B Demo Marketplace at release &lt;code&gt;release-202604.0&lt;/code&gt;; check newer releases for updates.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Application&lt;/th&gt;
&lt;th&gt;&lt;code&gt;api_platform.php&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;spryker_api_platform.php&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Glue&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://github.com/spryker-shop/b2b-demo-marketplace/blob/release-202604.0/config/Glue/packages/api_platform.php&quot;&gt;api_platform.php&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://github.com/spryker-shop/b2b-demo-marketplace/blob/release-202604.0/config/Glue/packages/spryker_api_platform.php&quot;&gt;spryker_api_platform.php&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GlueStorefront&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://github.com/spryker-shop/b2b-demo-marketplace/blob/release-202604.0/config/GlueStorefront/packages/api_platform.php&quot;&gt;api_platform.php&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://github.com/spryker-shop/b2b-demo-marketplace/blob/release-202604.0/config/GlueStorefront/packages/spryker_api_platform.php&quot;&gt;spryker_api_platform.php&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GlueBackend&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://github.com/spryker-shop/b2b-demo-marketplace/blob/release-202604.0/config/GlueBackend/packages/api_platform.php&quot;&gt;api_platform.php&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://github.com/spryker-shop/b2b-demo-marketplace/blob/release-202604.0/config/GlueBackend/packages/spryker_api_platform.php&quot;&gt;spryker_api_platform.php&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&quot;resource-generation-configuration-sprykerapiplatformphp&quot;&gt;Resource generation configuration (spryker_api_platform.php)&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;spryker_api_platform.php&lt;/code&gt; controls Spryker’s API Platform schema generator — it is separate from the native &lt;code&gt;api_platform.php&lt;/code&gt;. Create one in each application layer where you enable API Platform. The files share the same shape; they differ only in &lt;code&gt;apiTypes()&lt;/code&gt; and in the modules each application still serves via Glue.&lt;/p&gt;
&lt;p&gt;Three settings:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;apiTypes()&lt;/code&gt; — the API types this application serves: &lt;code&gt;[&apos;storefront&apos;]&lt;/code&gt; for the Glue and GlueStorefront applications, &lt;code&gt;[&apos;backend&apos;]&lt;/code&gt; for the GlueBackend application.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sourceDirectories()&lt;/code&gt; — where the generator scans for API Platform schemas. Optional; defaults to &lt;code&gt;src/Spryker&lt;/code&gt;, &lt;code&gt;src/SprykerFeature&lt;/code&gt;, and &lt;code&gt;src/Pyz&lt;/code&gt;. Set it only if your schemas live somewhere else.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;excludedPathFragments()&lt;/code&gt; — schema paths the generator skips. Use it to keep a module’s API Platform schemas hidden from the generator (for example, a module the project still serves via Glue). Each entry is matched as a substring of the full schema path.&lt;/li&gt;
&lt;/ul&gt;
&lt;section class=&apos;info-block info-block--warning&apos;&gt;&lt;i class=&apos;info-block__icon icon-warning&apos;&gt;&lt;/i&gt;&lt;div class=&apos;info-block__content&apos;&gt;&lt;div class=&quot;info-block__title&quot;&gt;excludedPathFragments does not switch routing&lt;/div&gt;
&lt;p&gt;&lt;code&gt;excludedPathFragments&lt;/code&gt; controls only what the schema generator emits. It does &lt;strong&gt;not&lt;/strong&gt; flip routing. A module stays on Glue as long as its &lt;code&gt;*ResourceRoutePlugin&lt;/code&gt; is registered in the project dependency provider. See &lt;a href=&quot;/docs/integrations/spryker-api/migrate-from-glue-to-api-platform/migrate-to-api-platform-overview.html#step-3--batch-migration-default&quot;&gt;Step 3 — Batch migration in the migration overview&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;&lt;/section&gt;
&lt;p&gt;&lt;code&gt;config/Glue/packages/spryker_api_platform.php&lt;/code&gt; (storefront example):&lt;/p&gt;
&lt;div class=&quot;language-php highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;declare&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;strict_types&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Symfony\Config\SprykerApiPlatformConfig&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;SprykerApiPlatformConfig&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$sprykerApiPlatform&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;$sprykerApiPlatform&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;apiTypes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;storefront&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]);&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// Keep these modules on the Glue REST stack by hiding their API Platform schemas from the generator.&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;$sprykerApiPlatform&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;excludedPathFragments&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([&lt;/span&gt;
        &lt;span class=&quot;s1&quot;&gt;&apos;vendor/spryker/customer/src/Spryker/Customer/resources/api/&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;s1&quot;&gt;&apos;vendor/spryker/store/src/Spryker/Store/resources/api/&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;s1&quot;&gt;&apos;vendor/spryker/authentication/src/Spryker/Authentication/resources/api/&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;]);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;For the GlueBackend application, set &lt;code&gt;apiTypes([&apos;backend&apos;])&lt;/code&gt; and list the modules that application still serves via Glue in &lt;code&gt;excludedPathFragments()&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;native-api-platform-configuration-apiplatformphp&quot;&gt;Native API Platform configuration (api_platform.php)&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;api_platform.php&lt;/code&gt; configures API Platform itself. Spryker ships working defaults, so most projects only adjust a few options. The sections below cover the Spryker-specific adaptations and the settings you are most likely to change.&lt;/p&gt;
&lt;h3 id=&quot;spryker-specific-differences&quot;&gt;Spryker-specific differences&lt;/h3&gt;
&lt;h4 id=&quot;php-configuration-instead-of-yaml&quot;&gt;PHP configuration instead of YAML&lt;/h4&gt;
&lt;p&gt;Spryker uses PHP configuration with Symfony’s type-safe configuration objects instead of YAML files:&lt;/p&gt;
&lt;div class=&quot;language-php highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Symfony\Config\ApiPlatformConfig&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;ApiPlatformConfig&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$apiPlatform&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$env&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// Configuration here&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;h4 id=&quot;environment-variable-for-conditional-settings&quot;&gt;Environment variable for conditional settings&lt;/h4&gt;
&lt;p&gt;The configuration function receives an &lt;code&gt;$env&lt;/code&gt; parameter for environment-specific behavior.&lt;/p&gt;
&lt;p&gt;Common environment values: &lt;code&gt;prod&lt;/code&gt;, &lt;code&gt;dev&lt;/code&gt;, &lt;code&gt;dockerdev&lt;/code&gt;, &lt;code&gt;test&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;For a full example, see &lt;a href=&quot;#enable-the-documentation-ui-only-in-development&quot;&gt;Enable the documentation UI only in development&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;configuration-examples&quot;&gt;Configuration examples&lt;/h3&gt;
&lt;h4 id=&quot;enable-the-documentation-ui-only-in-development&quot;&gt;Enable the documentation UI only in development&lt;/h4&gt;
&lt;p&gt;By default, Spryker shows the documentation UI (Swagger UI) only in development environments. You can configure this behavior based on the &lt;code&gt;$env&lt;/code&gt; parameter:&lt;/p&gt;
&lt;div class=&quot;language-php highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;ApiPlatformConfig&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$apiPlatform&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$env&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// Enable developer tools only in development&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$env&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;dockerdev&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;$apiPlatform&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;enableSwagger&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;$apiPlatform&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;enableSwaggerUi&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;$apiPlatform&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;enableReDoc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;$apiPlatform&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;enableDocs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;h4 id=&quot;enable-debug-mode-for-the-glue-kernels&quot;&gt;Enable debug mode for the Glue kernels&lt;/h4&gt;
&lt;p&gt;The Symfony kernel of each Glue application takes its debug flag from the application’s own configuration constant, not from &lt;code&gt;APPLICATION_ENV&lt;/code&gt;. The Docker dev configuration wires all three to the &lt;code&gt;SPRYKER_DEBUG_ENABLED&lt;/code&gt; environment variable:&lt;/p&gt;
&lt;div class=&quot;language-php highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// config/Shared/config_default-docker.dev.php&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;$config&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;GlueApplicationConstants&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;ENABLE_APPLICATION_DEBUG&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$config&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;GlueBackendApiApplicationConstants&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;ENABLE_APPLICATION_DEBUG&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$config&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;GlueStorefrontApiApplicationConstants&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;ENABLE_APPLICATION_DEBUG&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bool&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;getenv&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;SPRYKER_DEBUG_ENABLED&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;With debug enabled, the Symfony container is rebuilt when a resource, provider, or configuration file changes, the API Platform error renderer includes the exception message and trace in error responses, and &lt;code&gt;spryker_api_platform.debug&lt;/code&gt; follows &lt;code&gt;%kernel.debug%&lt;/code&gt;. With debug disabled, uncaught exceptions are reduced to a generic &lt;code&gt;500&lt;/code&gt; and are only visible in the container log.&lt;/p&gt;
&lt;h4 id=&quot;disable-doctrine-integration&quot;&gt;Disable Doctrine integration&lt;/h4&gt;
&lt;p&gt;Spryker does not use Doctrine with API Platform:&lt;/p&gt;
&lt;div class=&quot;language-php highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$apiPlatform&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;doctrine&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;enabled&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;$apiPlatform&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;doctrineMongodbOdm&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;enabled&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;h4 id=&quot;configure-resource-mapping-paths&quot;&gt;Configure resource mapping paths&lt;/h4&gt;
&lt;p&gt;Specify where API Platform discovers resource classes. By default, only the generated resource directory is configured:&lt;/p&gt;
&lt;div class=&quot;language-php highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$apiPlatform&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;mapping&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;paths&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;&apos;%kernel.project_dir%/src/Generated/Api/Backend&apos;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;h4 id=&quot;add-custom-resource-paths&quot;&gt;Add custom resource paths&lt;/h4&gt;
&lt;p&gt;To use native API Platform resources alongside generated resources, add your directories to the mapping paths:&lt;/p&gt;
&lt;div class=&quot;language-php highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$apiPlatform&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;mapping&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;paths&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;&apos;%kernel.project_dir%/src/Generated/Api/Backend&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;&apos;%kernel.project_dir%/src/Pyz/Glue/*/Api/Backend/Resource&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;API Platform scans all configured paths for PHP classes with &lt;code&gt;#[ApiResource]&lt;/code&gt; attributes. Generated and manually created resources coexist without conflict.&lt;/p&gt;
&lt;section class=&apos;info-block info-block--warning&apos;&gt;&lt;i class=&apos;info-block__icon icon-warning&apos;&gt;&lt;/i&gt;&lt;div class=&apos;info-block__content&apos;&gt;&lt;div class=&quot;info-block__title&quot;&gt;Keep the generated path&lt;/div&gt;
&lt;p&gt;Always keep the &lt;code&gt;src/Generated/Api/{ApiType}&lt;/code&gt; path in the list. Removing it disables all YAML-generated resources.&lt;/p&gt;
&lt;/div&gt;&lt;/section&gt;
&lt;p&gt;For a complete guide on creating native resources, see &lt;a href=&quot;/docs/integrations/spryker-api/api-platform/native-api-platform-resources.html&quot;&gt;Native API Platform resources&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id=&quot;set-pagination-defaults&quot;&gt;Set pagination defaults&lt;/h4&gt;
&lt;div class=&quot;language-php highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$apiPlatform&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;defaults&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;paginationItemsPerPage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;nv&quot;&gt;$apiPlatform&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;collection&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;pagination&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;pageParameterName&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;page&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;itemsPerPageParameterName&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;itemsPerPage&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;These global defaults apply to all resources. Individual resources can override pagination behavior using per-resource options such as &lt;code&gt;paginationEnabled&lt;/code&gt;, &lt;code&gt;paginationItemsPerPage&lt;/code&gt;, &lt;code&gt;paginationMaximumItemsPerPage&lt;/code&gt;, &lt;code&gt;paginationClientEnabled&lt;/code&gt;, and &lt;code&gt;paginationClientItemsPerPage&lt;/code&gt; in their YAML schema files. See &lt;a href=&quot;/docs/integrations/spryker-api/api-platform/resource-schemas.html#pagination&quot;&gt;Resource schemas — Pagination&lt;/a&gt; for details.&lt;/p&gt;
&lt;h4 id=&quot;configure-supported-formats&quot;&gt;Configure supported formats&lt;/h4&gt;
&lt;div class=&quot;language-php highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$apiPlatform&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;formats&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;jsonapi&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;mime_types&apos;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;application/vnd.api+json&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]]);&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;$apiPlatform&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;formats&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;jsonld&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;mime_types&apos;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;application/ld+json&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]]);&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;$apiPlatform&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;formats&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;xml&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;mime_types&apos;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;application/xml&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;h4 id=&quot;configure-security&quot;&gt;Configure security&lt;/h4&gt;
&lt;p&gt;Security is configured in a separate &lt;code&gt;security.php&lt;/code&gt; file. For details, see &lt;a href=&quot;/docs/integrations/spryker-api/authenticating-and-authorization/integrate-api-platform-security.html&quot;&gt;Integrate API Platform security&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;complete-configuration-reference&quot;&gt;Complete configuration reference&lt;/h2&gt;
&lt;p&gt;For all available configuration options and their details, refer to the &lt;a href=&quot;https://api-platform.com/docs/core/configuration/#symfony-configuration&quot;&gt;API Platform Symfony Configuration documentation&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The PHP method names in &lt;code&gt;ApiPlatformConfig&lt;/code&gt; correspond directly to the YAML keys in the official documentation.&lt;/p&gt;
</description>
            <pubDate>Mon, 14 Sep 2026 09:31:15 +0000</pubDate>
            <link>https://docs.spryker.com/docs/integrations/spryker-api/api-platform/configuration.html</link>
            <guid isPermaLink="true">https://docs.spryker.com/docs/integrations/spryker-api/api-platform/configuration.html</guid>
            
            
        </item>
        
        <item>
            <title>Resource schemas</title>
            <description>This document explains how to define API Platform resource schemas in Spryker.

## Schema file structure

API Platform uses YAML files to define resource schemas. Resource schemas describe the structure, operations, and behavior of your API resources.

### Schema location

Resource schemas must be placed in the `resources/api/{api-type}/` directory within your module:

```MARKDOWN
src/
├── Spryker/
│   └── {Module}/
│       └── resources/
│           └── api/
│               ├── storefront/
│               │   └── resource-name.resource.yml
│               └── backend/
│                   └── resource-name.resource.yml
├── SprykerFeature/
│   └── {Feature}/
│       └── resources/
│           └── api/
│               └── backend/
│                   └── resource-name.resource.yml
└── Pyz/
    └── Glue/
        └── {Module}/
            └── resources/
                └── api/
                    └── backend/
                        └── resource-name.resource.yml
```

## CodeBucket resources

API Platform supports CodeBucket-specific resource variants that are resolved at runtime based on the `APPLICATION_CODE_BUCKET` environment constant. A variant keeps the same file name as the base schema but lives in a separate variant module directory—for example, `StoresApiEU`—and sets the `codeBucket:` property inside the schema file. The generator produces one class per variant following the `{ResourceName}{CodeBucket}{ApiType}Resource` pattern, and the base resource is used when no matching variant exists. For file naming, class naming, URL behavior, and implementation examples, see [CodeBucket support](/docs/integrations/spryker-api/api-platform/code-buckets.html).

## Resource schema syntax

### Minimal example

```yaml
resource:
  name: Products
  shortName: products
  description: &quot;Product resource&quot;

  operations:
    - type: Get
    - type: GetCollection

  properties:
    id:
      type: integer
      writable: false
      identifier: true

    name:
      type: string
```

{% info_block infoBox &quot;shortName convention&quot; %}

`shortName` is the JSON:API `type` field for the resource and is used as the public URL segment. Use **lowercase kebab-case**, plural for noun-style resources (`products`, `addresses`, `abstract-product-prices`) and singular for action-style endpoints (`catalog-search`, `cart-reorder`). Multi-word names are always hyphenated. This matches every shipped resource in the platform.

{% endinfo_block %}

### Complete example with all options

```yaml
# yaml-language-server: $schema=../../../../../vendor/spryker/api-platform/resources/schemas/api-resource-schema-v1.json

resource:
  # Resource identification
  name: Customers                    # Internal name (used for schema merging)
  shortName: customers               # URL name (becomes /customers); JSON:API type field
  description: &quot;Customer resource&quot;   # OpenAPI description

  # State providers and processors
  provider: &quot;Pyz\\Glue\\Customer\\Api\\Backend\\Provider\\CustomerBackendProvider&quot;
  processor: &quot;Pyz\\Glue\\Customer\\Api\\Backend\\Processor\\CustomerBackendProcessor&quot;

  # Pagination configuration
  paginationEnabled: true
  paginationItemsPerPage: 10
  paginationMaximumItemsPerPage: 100
  paginationClientEnabled: true
  paginationClientItemsPerPage: true

  # JSON:API `included` array ordering — see &quot;Sort priority for included resources&quot;
  includedSortPriority: 0

  # Security
  security: &quot;is_granted(&apos;ROLE_ADMIN&apos;)&quot;
  securityPostDenormalize: &quot;is_granted(&apos;EDIT&apos;, object)&quot;

  # Operations
  operations:
    - type: Post                     # Create new resource
    - type: Get                      # Get single resource
    - type: GetCollection            # Get collection with pagination
    - type: Put                      # Replace entire resource
    - type: Patch                    # Update partial resource
    - type: Delete                   # Delete resource

  # Relationships — see Relationships article for full reference
  includes:
    - relationshipName: addresses
      targetResource: CustomersAddresses
      uriVariableMappings:
        customerReference: customerReference

  # Properties
  properties:
    idCustomer:
      type: integer
      description: &quot;The unique identifier of the customer.&quot;
      writable: false                # Read-only property
      readable: true                 # Include in responses (default: true)

    email:
      type: string
      description: &quot;The email address.&quot;
      required: true                 # Required for all operations
      openapiContext:
        example: &quot;john@example.com&quot;
        format: &quot;email&quot;

    firstName:
      type: string
      description: &quot;First name.&quot;
      openapiContext:
        example: &quot;John&quot;
        minLength: 1
        maxLength: 100

    status:
      type: string
      description: &quot;Customer status.&quot;
      openapiContext:
        example: &quot;active&quot;
        schema:
          enum: [&quot;active&quot;, &quot;inactive&quot;, &quot;pending&quot;]

    customerReference:
      type: string
      description: &quot;Unique customer reference.&quot;
      writable: false
      identifier: true               # Use as URL identifier instead of @id

    dateOfBirth:
      type: string
      description: &quot;Date of birth.&quot;
      openapiContext:
        format: &quot;date&quot;
        example: &quot;1990-01-01&quot;

    isActive:
      type: boolean
      description: &quot;Active status.&quot;
      default: true

    creditLimit:
      type: number
      description: &quot;Credit limit.&quot;
      openapiContext:
        format: &quot;float&quot;
        example: 5000.00
```

## Property types

### Supported types

| Type | PHP Type | Example | Description |
|------|----------|---------|-------------|
| `string` | `string` | `&quot;John&quot;` | Text values |
| `integer` | `int` | `42` | Whole numbers |
| `number` | `float` | `3.14` | Decimal numbers |
| `boolean` | `bool` | `true` | True/false values |
| `array` | `array` | `[&quot;a&quot;, &quot;b&quot;]` | Lists of values. Add an `items` sibling to publish a typed element schema instead of an untyped array — see [Object collections](#object-collections) and [Typed collections in the published contract](/docs/integrations/spryker-api/api-platform/typed-collections.html). |
| `object` | `object` | `{&quot;key&quot;: &quot;value&quot;}` | Strictly typed nested objects — generates a typed companion class. See [Typed nested objects](#typed-nested-objects). A project can also share one shape across resources with a [canonical nested object](#project-defined-canonical-nested-objects). |
| `map` | `array` | `{&quot;key&quot;: &quot;value&quot;}` | Free-shape associative payloads documented via `openapiContext`. Stored as PHP `array` and rendered as `type: object` in the OpenAPI specification. |
| `mixed` | `mixed` | any | Use only when the payload genuinely has no fixed shape and cannot be described via `openapiContext`. |

Use `map` when the payload is a structured JSON object whose schema you want to describe via
`openapiContext` rather than a strongly typed PHP class. This is the recommended type whenever a
request or response body is a JSON object with a known shape but no dedicated class — it
keeps the property typed as a simple `array` in PHP while still producing rich OpenAPI metadata
and a working &quot;Try Out&quot; body in Swagger UI. See
[Documenting nested properties for OpenAPI and Swagger UI](#documenting-nested-properties-for-openapi-and-swagger-ui)
for the full pattern.

When you do want a strongly typed class for the payload — so PHP enforces the field set and the
OpenAPI document publishes a named component schema — use `type: object` with nested
`properties:` instead. See [Typed nested objects](#typed-nested-objects).

### Property attributes

#### writable

Controls if property can be sent in requests (POST/PUT/PATCH):

```yaml
password:
  type: string
  writable: true    # Can be sent in requests
  readable: false   # Not included in responses
```

#### readable

Controls if property is included in responses:

```yaml
idCustomer:
  type: integer
  writable: false   # Cannot be modified
  readable: true    # Included in responses
```

#### identifier

Marks property as URL identifier:

```yaml
customerReference:
  type: string
  identifier: true  # URL becomes /customers/{customerReference}
```

#### required

Makes property mandatory (use validation schemas for detailed rules):

```yaml
email:
  type: string
  required: true    # Must be present
```

#### default

Sets default value:

```yaml
isActive:
  type: boolean
  default: true     # Defaults to true if not provided
```

## Typed nested objects

A property declared as `type: object` with its own nested `properties:` block generates a
dedicated, strongly typed companion class — not an untyped array. The generator emits one PHP
class per nested object, types the parent property to that class, and publishes a full
field-by-field schema in the OpenAPI document. The serializer hydrates the nested object from the
same JSON payload, so the response on the wire is identical to the array-based form it replaces.

This is the strongly typed counterpart to the `map` pattern described in
[Documenting nested properties for OpenAPI and Swagger UI](#documenting-nested-properties-for-openapi-and-swagger-ui):
`map` documents a nested object while keeping it a plain PHP `array`; `type: object` promotes it
to a real class whose shape is enforced by PHP&apos;s type system.

### Why use it

- **Type safety in PHP.** The parent property is typed to the generated class (for example,
  `?CartsTotalsStorefrontObject`) instead of `array`, so providers and processors get IDE
  autocompletion and the language enforces the field set.
- **Precise OpenAPI schema.** Each sub-field carries its own `type`, `description`, and `example`,
  so the OpenAPI document and Swagger UI render the object as a named component schema instead of
  an opaque `object`.
- **No runtime contract change.** Because the serializer denormalizes the typed object from the
  same keys, migrating a property from `array`/`map` to `type: object` leaves the JSON response
  unchanged — only the generated PHP and the published schema improve.

### When to use which type

| Use | When |
|-----|------|
| `type: object` (with `properties`) | The payload has a **stable, known shape** you want enforced as a PHP class — for example, cart and order `totals`, or a quote-request `customer`. |
| `type: map` (with `openapiContext`) | The shape is known and worth documenting, but you do **not** want a dedicated PHP class — for example, payloads aggregated from several transfer objects, or PSP-specific responses. See [Documenting nested properties for OpenAPI and Swagger UI](#documenting-nested-properties-for-openapi-and-swagger-ui). |
| `type: mixed` | The payload genuinely has **no fixed shape** and cannot be described via `openapiContext`. |

### How to declare it

Give the property `type: object` and nest its fields under `properties:`. Sub-fields accept the
same attributes as top-level properties (`type`, `description`, `openapiContext`, `nullable`,
`serializedName`, `serializedPath`):

```yaml
totals:
    type: object
    readable: true
    writable: false
    required: false
    description: &apos;Calculated cart totals in cents.&apos;
    properties:
        subtotal:
            type: integer
            description: &apos;Items × prices before any discount/tax.&apos;
            openapiContext: { example: 16058 }
        grandTotal:
            type: integer
            description: &apos;What the customer pays.&apos;
            openapiContext: { example: 14601 }
        priceToPay:
            type: integer
            description: &apos;Grand total adjusted for any pre-paid amount (e.g. gift cards).&apos;
            openapiContext: { example: 14601 }
```

### Generated output

For a `Carts` resource with the `totals` property above, the generator:

1. Types the property on the resource class:

   ```php
   public ?CartsTotalsStorefrontObject $totals = null;
   ```

2. Writes a companion class in the `Generated\Api\{ApiType}\{ResourceName}\` namespace (a
   sub-namespace named after the owning resource, alongside the resource class in
   `Generated\Api\{ApiType}\`). The class is `final`, carries **no** `#[ApiResource]` attribute —
   it is an embedded value object, not a routed resource — and exposes the typed sub-fields plus
   their accessors:

   ```php
   namespace Generated\Api\Storefront\Carts;

   use ApiPlatform\Metadata\ApiProperty;

   final class CartsTotalsStorefrontObject
   {
       #[ApiProperty(description: &apos;Items × prices before any discount/tax.&apos;, openapiContext: [&apos;example&apos; =&gt; 16058])]
       public ?int $subtotal = null;

       #[ApiProperty(description: &apos;What the customer pays.&apos;, openapiContext: [&apos;example&apos; =&gt; 14601])]
       public ?int $grandTotal = null;

       #[ApiProperty(description: &apos;Grand total adjusted for any pre-paid amount (e.g. gift cards).&apos;, openapiContext: [&apos;example&apos; =&gt; 14601])]
       public ?int $priceToPay = null;

       // Getters, setters, toArray(), fromArray() …
   }
   ```

The companion class name is `{ResourceName}{PropertyPath}{ApiType}Object` — the resource&apos;s
normalized name, the capitalized property path, the API type, and the `Object` suffix (contrast
the routed resource class itself, which keeps the `Resource` suffix). It lives in the
`Generated\Api\{ApiType}\{ResourceName}` sub-namespace. So `Carts` + `totals` on the storefront API
becomes `Generated\Api\Storefront\Carts\CartsTotalsStorefrontObject`; a checkout `billingAddress`
becomes `Generated\Api\Storefront\Checkout\CheckoutBillingAddressStorefrontObject`.

{% info_block infoBox &quot;Imports in companion classes&quot; %}

Companion classes import only the attributes they actually use (`ApiProperty`, `SerializedName`,
`SerializedPath`). An attribute referenced without its `use` statement would resolve to a
non-existent class in the `Generated` namespace and break attribute reflection at runtime, so the
generator never emits an unused import.

{% endinfo_block %}

### Nested objects within objects

Objects can nest to any depth. Each level generates its own class, named by concatenating the
property path onto the resource name. For example:

```yaml
totals:
    type: object
    properties:
        tax:
            type: object
            properties:
                amount:
                    type: integer
                    description: &apos;Tax amount in cents.&apos;
                    openapiContext: { example: 1457 }
```

on the storefront `Carts` resource generates a `CartsTotalsStorefrontObject` class with
`public ?CartsTotalsTaxStorefrontObject $tax = null;`, plus a separate
`CartsTotalsTaxStorefrontObject` class with `public ?int $amount = null;` (both in the
`Generated\Api\Storefront\Carts` namespace). A deeper path simply keeps concatenating — an agent
quote-request resource&apos;s `shownVersion.cartTotals` object becomes
`AgentQuoteRequestsShownVersionCartTotalsStorefrontObject`.

### Object collections

A `type: array` property whose `items:` are themselves a typed object (`type: object` with nested
`properties:`) generates a value-object class for the element type. The class is named after the
**pluralized** field segment — `{ResourceName}{PluralField}{ApiType}Object` — and the parent
property stays a PHP `array` carrying a `@var array&lt;…&gt;` docblock so the serializer denormalizes
each element into the generated class:

```yaml
# carts.resource.yml — a list of typed customer objects
customer:
    type: array
    items:
        type: object
        properties:
            firstName: { type: string }
            email:     { type: string }
```

On the storefront `Carts` resource this generates `CartsCustomersStorefrontObject` (the field
`customer` pluralized to `Customers`) as the element type, and types the property as
`array&lt;\Generated\Api\Storefront\Carts\CartsCustomersStorefrontObject&gt;`.

In the published contract, the property becomes `&quot;type&quot;: &quot;array&quot;` with an `items` reference to the
generated element schema, which API Platform registers in the same document. Without an `items` block,
the property publishes as a bare array with no element description.

{% info_block warningBox &quot;Typing an existing list is a backward-compatibility decision&quot; %}

Generated value objects copy only the fields you declare, so adding an `items` block to a list that is
already part of a released response silently drops every payload key missing from `items.properties`.
Check a real payload first — see
[Typed collections in the published contract](/docs/integrations/spryker-api/api-platform/typed-collections.html).

{% endinfo_block %}

### Per-resource validation lifting

Each typed nested object gets its **own** value-object class, so validation you authored the
array-shaped way — an `Assert\Collection` on the object property in the resource&apos;s
`{resource-name}.validation.yml` — would reject the denormalized object value with a 422
(`This value should be of type array`). The generator resolves this automatically: for a writable
object property it **lifts** the `Collection.fields` constraints off the property and onto the
matching fields of that resource&apos;s value object, and emits a plain `#[Assert\Valid]` cascade
(carrying the operation groups) on the property instead of the `Collection`.

You keep authoring validation exactly as before — write the `Collection` against the object
property:

```yaml
# checkout-data.validation.yml
post:
    customer:
        - Optional:
              constraints:
                  - Collection:
                        allowExtraFields: true
                        fields:
                            email:
                                - NotBlank: { message: &apos;Email is invalid.&apos; }
                                - Email:    { message: &apos;Email is invalid.&apos; }
```

The lifted constraints are re-grouped through the resource&apos;s own operation groups (so this
`checkout-data` `customer.email` rule stays in the `checkout-data:create` group) and attached to
the value object&apos;s `email` field; the `customer` property itself carries only `#[Assert\Valid]`.
Each resource&apos;s value object is validated independently — there is **no** cross-resource union,
because every resource has its own value-object class. A property whose object is not writable, or
a plain list property that is not a typed object collection, keeps its array-shaped `Collection` —
only writable typed-object properties are lifted.

#### `allowMissingFields`

A `Collection` with `allowMissingFields: true` (for example, a checkout `billingAddress` referenced
only by id) tolerates absent keys. On a value object an absent field denormalizes to `null`, so the
generator relaxes presence constraints when lifting: each `NotBlank` gains `allowNull: true` and
each `NotNull` is dropped — an absent field passes, a present-but-empty one still fails.

### Cross-module field contribution

Because each resource owns its value-object class, a nested object&apos;s fields can still be
contributed from several modules — this is how you keep the dependency direction correct, with
each field declared in its owning module. Multiple modules ship a same-named `*.resource.yml`
fragment for the same resource, and the schema merger **deep-merges nested object `properties`**
(and `items.properties` for collections) rather than letting a later fragment&apos;s nested block
replace an earlier one.

For example, both `DiscountsRestApi` and `ProductOptionsRestApi` add fields to the cart-items
`calculations` object:

```yaml
# DiscountsRestApi — cart-items.resource.yml
resource:
    name: CartItems
    properties:
        calculations:
            type: object
            properties:
                discountTotal: { type: integer }

# ProductOptionsRestApi — cart-items.resource.yml
resource:
    name: CartItems
    properties:
        calculations:
            type: object
            properties:
                productOptionTotal: { type: integer }
```

The merged `calculations` object carries **both** `discountTotal` and `productOptionTotal`, and a
single `CartItemsCalculationsStorefrontObject` value object is generated for it. This deep merge —
not a shared class — is how identically-named objects accumulate fields across modules while each
resource keeps its own independent request/response shape.

#### Conflicting shapes fail generation

Deep merge only applies when the contributors agree on the shape. When one contributor declares a
property as a typed object (`type: object` with `properties`) or an object collection (`type: array`
with `items.properties`) and another declares the **same** property as something structurally
different — a `map`, a scalar, a plain array, or an object without `properties` — a silent
last-wins merge would drop either the typed value object or the plain field. Instead, generation
**fails with an error** that names the property and both contributing source files:

```text
Conflicting shapes for property &quot;calculations&quot;: .../DiscountsRestApi/.../cart-items.resource.yml
declares it as a typed object (`type: object` with `properties`), but
.../project/.../cart-items.resource.yml declares it as `type: map`. ...
```

This applies both within a layer and across layers (project overrides feature overrides core). The
usual cause is a project fragment that still declares a property as `type: map`/`array` while a core
module has since promoted it to a typed object — convert the project fragment to the typed form.
Same-shape overrides (object + object, collection + collection) still deep-merge, and attribute-only
overrides (an override that sets, for example, `writable: false` without re-declaring `type`) merge
as before.

If you deliberately intend to re-shape an inherited property — for example, collapse a core typed
object back into a `map`, or replace it wholesale rather than extend it — set `replace: true` on the
overriding declaration. It takes your declaration wholesale (the inherited one is discarded),
suppresses the conflict guard, and is stripped from the generated output:

```yaml
# project cart-items.resource.yml — deliberately override the core shape
calculations:
    type: map
    replace: true
```

## Project-defined canonical nested objects

[Typed nested objects](#typed-nested-objects) generate one value-object class **per resource
property**: a `billingAddress` on the checkout resource and a `shippingAddress` on the order
resource each get their own independent class, even when both describe the same real-world shape.
That keeps each resource self-contained, but it also means the same address shape is authored and
maintained in several places.

A **canonical nested object** lets a project define that shared shape **once** and have it flow
into every resource property that opts in. All the opting-in properties then collapse onto a single
generated class — `Generated\Api\{ApiType}\{Object}` (for example, `Generated\Api\Storefront\Address`) —
instead of a per-resource companion class.

This is a pure project opt-in. With no canonical object files present, generation is byte-for-byte
identical to the default per-resource behavior described above — nothing changes until a project
adds its first `*.object.yml`.

### File location and naming

Canonical objects live in a **dedicated, reserved subdirectory literally named `objects/`** inside the per-`apiType` resource directory. The directory name is always `objects` — it is never named after a resource or module. This is distinct from resource definition files, which live directly in the `apiType` directory:

```text
resources/api/storefront/
├── checkout.resource.yml          # a resource definition
├── checkout.validation.yml        # its validation
└── objects/                       # reserved dir — canonical objects only
    ├── address.object.yml
    └── address.object.validation.yml
```

Only `*.object.yml` and `*.object.validation.yml` files belong in `objects/`. Resource files (`*.resource.yml`) are placed directly in the per-`apiType` directory, never inside `objects/`.

The `&lt;dashed-name&gt;.&lt;kind&gt;.yml` naming pattern is the same for both file types — only the kind word differs. `address.object.yml` is the canonical-object analog of `checkout.resource.yml`, and `address.object.validation.yml` is the analog of `checkout.validation.yml`. The `object` versus `resource` word identifies the artifact kind, not a different naming scheme.

Full path patterns:

```text
resources/api/&lt;apiType&gt;/objects/&lt;dashed-name&gt;.object.yml
resources/api/&lt;apiType&gt;/objects/&lt;dashed-name&gt;.object.validation.yml   # optional, see Validation
```

For example, on the storefront API:

```text
src/Pyz/resources/api/storefront/objects/address.object.yml
src/Pyz/resources/api/storefront/objects/address.object.validation.yml
```

The file name uses a dashed (kebab-case) object name, while `object.name` **inside** the file is
CamelCase. The CamelCase `object.name` is the contract: it must exactly match the `objectName:`
join tag declared on the resource properties that want this shape (see [The `objectName` join
tag](#the-objectname-join-tag)).

### Central directory

A project may keep canonical object files in one central location instead of (or in addition to) the per-module `objects/` directories. Both locations are scanned simultaneously.

Configure the central directory via the Symfony bundle config node `spryker_api_platform.canonical_object_search_directories`, keyed by API type. Relative paths resolve against the project root; `%kernel.project_dir%` is also supported:

```yaml
# config/packages/spryker_api_platform.yaml
spryker_api_platform:
    canonical_object_search_directories:
        storefront:
            - &apos;%kernel.project_dir%/config/api/objects/storefront&apos;
```

The same `*.object.yml` / `*.object.validation.yml` naming rules apply. Files in a central directory are always treated as the **project** layer, so they participate in the standard `project &gt; feature &gt; core` merge precedence.

Defining the same `objectName` more than once within the same layer — for example, one module file and one central-directory file both at project layer — is a fail-loud error: generation aborts with an `ApiSchemaGenerationException` naming both source files. The same name across different layers is fine — that is the normal override.

### File format

The file contains a single top-level `object:` key:

```yaml
# address.object.yml
object:
    name: Address                                   # CamelCase; matches `objectName: Address` on resource properties
    properties:
        salutation: { type: string, description: &apos;Address salutation.&apos;, example: &apos;Mr&apos; }
        firstName:  { type: string, description: &apos;First name.&apos;, example: &apos;Jane&apos; }
        lastName:   { type: string, description: &apos;Last name.&apos;, example: &apos;Doe&apos; }
        address1:   { type: string, description: &apos;Street name.&apos;, example: &apos;Julie-Wolfthorn-Straße&apos; }
        zipCode:    { type: string, description: &apos;ZIP / postal code.&apos;, example: &apos;10115&apos; }
        city:       { type: string, description: &apos;City.&apos;, example: &apos;Berlin&apos; }
```

| Key | Type | Required | Description |
|-----|------|----------|-------------|
| `object.name` | string | Yes | CamelCase object name. Matched against `objectName:` join tag on every resource property that references this object. |
| `object.properties` | map | Yes | Field definitions. Each field uses the **same syntax as a resource property** — `type`, `description`, `validation`, `example`, and so on. |
| `object.extends` | string | No | CamelCase name of another canonical object whose resolved fields are inherited first. See [Composition](#composition-with-extends-and-omit). |
| `object.omit` | string[] | No | Names of inherited fields to drop from the `extends` base before this object&apos;s own properties are applied. |

### Composition with `extends` and `omit`

An object can inherit another canonical object&apos;s fields with `extends`, then trim and extend them.
This avoids re-declaring a shared shape when one variant is a near-copy of another — for example, a
read-only address snapshot derived from a writable address:

```yaml
# address-snapshot.object.yml
object:
    name: AddressSnapshot
    extends: Address                                # inherit all Address fields first
    omit: [id, idCompanyBusinessUnitAddress]        # drop the write-only identifiers
    properties:
        country: { type: string, description: &apos;Country name.&apos;, example: &apos;Germany&apos; }   # add a read-only field
```

Fields resolve in this order, with later steps winning:

1. The fields inherited from `extends`.
2. Any field named in `omit` is removed.
3. This object&apos;s own `properties` are applied — a field redeclared here overrides the inherited one.

An `extends` cycle (for example, two objects that extend each other) is rejected at generation time
with an `ApiSchemaGenerationException`.

### The `objectName` join tag

A resource property opts into a canonical object by declaring `type: object` together with an
`objectName:` tag whose value equals the canonical `object.name`:

```yaml
# checkout.resource.yml
properties:
    billingAddress:
        type: object
        objectName: Address       # joins this property to the canonical Address object
        readable: false
        writable: true
        properties:
            zipCode: { type: string }
```

The `objectName` tag is dormant on its own: if no `address.object.yml` exists, the property&apos;s
inline `properties:` block is generated exactly as a normal [typed nested
object](#typed-nested-objects). When a canonical file for `Address` **is** present, the tag
activates and:

- The property&apos;s inline `properties:` are **replaced** by the canonical object&apos;s resolved shape.
- The mount attributes — `readable`, `writable`, `required`, `nullable` — stay on the referencing
  property. They describe how this property is mounted on this resource and are **not** owned by
  the canonical object, so the same canonical shape can be writable on one resource and read-only
  on another.
- A single shared `Generated\Api\{ApiType}\{Object}` class is emitted for the canonical object. No
  per-property companion class is generated for that property; every property tagged with the same
  `objectName` is typed to the one shared class.

{% info_block infoBox &quot;Shared class versus per-resource class&quot; %}

Without `objectName`, each `type: object` property generates its own per-resource value-object
class (for example, `CheckoutBillingAddressStorefrontObject`). With `objectName: Address`, all
matching properties across all resources instead share the single `Generated\Api\Storefront\Address`
class. Use a canonical object when several resources genuinely share one shape and you want them to
stay in lockstep; keep the inline form when each resource&apos;s shape is independent.

{% endinfo_block %}

### Validation

Field-level validation for a canonical object is authored in a parallel
`&lt;dashed-name&gt;.object.validation.yml` file, using the same format as a resource
[validation schema](/docs/integrations/spryker-api/api-platform/validation-schemas.html):

```yaml
# address.object.validation.yml
zipCode:
    - NotBlank: { message: &apos;ZIP code is required.&apos; }
firstName:
    - NotBlank: { message: &apos;First name is required.&apos; }
```

These constraints are lifted onto the generated canonical class. Every resource property that
references the object through `objectName` then carries an `Assert\Valid` cascade to that class, so
the canonical field rules are enforced wherever the object is used — you author the object&apos;s
validation once, in one place.

### Layer precedence

Canonical objects follow the same layer rules as resource schemas. The layer is detected from the
file path — a `/Pyz/` path is a project file, a `/SprykerFeature/` path is a feature file, and
anything else is core. Same-named objects merge by `object.name` with the precedence:

```text
project &gt; feature &gt; core
```

Because the merge is by `objectName`, a project can add a single field to a feature-layer canonical
object without redefining the whole object. Core ships no canonical object files today; the
mechanism is available to the project, feature, and core layers, and in practice projects are the
primary users.

## Documenting nested properties for OpenAPI and Swagger UI

Many endpoints accept or return structured JSON payloads — for example, a payment initialization
request that takes `payment`, `quote`, and `customer` sub-objects. Without explicit metadata,
those payloads appear as opaque `object` entries in the OpenAPI document, which means:

- The generated OpenAPI specification does not describe the child fields, their types, or which
  ones are required.
- The Swagger UI &quot;Try Out&quot; button shows an empty request body, forcing consumers to read code or
  external documentation to discover the expected shape.

The `map` property type combined with nested `openapiContext` entries closes both gaps.

### When to use this pattern

Use this pattern when the request or response body is a structured JSON object whose schema you
want to publish through OpenAPI, but you do not want to introduce a dedicated typed PHP class
for it. Typical cases are:

- Request payloads that aggregate fields from multiple transfer objects (for example, payment
  selection plus quote context).
- PSP- or provider-specific response payloads whose shape varies by configuration.

For payloads with a stable, strongly typed shape, prefer `type: object` so the generated PHP
class enforces the structure at the language level.

### Pattern

Combine `type: map` on the property with the following entries inside `openapiContext`:

| Entry | Purpose |
|-------|---------|
| `properties` | Declares each child field with its own `type`, `description`, `format`, and `example`. Used by Swagger UI to render the field-by-field schema. |
| `required` | Lists the child fields that must be present on a request. Drives the &quot;required&quot; markers in Swagger UI and the OpenAPI specification. |
| `example` | A complete sample payload. This is the value Swagger UI prefills into the &quot;Try Out&quot; body, so consumers can execute the request immediately. |

When the property is a `map`, the generator merges `&apos;type&apos; =&gt; &apos;object&apos;` into the emitted
`openapiContext`, so the property appears as an object — with the documented schema — in the
OpenAPI document while staying as a plain PHP `array` in the generated resource class.

### Worked example

The following extract is taken from
`src/Spryker/PaymentsRestApi/resources/api/storefront/payments.resource.yml`. It shows three
common shapes: a flat request object (`payment`), a request object with nested object children
(`quote`), and a response-only object whose contents vary at runtime (`preOrderPaymentData`).

```yaml
properties:
    payment:
        type: map
        writable: true
        readable: false
        required: true
        description: &apos;Payment selection for the pre-order initialization&apos;
        openapiContext:
            required: [&apos;paymentProviderName&apos;, &apos;paymentMethodName&apos;, &apos;amount&apos;]
            properties:
                paymentProviderName:
                    type: string
                    example: &apos;DummyPayment&apos;
                paymentMethodName:
                    type: string
                    example: &apos;Invoice&apos;
                amount:
                    type: integer
                    description: &apos;Amount in minor units (cents)&apos;
                    example: 9999
            example:
                paymentProviderName: &apos;DummyPayment&apos;
                paymentMethodName: &apos;Invoice&apos;
                amount: 9999

    quote:
        type: map
        writable: true
        readable: false
        required: true
        description: &apos;Quote context required to initialize the payment&apos;
        openapiContext:
            required: [&apos;customer&apos;, &apos;billingAddress&apos;, &apos;currency&apos;]
            properties:
                customer:
                    type: object
                    required: [&apos;firstName&apos;, &apos;lastName&apos;, &apos;email&apos;]
                    properties:
                        firstName: { type: string, example: &apos;Sonia&apos; }
                        lastName: { type: string, example: &apos;Wagner&apos; }
                        email: { type: string, format: email, example: &apos;sonia@acme.com&apos; }
                billingAddress:
                    type: object
                    required: [&apos;iso2Code&apos;]
                    properties:
                        iso2Code: { type: string, example: &apos;DE&apos; }
                currency:
                    type: object
                    required: [&apos;code&apos;]
                    properties:
                        code: { type: string, example: &apos;EUR&apos; }
            example:
                customer:
                    firstName: &apos;Sonia&apos;
                    lastName: &apos;Wagner&apos;
                    email: &apos;sonia@acme.com&apos;
                billingAddress:
                    iso2Code: &apos;DE&apos;
                currency:
                    code: &apos;EUR&apos;

    preOrderPaymentData:
        type: map
        writable: false
        readable: true
        required: false
        description: &apos;PSP-specific response payload returned by the payment provider&apos;
        openapiContext:
            example:
                transactionId: &apos;tx_abc123&apos;
                redirectUrl: &apos;https://psp.example.com/pay/tx_abc123&apos;
```

### Read-only versus write-only payloads

- **Write-only request payloads** (`writable: true`, `readable: false`) should declare
  `properties`, `required`, and `example`. The first two drive request validation and the
  generated OpenAPI schema; `example` makes the Swagger UI &quot;Try Out&quot; body usable without
  edits.
- **Read-only response payloads** (`writable: false`, `readable: true`) only need
  `openapiContext.example` when the response shape is dynamic. If the response shape is fixed,
  prefer declaring `properties` (and optionally `required`) so consumers see the full schema.

### Validation note

`openapiContext.required` controls only the OpenAPI documentation. If a request field must be
enforced at runtime, add the matching constraint to the resource&apos;s validation schema — see
[Validation schemas](/docs/integrations/spryker-api/api-platform/validation-schemas.html).

## Automatic JSON:API request body examples

For JSON:API endpoints (`application/vnd.api+json`), the generator automatically wraps property-level examples in the JSON:API envelope (`data.type` + `data.attributes`) when it builds the OpenAPI request body. You define examples once per property; the generator assembles the envelope for every write operation.

Given:

```yaml
resource:
  name: Customers
  shortName: customers   # becomes the JSON:API &quot;type&quot; field

  properties:
    email:
      type: string
      writable: true
      openapiContext:
        example: &quot;john@example.com&quot;
    firstName:
      type: string
      writable: true
      openapiContext:
        example: &quot;John&quot;
    idCustomer:
      type: integer
      writable: false      # excluded from request body example
      openapiContext:
        example: 42
```

…the generated OpenAPI request body for `POST`, `PATCH`, and `PUT` operations is:

```json
{
  &quot;data&quot;: {
    &quot;type&quot;: &quot;customers&quot;,
    &quot;attributes&quot;: {
      &quot;email&quot;: &quot;john@example.com&quot;,
      &quot;firstName&quot;: &quot;John&quot;
    }
  }
}
```

Rules the generator applies:

- The `shortName` value becomes the `type` field.
- Only **writable** properties are included — anything marked `writable: false` is filtered out (so identifiers and timestamps do not appear in the request example).
- Properties without an `openapiContext.example` are omitted from the example body.
- If no writable property has an example, no `requestBody` example is emitted at all — the operation appears without a prefilled &quot;Try Out&quot; body.

If you need a custom request body example that does not match this shape, override it at the operation level — see [Operations](#operations).

## Operations

Define which HTTP operations are available for the resource:

```yaml
operations:
  - type: Get                      # GET /customers/{id}
  - type: GetCollection            # GET /customers
  - type: Post                     # POST /customers
  - type: Put                      # PUT /customers/{id}
  - type: Patch                    # PATCH /customers/{id}
  - type: Delete                   # DELETE /customers/{id}
```

The operation names map to HTTP methods:
- `post` → POST (create)
- `get` → GET (single resource)
- `getCollection` → GET (collection)
- `put` → PUT (replace)
- `patch` → PATCH (update)
- `delete` → DELETE (remove)

### Operation options

Each operation entry accepts the following optional keys:

| Key | Description |
|-----|-------------|
| `name` | Operation name used in the generated attribute. Required when a resource has several operations of the same type. |
| `uriTemplate` | Custom URL path for the operation. See [Custom URL paths](#custom-url-paths). |
| `description` | Prose shown for the operation in Swagger UI. |
| `openapiContext` | OpenAPI details for the operation: `summary`, `parameters`, `responses`, and a custom `requestBody` example. |
| `openapi` | Set to `false` to keep the route but hide the operation from the OpenAPI document and Swagger UI. |
| `security`, `securityMessage` | Operation-level security expression. See [Security expressions](#security-expressions). |

#### Documenting query parameters

Query parameters that a provider reads, such as `sort`, `filter[&lt;resource&gt;.&lt;field&gt;]`, or `q`, are not derived from the resource properties. Declare each of them under `openapiContext.parameters` so that it appears as an input field in Swagger UI. Use `schema.enum` for closed value sets, for example the supported sort fields.

```yaml
operations:
  - type: GetCollection
    description: &apos;List categories.&apos;
    openapiContext:
      summary: &apos;Retrieve the category collection&apos;
      parameters:
        - name: sort
          in: query
          required: false
          description: &apos;Sort field. Prefix with `-` for descending order.&apos;
          schema:
            type: string
            enum: [categoryKey, -categoryKey, position, -position, name, -name]
          example: -position
        - name: parentCategoryKey
          in: query
          required: false
          description: &apos;Return only the direct children of this category.&apos;
          schema:
            type: string
          example: computer
      responses:
        200:
          description: &apos;Category collection.&apos;
```

Do not declare `page[limit]` and `page[offset]`: they are added to every paginated collection operation automatically. See [Pagination](#pagination).

#### Hiding an operation from the documentation

Some operations exist only to answer legacy URL shapes with a `400` or `501` error, for example `GET /abstract-products/abstract-product-prices` next to the real `GET /abstract-products/{sku}/abstract-product-prices`. Mark them with `openapi: false`. The route keeps working, but Swagger UI does not list the operation as an endpoint.

```yaml
operations:
  - type: GetCollection
    name: getAbstractProductPricesBare
    uriTemplate: /abstract-products/abstract-product-prices
    description: &apos;Returns 400 when called without the abstract product SKU.&apos;
    openapi: false
```

## Pagination

API Platform provides built-in pagination for collection endpoints (`GetCollection`). You can configure pagination behavior per resource using YAML schema options.

### Pagination options

| Option | Type | Description |
|--------|------|-------------|
| `paginationEnabled` | `boolean` | Enables or disables pagination for this resource. When `false`, `GetCollection` returns all results without pagination. Default: inherits from global configuration. |
| `paginationItemsPerPage` | `integer` | Number of items returned per page. Overrides the global default. |
| `paginationMaximumItemsPerPage` | `integer` | Maximum number of items a client can request per page via the `page[limit]` query parameter. Larger values are clamped to this maximum. |
| `paginationClientEnabled` | `boolean` | Allows clients to enable or disable pagination via the `pagination` query parameter (for example, `?pagination=false`). |
| `paginationClientItemsPerPage` | `boolean` | Allows clients to set the number of items per page via the `page[limit]` query parameter. |

Spryker resources use the JSON:API offset pagination parameters `page[limit]` and `page[offset]`. The OpenAPI document exposes these two parameters on every paginated collection operation instead of API Platform&apos;s native `page` and `itemsPerPage`.

The global default for `paginationItemsPerPage` is defined in the project&apos;s `api_platform.php` configuration file. To override it for a specific resource, set `paginationItemsPerPage` in the resource schema.

### Minimal pagination example

```yaml
resource:
  name: Products
  shortName: products

  paginationEnabled: true
  paginationItemsPerPage: 10

  operations:
    - type: GetCollection
```

### Full pagination example

```yaml
resource:
  name: Products
  shortName: products

  paginationEnabled: true
  paginationItemsPerPage: 20
  paginationMaximumItemsPerPage: 100
  paginationClientEnabled: true
  paginationClientItemsPerPage: true

  operations:
    - type: GetCollection
    - type: Get
```

With this configuration, clients can use the following query parameters:

```bash
# Default pagination (20 items per page)
GET /products

# Third page of 20 items
GET /products?page[offset]=40

# Request 50 items per page (up to maximum of 100)
GET /products?page[limit]=50

# 50 items per page, second page
GET /products?page[limit]=50&amp;page[offset]=50
```

A `page[limit]` below `1` falls back to the default items per page, and a negative `page[offset]` is treated as `0`. A page number parameter such as `page=3` is not supported.

### Generated output

The pagination options are rendered as named parameters in the `#[ApiResource]` attribute:

```php
#[ApiResource(
    operations: [new GetCollection(), new Get()],
    shortName: &apos;products&apos;,
    provider: ProductsBackendProvider::class,
    paginationItemsPerPage: 20,
    paginationEnabled: true,
    paginationMaximumItemsPerPage: 100,
    paginationClientEnabled: true,
    paginationClientItemsPerPage: true
)]
```

### Provider requirements

Providers extending `Spryker\ApiPlatform\State\Provider\AbstractProvider` call `buildPaginationTransfer()` to read `page[limit]` and `page[offset]` from the request into a `PaginationTransfer`, pass it to the facade, and call `setCollectionPagination()` with the total number of results:

```php
$paginationTransfer = $this-&gt;buildPaginationTransfer();
$criteriaTransfer-&gt;setPagination($paginationTransfer);

$collectionTransfer = $this-&gt;facade-&gt;getCollection($criteriaTransfer);

$nbResults = $collectionTransfer-&gt;getPagination()?-&gt;getNbResults();
if ($nbResults !== null) {
    $this-&gt;setCollectionPagination($paginationTransfer-&gt;getOffsetOrFail(), $paginationTransfer-&gt;getLimitOrFail(), $nbResults);
}

return $resources;
```

`buildPaginationTransfer()` resolves `page[limit]` against `paginationItemsPerPage` and `paginationMaximumItemsPerPage` and `page[offset]` against `0`. `setCollectionPagination()` renders the top-level `meta.pagination` object with `numFound`, `currentPage`, `maxPage`, and `currentItemsPerPage`, and the `first`, `last`, `prev`, and `next` entries of the top-level `links` object. Collection members do not carry pagination data.

```json
{
    &quot;links&quot;: {
        &quot;first&quot;: &quot;https://glue-backend.mysprykershop.com/products?page[limit]=20&amp;page[offset]=0&quot;,
        &quot;last&quot;: &quot;https://glue-backend.mysprykershop.com/products?page[limit]=20&amp;page[offset]=80&quot;,
        &quot;prev&quot;: &quot;https://glue-backend.mysprykershop.com/products?page[limit]=20&amp;page[offset]=20&quot;,
        &quot;next&quot;: &quot;https://glue-backend.mysprykershop.com/products?page[limit]=20&amp;page[offset]=60&quot;
    },
    &quot;meta&quot;: {
        &quot;pagination&quot;: {
            &quot;numFound&quot;: 97,
            &quot;currentPage&quot;: 3,
            &quot;maxPage&quot;: 5,
            &quot;currentItemsPerPage&quot;: 20
        }
    },
    &quot;data&quot;: []
}
```

Native API Platform resources that do not extend `AbstractProvider` return a `TraversablePaginator` instead. See [Native API Platform resources](/docs/integrations/spryker-api/api-platform/native-api-platform-resources.html).

### Global pagination defaults

Global pagination defaults can be configured in the application configuration file. Per-resource settings override the global defaults. See [API Platform configuration](/docs/integrations/spryker-api/api-platform/configuration.html) for details.

## Relationships

Define relationships between resources to enable including related resources via the `?include=` query parameter.

### includes section

Declares what relationships this resource can include. `includes` is declared once on the parent resource — the child resource does not need a reverse declaration.

```yaml
includes:
  - relationshipName: addresses
    targetResource: CustomersAddresses
    uriVariableMappings:
      customerReference: customerReference
```

**Entry fields:**

| Field | Required | Description |
|-------|----------|-------------|
| `relationshipName` | Yes | Name used in the `?include=` parameter and as the JSON:API relationship key. |
| `targetResource` | Yes | The `name` of the included resource as declared in its `resource.yml` (for example, `CustomersAddresses`). Also determines the JSON:API `type` field of the related resources. |
| `uriVariableMappings` | Conditional | Maps properties from the parent resource to the URI variables of the included resource. Required when the included resource is routed by URI variables. Format: `parentProperty: childUriVariable`. Ignored when `resolverClass` is set. |
| `uriTemplate` | Optional | Explicit URI template for the included resource when it has multiple operations and the relationship must target a specific path (for example, `/abstract-products/{abstractProductSku}/abstract-product-prices`). |
| `resolverClass` | Optional | Fully qualified class name of a relationship resolver. Use when the relationship cannot be expressed via URI variables — the resolver receives the parent resources and the request context, and returns the related resources directly. When `resolverClass` is set, `uriVariableMappings` and `uriTemplate` are not used for routing. See [Custom relationship resolvers](/docs/integrations/spryker-api/api-platform/relationships.html#custom-relationship-resolvers). |
| `autoInclude` | Optional | Resolve this relationship for every response of the parent type, even when the client did not request it via `?include=`. Use `autoIncludeMaxDepth` and `autoIncludeMinDepth` to bound where in the response graph the auto-include applies. |

#### URI-variable mapping example

For relationships routed by sub-resource URLs, map parent properties to child URI variables:

```yaml
includes:
  - relationshipName: abstract-product-prices
    targetResource: AbstractProductPrices
    uriTemplate: /abstract-products/{abstractProductSku}/abstract-product-prices
    uriVariableMappings:
      sku: abstractProductSku
```

#### Resolver-based example

For relationships whose targets cannot be derived from URI variables (for example, derived from order state or aggregated across multiple sources), reference a resolver class:

```yaml
includes:
  - relationshipName: order-shipments
    targetResource: OrderShipments
    resolverClass: Spryker\Glue\ShipmentsRestApi\Api\Storefront\Relationship\OrderShipmentsRelationshipResolver
```

**Further reading:** [Resource relationships](/docs/integrations/spryker-api/api-platform/relationships.html) — full reference for declaring, resolving, and troubleshooting relationships between API Platform resources, including provider-based and resolver-based dispatch, response shape, validation, and worked examples.

## Sort priority for included resources

The JSON:API response wraps related resources in an `included` array. By default, API Platform sorts that array alphabetically by resource `type`. Use `includedSortPriority` on a resource to override where its entries appear relative to other types.

### How it works

| Rule | Behavior |
|------|----------|
| Default | Every resource has an implicit priority of `0`. |
| Higher priority | Entries appear **later** in the `included` array. |
| Equal priority | Entries are sorted alphabetically by `type`. |

The priority is read from the resource&apos;s own `.resource.yml` and applied globally to every response that surfaces that type in `included`.

### Syntax

```yaml
resource:
  name: CartItems
  shortName: items

  includedSortPriority: 100
```

The generator passes the value through to the generated `#[ApiResource]` attribute via `extraProperties`:

```php
#[ApiResource(
    shortName: &apos;items&apos;,
    extraProperties: [&apos;includedSortPriority&apos; =&gt; 100],
    // ...
)]
```

### When to set a custom priority

Set `includedSortPriority` higher than `0` when a resource must appear after its nested children in the `included` array. The typical case is cart-item-like resources whose `?include=` chain resolves to abstract or concrete products: keeping the parent items last preserves the ordering of the legacy REST API and matches the order most clients expect when iterating the `included` array.

The following resources ship with `includedSortPriority: 100`:

- `items`
- `guest-cart-items`
- `bundle-items`
- `configurable-bundle-template-image-sets`

All other shipped resources rely on the default of `0`. Override the priority on project-level resources only when you need to enforce a specific ordering in `included`.

{% info_block infoBox &quot;Sort priority is not a guarantee of stable ordering across versions&quot; %}

`includedSortPriority` is a hint for the sort algorithm, not a JSON:API contract. Clients should still address resources by `type` and `id` rather than by index in the `included` array.

{% endinfo_block %}

## Resource generation process

### Generation workflow

The resource generation process is organized into distinct phases, each producing result objects for comprehensive error tracking and reporting:

```MARKDOWN
1. Preparation Phase
   ↓
2. Schema Parsing Phase → ParseResult
   - Load validation schemas
   - Parse validation rules
   - Load resource schemas
   - Parse resource definitions
   ↓
3. Schema Merging Phase → MergeResult
   - Merge schemas (Core → Feature → Project)
   - Track contributing source files
   ↓
4. Validation Phase → ValidationResult
   - Validate merged schemas
   - Apply validation rules
   ↓
5. Code Generation Phase
   - Generate PHP resource classes
   - Write files to output directory
   ↓
6. Cache Update
```

### Result objects

Each phase produces result objects that encapsulate both successful outcomes and failures:

- **ParseResult**: Contains grouped schemas and tracks failed validation files and schema files that could not be parsed
- **MergeResult**: Contains successfully merged schemas and tracks resources that failed to merge
- **ValidationResult**: Contains validated schemas and tracks resources that failed validation with detailed error messages

This structured approach ensures that errors in one resource do not block the generation of other valid resources, and provides clear feedback about what succeeded and what failed.

### Extending an existing resource (schema layering)

Spryker automatically merges schemas from multiple layers:

**Core layer** (lowest priority):

**vendor/spryker/customer/resources/api/backend/customer.resource.yml**

```yaml
resource:
  name: Customers
  properties:
    email:
      type: string
    firstName:
      type: string
```

**Feature layer** (medium priority):

**src/SprykerFeature/CRM/resources/api/backend/customer.resource.yml**

```yaml
resource:
  name: Customers
  properties:
    phone:
      type: string      # Added property
```

**Project layer** (highest priority):

**src/Pyz/Glue/Customer/resources/api/backend/customer.resource.yml**

```yaml
resource:
  name: Customers
  properties:
    email:
      required: true    # Override core definition
    customField:
      type: string      # Project-specific field
```

**Merged result:**

```yaml
resource:
  name: Customers
  properties:
    email:
      type: string
      required: true    # From project layer
    firstName:
      type: string      # From core layer
    phone:
      type: string      # From feature layer
    customField:
      type: string      # From project layer
```

### Generated resource class

The generator creates a complete PHP class with API Platform attributes:

```php
&lt;?php

declare(strict_types=1);
namespace Generated\Api\Backend;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\ApiProperty;
use Symfony\Component\Validator\Constraints as Assert;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Delete;

#[ApiResource(
    operations: [new Post(), new Get(), new GetCollection(), new Patch(), new Delete()],
    shortName: &apos;customers&apos;,
    provider: CustomerBackendProvider::class,
    processor: CustomerBackendProcessor::class,
    paginationItemsPerPage: 10,
    paginationEnabled: true,
    paginationMaximumItemsPerPage: 100,
    paginationClientEnabled: true,
    paginationClientItemsPerPage: true
)]
final class CustomersBackendResource
{
    #[ApiProperty(writable: false)]
    public ?int $idCustomer = null;

    #[ApiProperty(openapiContext: [&apos;example&apos; =&gt; &apos;john@example.com&apos;])]
    #[Assert\NotBlank(groups: [&apos;customers:create&apos;])]
    #[Assert\Email(groups: [&apos;customers:create&apos;])]
    public ?string $email = null;

    #[ApiProperty(identifier: true, writable: false)]
    public ?string $customerReference = null;

    public ?bool $isActive = true;

    // Getters, setters, toArray(), fromArray() methods...
}
```

## Debugging schemas

### Debug commands

```bash
# List all resources
docker/sdk cli GLUE_APPLICATION=GLUE_BACKEND glue api:debug --list

# Show specific resource
docker/sdk cli GLUE_APPLICATION=GLUE_BACKEND glue api:debug customers --api-type=backend

# Show merged schema
docker/sdk cli GLUE_APPLICATION=GLUE_BACKEND glue api:debug customers --api-type=backend --show-merged

# Show contributing source files
docker/sdk cli GLUE_APPLICATION=GLUE_BACKEND glue api:debug customers --api-type=backend --show-sources

# Validate schemas without generating
docker/sdk cli GLUE_APPLICATION=GLUE_BACKEND glue api:generate --validate-only
```

### Common schema errors

The generator validates schemas and provides detailed error messages:

```bash
# Missing required fields
Error: Resource &quot;customers&quot; is missing required field &quot;name&quot;

# Invalid operation type
Error: Invalid operation type &quot;INVALID&quot;. Must be one of: Get, Post, Put, Patch, Delete, GetCollection

# Invalid property type
Error: Property &quot;age&quot; has invalid type &quot;int&quot;. Must be one of: string, integer, number, boolean, array, object

# Provider class not found
Error: Provider class &quot;Pyz\Glue\Customer\Api\Backend\Provider\MissingProvider&quot; does not exist
```

## Advanced schema features

### Custom URL paths

Operations support `uriTemplate` and `uriVariables` to define custom URL paths, including sub-resource URLs like `/customers/{customerReference}/addresses`.

#### Sub-resource with full CRUD

Define a child resource with nested URLs by adding `uriTemplate` and `uriVariables` to each operation:

**customers-addresses.resource.yml**

```yaml
resource:
  name: CustomersAddresses
  shortName: customers-addresses

  operations:
    - type: GetCollection
      uriTemplate: &apos;/customers/{customerReference}/addresses&apos;
      uriVariables:
        customerReference:
          toProperty: &apos;customer&apos;
          fromClass: CustomersStorefrontResource

    - type: Get
      uriTemplate: &apos;/customers/{customerReference}/addresses/{uuid}&apos;
      uriVariables:
        customerReference:
          toProperty: &apos;customer&apos;
          fromClass: CustomersStorefrontResource
        uuid:
          fromClass: CustomersAddressesStorefrontResource

    - type: Post
      uriTemplate: &apos;/customers/{customerReference}/addresses&apos;
      uriVariables:
        customerReference:
          toProperty: &apos;customer&apos;
          fromClass: CustomersStorefrontResource
```

**`uriVariables` properties:**
- `fromClass`: The generated resource class the variable originates from
- `toProperty`: The property on the current resource that links to the parent resource

#### Action-style sub-resource

For single-action endpoints nested under a parent resource:

**customers-confirm-registration.resource.yml**

```yaml
resource:
  name: CustomersConfirmRegistration
  shortName: customers-confirm-registration

  operations:
    - type: Post
      uriTemplate: /customers/{customerReference}/confirm-registration
```

For more details on `uriTemplate`, `uriVariables`, and sub-resource patterns, see the [API Platform sub-resources documentation](https://api-platform.com/docs/core/subresources/).

### Security expressions

Security expressions protect resources and operations using [Symfony&apos;s ExpressionLanguage](https://symfony.com/doc/current/security/expressions.html). They require the SecurityBundle to be configured. See [Integrate API Platform security](/docs/integrations/spryker-api/authenticating-and-authorization/integrate-api-platform-security.html) for setup instructions.

{% info_block infoBox &quot;Where roles come from&quot; %}

Roles like `ROLE_CUSTOMER` in security expressions come from OAuth scopes that are automatically mapped to Symfony roles. The mapping convention is as follows: a scope name is uppercased and prefixed with `ROLE_`. For example, the `customer` scope becomes `ROLE_CUSTOMER`.

Scopes are provided by scope provider plugins registered in `OauthDependencyProvider::getScopeProviderPlugins()`. The following table lists the out-of-the-box scope provider plugins and the scopes they provide:

| Plugin | Scopes |
|--------|--------|
| `CustomerOauthScopeProviderPlugin` | `customer` |
| `CompanyUserOauthScopeProviderPlugin` | `company_user` |
| `AgentOauthScopeProviderPlugin` | `agent` |
| `CustomerImpersonationOauthScopeProviderPlugin` | `customer_impersonation`, `customer` |
| `UserOauthScopeProviderPlugin` | `user`, plus UserType sub-plugins |
| `WarehouseOauthScopeProviderPlugin` | `warehouse` |

For details on how the mapping works, see [Security — Roles and OAuth scope mapping](/docs/integrations/spryker-api/authenticating-and-authorization/security.html). For instructions on setting up scopes, see [Integrate the authorization scopes](/docs/integrations/spryker-api/backend-api/integrate-backend-api/integrate-the-authorization-scopes.html).

{% endinfo_block %}

Three types of security expressions are supported:

| Expression | Evaluated | Use case | When to use |
|-----------|-----------|----------|-------------|
| `security` | Before the request is processed | Check user roles or authentication status | For role or authentication checks that do not depend on the request body. |
| `securityPostDenormalize` | After the request body is deserialized | Check authorization based on submitted data | When authorization depends on the deserialized resource `object`, for example, to verify the user owns the resource being modified. |
| `securityPostValidation` | After validation passes | Check authorization based on validated data | When authorization depends on validated data, for example, to verify a value is within the user&apos;s authorized limit after validation confirms the data is structurally correct. |

#### Resource-level security

Applies to all operations on the resource:

```yaml
resource:
  name: Customers
  shortName: customers
  security: &quot;is_granted(&apos;ROLE_USER&apos;)&quot;
```

#### Operation-level security

Applies to a specific operation, overriding resource-level security:

```yaml
resource:
  name: Customers
  shortName: customers

  operations:
    - type: Post
      # No security — public registration

    - type: Get
      security: &quot;is_granted(&apos;ROLE_USER&apos;)&quot;

    - type: Patch
      security: &quot;is_granted(&apos;ROLE_USER&apos;)&quot;
```

#### Post-denormalize security

Evaluated after the request body has been deserialized. The `object` variable contains the resource instance:

```yaml
resource:
  name: Orders
  shortName: orders
  security: &quot;is_granted(&apos;ROLE_USER&apos;)&quot;
  securityPostDenormalize: &quot;is_granted(&apos;EDIT&apos;, object)&quot;
```

{% info_block infoBox &quot;Custom voter attributes&quot; %}

`EDIT` in the example is a **custom voter attribute** — it is an application-defined string, not a built-in Symfony or Spryker constant. For `is_granted(&apos;EDIT&apos;, object)` to work, you must register a custom Symfony [Voter](https://symfony.com/doc/current/security/voters.html) that supports the `EDIT` attribute and implements the authorization logic, for example, checking that the authenticated user owns the resource.

Use `securityPostDenormalize` when the authorization decision depends on the **submitted request data** (the deserialized `object`), such as verifying resource ownership.

{% endinfo_block %}

#### Post-validation security

Evaluated after validation has passed:

```yaml
resource:
  name: Payments
  shortName: payments
  securityPostValidation: &quot;is_granted(&apos;PROCESS&apos;, object)&quot;
```

{% info_block infoBox &quot;Custom voter attributes&quot; %}

`PROCESS` in the example is a **custom voter attribute** — it is an application-defined string, not a built-in Symfony or Spryker constant. For `is_granted(&apos;PROCESS&apos;, object)` to work, you must register a custom Symfony [Voter](https://symfony.com/doc/current/security/voters.html) that supports the `PROCESS` attribute.

Use `securityPostValidation` when the authorization decision depends on **validated data**, for example, to verify a payment amount is within the user&apos;s authorized limit after validation confirms the data is structurally correct.

{% endinfo_block %}

For detailed information about the authentication flow, role mapping, and accessing the authenticated user in providers, see [Security](/docs/integrations/spryker-api/authenticating-and-authorization/security.html).

## Generation commands

### Basic generation

```bash
# Generate all configured API types
docker/sdk cli GLUE_APPLICATION=GLUE_BACKEND glue api:generate

# Generate specific API type
docker/sdk cli GLUE_APPLICATION=GLUE_BACKEND glue api:generate backend
docker/sdk cli GLUE_APPLICATION=GLUE_STOREFRONT glue api:generate storefront

# Generate with options
docker/sdk cli GLUE_APPLICATION=GLUE_BACKEND glue api:generate --dry-run           # Preview without writing
docker/sdk cli GLUE_APPLICATION=GLUE_BACKEND glue api:generate --validate-only     # Only validate schemas
docker/sdk cli GLUE_APPLICATION=GLUE_BACKEND glue api:generate --resource=customers  # Generate single resource
```

### Output

```bash
Generating API resources for ApiType: backend

Discovering schema files...
Validating schemas... OK
Merging schemas... OK

Generating resources:
 10/10 [============================] 100%

Generated: 10 file(s)
Cache updated

Done!
```

## Schema validation rules

The generator enforces these rules:

### Required fields

Every resource must have:
- `name` - Internal resource name
- `shortName` - URL-friendly name
- At least one `operation`
- At least one `property`

### Valid operation types

Only these operation types are allowed:
- `Get` - Retrieve single resource
- `GetCollection` - Retrieve collection
- `Post` - Create resource
- `Put` - Replace entire resource
- `Patch` - Update partial resource
- `Delete` - Delete resource

### Valid property types

Only these property types are allowed:
- `string`
- `integer`
- `number`
- `boolean`
- `array`
- `object`
- `map`
- `mixed`

### Provider/Processor validation

- Provider/Processor classes must exist
- Classes must implement correct interfaces
- Namespaces must be valid PHP namespaces

## Best practices

### 1. Use semantic naming

```yaml
# ✅ Good
resource:
  name: Customers              # PascalCase plural — used for schema merging
  shortName: customers         # lowercase kebab-case plural — JSON:API type + URL segment

# ✅ Good — multi-word
resource:
  name: AbstractProductPrices
  shortName: abstract-product-prices

# ❌ Bad — wrong shortName casing/form
resource:
  name: Customers
  shortName: Customer          # Should be lowercase plural

# ❌ Bad — abbreviated, unclear
resource:
  name: CustomerData
  shortName: cust
```

### 2. Document all properties

```yaml
# ✅ Good
email:
  type: string
  description: &quot;The customer&apos;s email address used for login and notifications&quot;

# ❌ Bad
email:
  type: string
```

### 3. Leverage schema merging

Core — define base properties:

**src/Spryker/Customer/resources/api/backend/customer.resource.yml**

```yaml
resource:
  name: Customers
  properties:
    email:
      type: string
```

Project — only override what is needed:

**src/Pyz/Glue/Customer/resources/api/backend/customer.resource.yml**

```yaml
resource:
  name: Customers
  properties:
    email:
      required: true  # ← Only the difference
```

### 4. Use readable/writable correctly

```yaml
# Read-only fields (IDs, timestamps)
idCustomer:
  type: integer
  writable: false

# Write-only fields (passwords)
password:
  type: string
  readable: false

# Read-write fields (normal data)
email:
  type: string
  writable: true
  readable: true
```
</description>
            <pubDate>Fri, 11 Sep 2026 08:04:21 +0000</pubDate>
            <link>https://docs.spryker.com/docs/integrations/spryker-api/api-platform/resource-schemas.html</link>
            <guid isPermaLink="true">https://docs.spryker.com/docs/integrations/spryker-api/api-platform/resource-schemas.html</guid>
            
            
        </item>
        
        <item>
            <title>API Platform</title>
            <description>&lt;p&gt;Spryker’s API Platform integration provides schema-based API resource generation with automatic OpenAPI documentation. This allows you to define your API resources using YAML schemas and automatically generate fully functional API endpoints with validation, pagination, and &lt;a href=&quot;/docs/integrations/spryker-api/api-platform/serialization.html&quot;&gt;serialization&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This document describes the API Platform architecture and how it integrates with Spryker.&lt;/p&gt;
&lt;h2 id=&quot;what-is-api-platform&quot;&gt;What is API Platform&lt;/h2&gt;
&lt;p&gt;API Platform is a framework for building modern APIs based on web standards and best practices. In Spryker, it complements the existing Glue API infrastructure by providing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Schema-based resource generation&lt;/strong&gt;: Define resources in YAML, generate PHP classes automatically&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Automatic OpenAPI documentation&lt;/strong&gt;: Interactive API documentation generated from schemas&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Built-in validation&lt;/strong&gt;: Symfony Validator integration with operation-specific rules&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pagination support&lt;/strong&gt;: Standardized pagination with configurable defaults&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;State management&lt;/strong&gt;: Separate providers (read) and processors (write) for clean architecture&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Read more about the API Platform project at &lt;a href=&quot;https://api-platform.com/&quot;&gt;api-platform.com&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;why-spryker-is-moving-to-api-platform&quot;&gt;Why Spryker is moving to API Platform&lt;/h3&gt;
&lt;p&gt;API Platform replaces Spryker-specific patterns for routing, authentication, and resource definition with industry-standard Symfony conventions, automatic OpenAPI schema generation, and a clean separation between resource schema, provider, and validation.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Previous infrastructure&lt;/th&gt;
&lt;th&gt;API Platform&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Bootstrap&lt;/td&gt;
&lt;td&gt;Spryker-specific application bootstrap&lt;/td&gt;
&lt;td&gt;Symfony Kernel-based routing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resource registration&lt;/td&gt;
&lt;td&gt;Manual plugin registration in &lt;code&gt;GlueApplicationDependencyProvider&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Declarative YAML resource definitions (&lt;code&gt;*.resource.yml&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authentication&lt;/td&gt;
&lt;td&gt;Custom flows per module&lt;/td&gt;
&lt;td&gt;Standard OAuth2 / Symfony Security&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coupling&lt;/td&gt;
&lt;td&gt;Tight coupling between resource and routing logic&lt;/td&gt;
&lt;td&gt;Clean separation: provider + resource schema + validation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Testability&lt;/td&gt;
&lt;td&gt;Complex to test and extend&lt;/td&gt;
&lt;td&gt;Symfony-native, testable with standard PHPUnit patterns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenAPI&lt;/td&gt;
&lt;td&gt;Manual / partial&lt;/td&gt;
&lt;td&gt;Automatic OpenAPI schema generation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&quot;architecture-overview&quot;&gt;Architecture overview&lt;/h2&gt;
&lt;h3 id=&quot;resource-generation-workflow&quot;&gt;Resource generation workflow&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-MARKDOWN&quot;&gt;&gt;Schema Files (YAML)
    ↓
Schema Discovery &amp;amp; Validation
    ↓
Multi-layer Schema Merging (Core → Feature → Project → [Code Buckets])
    ↓
Resource Class Generation
    ↓
API Platform Resource (with attributes)
    ↓
API Endpoints
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;core-components&quot;&gt;Core components&lt;/h3&gt;
&lt;h4 id=&quot;schema-files&quot;&gt;1. Schema files&lt;/h4&gt;
&lt;p&gt;Resources are defined in YAML files located in module directories:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-MARKDOWN&quot;&gt;&gt;src/Spryker/{Module}/resources/api/{api-type}/{resource-name}.resource.yml
src/Spryker/{Module}/resources/api/{api-type}/{resource-name}.validation.yml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Example resource schema &lt;code&gt;src/Spryker/{Module}/resources/api/{api-type}/{resource-name}.resource.yml&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;resource&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Customers&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;shortName&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;customers&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Customer&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;resource&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;for&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;backend&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;API&quot;&lt;/span&gt;

  &lt;span class=&quot;na&quot;&gt;provider&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Pyz&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\\&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Glue&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\\&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Customer&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\\&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Api&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\\&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Backend&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\\&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Provider&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\\&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;CustomerBackendProvider&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;processor&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Pyz&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\\&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Glue&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\\&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Customer&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\\&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Api&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\\&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Backend&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\\&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Processor&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\\&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;CustomerBackendProcessor&quot;&lt;/span&gt;

  &lt;span class=&quot;na&quot;&gt;paginationEnabled&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;true&lt;/span&gt;

  &lt;span class=&quot;na&quot;&gt;operations&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Post&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Get&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;GetCollection&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Patch&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Delete&lt;/span&gt;

  &lt;span class=&quot;na&quot;&gt;properties&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;string&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Customer&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;address&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;customerReference&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;string&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;identifier&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;true&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;writable&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;Example validation schema &lt;code&gt;src/Spryker/{Module}/resources/api/{api-type}/{resource-name}.validation.yml&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;NotBlank&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;First name is required&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;Length&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;min&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;2&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;max&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;64&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;minMessage&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;First name must be at least 2 characters&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;maxMessage&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;First name cannot exceed 64 characters&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;patch&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;Optional&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;constraints&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;Length&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
              &lt;span class=&quot;na&quot;&gt;min&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;2&lt;/span&gt;
              &lt;span class=&quot;na&quot;&gt;max&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;64&lt;/span&gt;
              &lt;span class=&quot;na&quot;&gt;minMessage&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;First name must be at least 2 characters&lt;/span&gt;
              &lt;span class=&quot;na&quot;&gt;maxMessage&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;First name cannot exceed 64 characters&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;h4 id=&quot;generated-resources&quot;&gt;2. Generated resources&lt;/h4&gt;
&lt;p&gt;The generator creates PHP classes with API Platform attributes:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;src/Generated/Api/Backend/CustomersBackendResource.php&lt;/code&gt;&lt;/p&gt;
&lt;div class=&quot;language-php highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class=&quot;kn&quot;&gt;namespace&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;Generated\Api\Backend&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ApiPlatform\Metadata\ApiResource&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ApiPlatform\Metadata\ApiProperty&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Symfony\Component\Validator\Constraints&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Assert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;#[ApiResource(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;operations&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Post&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;GetCollection&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Patch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Delete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()],&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;shortName&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;customers&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;provider&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CustomerBackendProvider&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;processor&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CustomerBackendProcessor&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;class&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)]&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CustomersBackendResource&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;#[ApiProperty(identifier: true, writable: false)]&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;?string&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$customerReference&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;#[ApiProperty]&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;#[Assert\NotBlank(groups: [&apos;customers:create&apos;])]&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;#[Assert\Email(groups: [&apos;customers:create&apos;])]&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;?string&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$email&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// Getters, setters, toArray(), fromArray()...&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;h4 id=&quot;state-providers-and-processors&quot;&gt;3. State providers and processors&lt;/h4&gt;
&lt;p&gt;Detailed information about the API-Platform Provider and Resources can be found on the public docs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://api-platform.com/docs/core/state-providers/&quot;&gt;API Platform Providers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://api-platform.com/docs/core/state-processors/&quot;&gt;API Platform Processors&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Provider (read operations):&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-php highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CustomerBackendProvider&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;implements&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ProviderInterface&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;provide&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;Operation&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$operation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;array&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$uriVariables&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[],&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;array&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$context&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]):&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;object&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;array&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// Fetch and return data from your business layer&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$customerResource&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Processor (write operations):&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-php highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CustomerBackendProcessor&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;implements&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ProcessorInterface&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;process&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;mixed&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Operation&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$operation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;array&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$uriVariables&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[],&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;array&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$context&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]):&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;mixed&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// Persist changes through your business layer&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$updatedResource&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;h2 id=&quot;api-types&quot;&gt;API types&lt;/h2&gt;
&lt;p&gt;Any of the &lt;a href=&quot;/docs/integrations/spryker-api/getting-started-with-apis/getting-started-with-apis.html&quot;&gt;existing APIs&lt;/a&gt; can be extended using API Platform.&lt;/p&gt;
&lt;p&gt;Spryker supports multiple API types for different use cases:&lt;/p&gt;
&lt;h3 id=&quot;glue-api&quot;&gt;Glue API&lt;/h3&gt;
&lt;p&gt;This API is configured to serve the &lt;a href=&quot;https://jsonapi.org/format/&quot;&gt;JSON:API&lt;/a&gt; format by default; to change the supported formats, see &lt;a href=&quot;/docs/integrations/spryker-api/api-platform/configuration.html#configure-supported-formats&quot;&gt;Configure supported formats&lt;/a&gt;. Projects migrating their APIs can provide new APIs as well as supporting the existing ones while migrating.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;API Type:&lt;/strong&gt; &lt;code&gt;storefront&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Application:&lt;/strong&gt; Glue&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Base URL:&lt;/strong&gt; &lt;code&gt;http://glue.eu.spryker.local/&lt;/code&gt; - Configurable per project&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use cases:&lt;/strong&gt; Customer-facing APIs, mobile apps, PWAs&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;gluestorefront-api&quot;&gt;GlueStorefront API&lt;/h3&gt;
&lt;p&gt;This API serves the &lt;a href=&quot;https://jsonapi.org/format/&quot;&gt;JSON:API&lt;/a&gt; format by default; additional formats, such as JSON-LD, can be enabled per project. For instructions, see &lt;a href=&quot;/docs/integrations/spryker-api/api-platform/configuration.html#configure-supported-formats&quot;&gt;Configure supported formats&lt;/a&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;API Type:&lt;/strong&gt; &lt;code&gt;storefront&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Application:&lt;/strong&gt; GlueStorefront&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Base URL:&lt;/strong&gt; &lt;code&gt;http://glue-storefront.eu.spryker.local/&lt;/code&gt; - Configurable per project&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use cases:&lt;/strong&gt; Customer-facing APIs, mobile apps, PWAs&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;gluebackend-api&quot;&gt;GlueBackend API&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;API Type:&lt;/strong&gt; &lt;code&gt;backend&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Application:&lt;/strong&gt; GlueBackend&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Base URL:&lt;/strong&gt; &lt;code&gt;http://glue-backend.eu.spryker.local/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use cases:&lt;/strong&gt; Admin panels, internal tools, ERP integrations&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;merchant-portal-api&quot;&gt;Merchant Portal API&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;API Type:&lt;/strong&gt; &lt;code&gt;merchant-portal&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Application:&lt;/strong&gt; MerchantPortal&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Base URL:&lt;/strong&gt; &lt;code&gt;http://mp.glue.eu.spryker.local/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use cases:&lt;/strong&gt; Marketplace merchant interfaces&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Example:&lt;/strong&gt; &lt;code&gt;/products&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;multi-layer-schema-merging&quot;&gt;Multi-layer schema merging&lt;/h2&gt;
&lt;p&gt;One of the key features is support for multi-layer schema definitions that automatically merge:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Core layer&lt;/strong&gt; (vendor/spryker):&lt;/p&gt;
&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;resource&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Customers&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;properties&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;string&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Feature layer&lt;/strong&gt; (src/SprykerFeature):&lt;/p&gt;
&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;resource&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Customers&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;properties&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;loyaltyPoints&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;integer&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Project layer&lt;/strong&gt; (src/Pyz):&lt;/p&gt;
&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;resource&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Customers&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;properties&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;required&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;true&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# Override core&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;customField&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;string&lt;/span&gt;    &lt;span class=&quot;c1&quot;&gt;# Project-specific&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Result&lt;/strong&gt;: A single merged resource with all properties, project code-bucket layer taking precedence.&lt;/p&gt;
&lt;h2 id=&quot;integration-with-spryker-architecture&quot;&gt;Integration with Spryker architecture&lt;/h2&gt;
&lt;h3 id=&quot;dependency-injection&quot;&gt;Dependency Injection&lt;/h3&gt;
&lt;p&gt;API Platform fully integrates with Symfony Dependency Injection:&lt;/p&gt;
&lt;div class=&quot;language-php highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// config/Zed/ApplicationServices.php&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;$services&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;load&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;Pyz\\Zed\\&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;../../../src/Pyz/Zed/&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;Providers and Processors are automatically discovered and can use constructor injection:&lt;/p&gt;
&lt;div class=&quot;language-php highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CustomerBackendProvider&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;implements&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ProviderInterface&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;__construct&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;CustomerFacadeInterface&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$customerFacade&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;CustomerRepositoryInterface&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$customerRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;h3 id=&quot;facade-integration&quot;&gt;Facade integration&lt;/h3&gt;
&lt;p&gt;Resources can leverage existing Spryker facades:&lt;/p&gt;
&lt;div class=&quot;language-php highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CustomerBackendProcessor&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;implements&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ProcessorInterface&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;__construct&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;CustomerFacadeInterface&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$customerFacade&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;process&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;mixed&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Operation&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$operation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;...):&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;mixed&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;$customerTransfer&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;mapToTransfer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;$response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;customerFacade&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;createCustomer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$customerTransfer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;mapToResource&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$response&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getCustomerTransfer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;());&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;h2 id=&quot;resource-generation&quot;&gt;Resource generation&lt;/h2&gt;
&lt;h3 id=&quot;console-commands&quot;&gt;Console commands&lt;/h3&gt;
&lt;p&gt;All the following commands can be used with a specific GLUE_APPLICATION by prefixing them with &lt;code&gt;GLUE_APPLICATION=GLUE_BACKEND&lt;/code&gt; environment variable. For example: &lt;code&gt;docker/sdk cli GLUE_APPLICATION=GLUE_BACKEND glue api:debug --list&lt;/code&gt;&lt;/p&gt;
&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Generate resource classes for all configured API types at once. Usually used during deployment/installation.&lt;/span&gt;
docker/sdk cli glue api:generate

&lt;span class=&quot;c&quot;&gt;# Generate API type specific resource classes. Usually used during development.&lt;/span&gt;
docker/sdk cli glue api:generate backend

&lt;span class=&quot;c&quot;&gt;# Validate schemas only to see if there is any issue in the definitions&lt;/span&gt;
docker/sdk cli glue api:generate &lt;span class=&quot;nt&quot;&gt;--validate-only&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;h3 id=&quot;debug-commands&quot;&gt;Debug commands&lt;/h3&gt;
&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# List all resources to see which ones are defined in the schema files.&lt;/span&gt;
docker/sdk cli glue  api:debug &lt;span class=&quot;nt&quot;&gt;--list&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Inspect specific resource and print details about properties and operations&lt;/span&gt;
docker/sdk cli glue  api:debug customers &lt;span class=&quot;nt&quot;&gt;--api-type&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;backend

&lt;span class=&quot;c&quot;&gt;# Show merged schema&lt;/span&gt;
docker/sdk cli glue  api:debug customers &lt;span class=&quot;nt&quot;&gt;--api-type&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;backend &lt;span class=&quot;nt&quot;&gt;--show-merged&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Show contributing files for a resource&lt;/span&gt;
docker/sdk cli glue  api:debug customers &lt;span class=&quot;nt&quot;&gt;--api-type&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;backend &lt;span class=&quot;nt&quot;&gt;--show-sources&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;h2 id=&quot;features&quot;&gt;Features&lt;/h2&gt;
&lt;h3 id=&quot;automatic-openapi-documentation&quot;&gt;Automatic OpenAPI documentation&lt;/h3&gt;
&lt;p&gt;API Platform generates interactive OpenAPI documentation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Swagger UI at the root URL &lt;code&gt;/&lt;/code&gt; for example &lt;code&gt;http://glue-backend.eu.spryker.local/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Query parameters declared under an operation’s &lt;code&gt;openapiContext.parameters&lt;/code&gt;, such as &lt;code&gt;sort&lt;/code&gt; or &lt;code&gt;filter[...]&lt;/code&gt;, are rendered as input fields. Paginated collections get &lt;code&gt;page[limit]&lt;/code&gt; and &lt;code&gt;page[offset]&lt;/code&gt; automatically.&lt;/li&gt;
&lt;li&gt;Operations marked with &lt;code&gt;openapi: false&lt;/code&gt; keep their route but are not listed. See &lt;a href=&quot;/docs/integrations/spryker-api/api-platform/resource-schemas.html#operation-options&quot;&gt;Operation options&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can disable this interface in production environments by configuring the settings in your &lt;code&gt;api_platform.php&lt;/code&gt; configuration file. For details, see &lt;a href=&quot;/docs/integrations/spryker-api/api-platform/configuration.html#enable-the-documentation-ui-only-in-development&quot;&gt;Enable the documentation UI only in development&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;built-in-validation&quot;&gt;Built-in validation&lt;/h3&gt;
&lt;p&gt;Validation rules from &lt;code&gt;*.validation.yml&lt;/code&gt; files are converted to Symfony Validator constraints:&lt;/p&gt;
&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;NotBlank&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Email&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;Becomes:&lt;/p&gt;
&lt;div class=&quot;language-php highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;#[Assert\NotBlank(groups: [&apos;customers:create&apos;])]&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#[Assert\Email(groups: [&apos;customers:create&apos;])]&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;?string&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$email&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;h3 id=&quot;pagination-support&quot;&gt;Pagination support&lt;/h3&gt;
&lt;p&gt;Standardized JSON:API offset pagination with query parameters:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-MARKDOWN&quot;&gt;&gt;GET /customers?page[limit]=20&amp;amp;page[offset]=20
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Providers extending &lt;code&gt;AbstractProvider&lt;/code&gt; call &lt;code&gt;buildPaginationTransfer()&lt;/code&gt; to read &lt;code&gt;page[limit]&lt;/code&gt; and &lt;code&gt;page[offset]&lt;/code&gt; from the request into a &lt;code&gt;PaginationTransfer&lt;/code&gt;, pass it to the facade, and call &lt;code&gt;setCollectionPagination()&lt;/code&gt; with the total number of results. The response carries the summary in the top-level &lt;code&gt;meta.pagination&lt;/code&gt; object and the &lt;code&gt;first&lt;/code&gt;, &lt;code&gt;last&lt;/code&gt;, &lt;code&gt;prev&lt;/code&gt;, and &lt;code&gt;next&lt;/code&gt; links in the top-level &lt;code&gt;links&lt;/code&gt; object. See &lt;a href=&quot;/docs/integrations/spryker-api/api-platform/resource-schemas.html#pagination&quot;&gt;Resource schemas — Pagination&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;operation-specific-behavior&quot;&gt;Operation-specific behavior&lt;/h3&gt;
&lt;p&gt;Define different validation and behavior per operation:&lt;/p&gt;
&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;operations&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Post&lt;/span&gt;            &lt;span class=&quot;c1&quot;&gt;# Create&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Get&lt;/span&gt;             &lt;span class=&quot;c1&quot;&gt;# Read one&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;GetCollection&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;# Read many&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Patch&lt;/span&gt;           &lt;span class=&quot;c1&quot;&gt;# Update&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Delete&lt;/span&gt;          &lt;span class=&quot;c1&quot;&gt;# Delete&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;Each operation can have specific validation rules and security settings.&lt;/p&gt;
&lt;h3 id=&quot;relationships&quot;&gt;Relationships&lt;/h3&gt;
&lt;p&gt;Include related resources via the &lt;code&gt;?include=&lt;/code&gt; query parameter:&lt;/p&gt;
&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;relationshipName&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;addresses&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;targetResource&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;CustomersAddresses&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;uriVariableMappings&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;customerReference&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;customerReference&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;Request:&lt;/p&gt;
&lt;div class=&quot;language-markdown highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;GET /customers/customer--35?include=addresses
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;Response includes both the customer and related addresses in JSON:API format. No provider code changes required - relationships work automatically through decoration.&lt;/p&gt;
&lt;p&gt;For detailed information, see &lt;a href=&quot;/docs/integrations/spryker-api/api-platform/relationships.html&quot;&gt;Resource relationships&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;sparse-fieldsets&quot;&gt;Sparse fieldsets&lt;/h3&gt;
&lt;p&gt;Request only the attributes you need using the &lt;code&gt;fields&lt;/code&gt; query parameter:&lt;/p&gt;
&lt;div class=&quot;language-markdown highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;GET /stores?fields[stores]=name,locale
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;This returns only &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;locale&lt;/code&gt; in the response attributes, reducing payload size. Sparse fieldsets work with relationships too — filter attributes on both the main resource and included resources.&lt;/p&gt;
&lt;p&gt;For detailed information, see &lt;a href=&quot;/docs/integrations/spryker-api/api-platform/sparse-fieldsets.html&quot;&gt;Sparse Fieldsets&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;performance&quot;&gt;Performance&lt;/h2&gt;
&lt;h3 id=&quot;opcache&quot;&gt;Opcache&lt;/h3&gt;
&lt;p&gt;API Platform loads a significantly larger class graph per request than the legacy Glue stack—the Symfony kernel, serializer, validator, security components, and the generated resource classes. Opcache must be enabled on all deployed environments; without it, every request recompiles this class graph, adding a flat overhead of seconds per request. For configuration details, see &lt;a href=&quot;/docs/dg/dev/guidelines/performance-guidelines/general-performance-guidelines.html#opcache-activation&quot;&gt;Opcache activation&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;cache-warming&quot;&gt;Cache warming&lt;/h3&gt;
&lt;p&gt;API Platform deployment requires two sequential steps, not alternatives:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Generate the API resource classes from the schema files:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;docker/sdk cli glue api:generate
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;Warm the application cache—including the &lt;strong&gt;router cache&lt;/strong&gt;—once the resources from step 1 exist. Run it per Glue application:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;docker/sdk cli &lt;span class=&quot;nv&quot;&gt;GLUE_APPLICATION&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;GLUE_STOREFRONT glue cache:warmup
docker/sdk cli &lt;span class=&quot;nv&quot;&gt;GLUE_APPLICATION&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;GLUE_BACKEND glue cache:warmup
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;API Platform registers its operations as routes in the standard Symfony router, whose compiled matcher and generator are dumped to &lt;code&gt;data/cache/Glue&amp;lt;Storefront|Backend&amp;gt;/&amp;lt;environment&amp;gt;/url_matching_routes.php&lt;/code&gt; and &lt;code&gt;url_generating_routes.php&lt;/code&gt;. &lt;code&gt;cache:warmup&lt;/code&gt; builds these dumps from the resource collection produced in step 1. Add both steps to your deployment and installation recipes for every API Platform application.&lt;/p&gt;
&lt;section class=&apos;info-block info-block--warning&apos;&gt;&lt;i class=&apos;info-block__icon icon-warning&apos;&gt;&lt;/i&gt;&lt;div class=&apos;info-block__content&apos;&gt;&lt;div class=&quot;info-block__title&quot;&gt;Use cache:warmup, not api:router:cache:warm-up&lt;/div&gt;
&lt;p&gt;&lt;code&gt;api:router:cache:warm-up&lt;/code&gt; warms only the legacy Glue (&lt;code&gt;GlueApplication&lt;/code&gt;) custom-route router—it does &lt;strong&gt;not&lt;/strong&gt; build the API Platform router dump. Use &lt;code&gt;cache:warmup&lt;/code&gt; (or &lt;code&gt;cache:clear&lt;/code&gt;) to warm the API Platform router.&lt;/p&gt;
&lt;/div&gt;&lt;/section&gt;
&lt;h4 id=&quot;multi-container-and-cloud-deployments&quot;&gt;Multi-container and cloud deployments&lt;/h4&gt;
&lt;p&gt;In production, applications run with debug disabled. The router dump is then written once and never revalidated—whatever route set it was first built from is frozen for the life of the container.&lt;/p&gt;
&lt;p&gt;In a single-container setup this is harmless: the cache is warmed in the same place that serves requests, with the full route set. In a multi-container topology where resource generation runs in a build container and requests are served by a separate runtime container (for example, AWS ECS), you must guarantee the router dump is built against the complete resource collection &lt;strong&gt;for the runtime container&lt;/strong&gt;—either warmed in the runtime container after deployment, or baked at build time only if &lt;code&gt;data/cache&lt;/code&gt; is shipped to every runtime replica with the full route set.&lt;/p&gt;
&lt;p&gt;If the dump is built before resources are generated (an empty or incomplete collection), the runtime container freezes that empty dump and:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;every API request returns HTTP 404 (Glue code &lt;code&gt;007&lt;/code&gt;, legacy fallthrough) because the route is absent from the matcher;&lt;/li&gt;
&lt;li&gt;once the matcher is partially rebuilt, data endpoints return HTTP 500 from IRI generation (&lt;code&gt;RouteNotFoundException&lt;/code&gt;), because the URL generator dump is also empty;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/docs.json&lt;/code&gt; returns 0 paths, even though &lt;code&gt;api:debug --list&lt;/code&gt; shows the resources resolving correctly.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To recover a frozen container, clear and re-warm the cache (&lt;code&gt;cache:clear&lt;/code&gt;) with the full resource collection present.&lt;/p&gt;
&lt;h3 id=&quot;property-level-access-control&quot;&gt;Property-level access control&lt;/h3&gt;
&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;properties&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;writable&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;true&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;# Can be written&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;readable&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;false&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# Not in responses&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;h2 id=&quot;comparison-with-glue-api&quot;&gt;Comparison with Glue API&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;API Platform&lt;/th&gt;
&lt;th&gt;Glue API&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Definition&lt;/td&gt;
&lt;td&gt;Schema-based (YAML)&lt;/td&gt;
&lt;td&gt;Code-based (PHP)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Documentation&lt;/td&gt;
&lt;td&gt;Auto-generated OpenAPI&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Validation&lt;/td&gt;
&lt;td&gt;Declarative&lt;/td&gt;
&lt;td&gt;Programmatic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Standards&lt;/td&gt;
&lt;td&gt;JSON:API by default; JSON-LD and other formats available&lt;/td&gt;
&lt;td&gt;JSON API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Use cases&lt;/td&gt;
&lt;td&gt;Standard CRUD&lt;/td&gt;
&lt;td&gt;Complex business logic&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Both can coexist in the same application. For further migration guidance, see &lt;a href=&quot;/docs/integrations/spryker-api/migrate-from-glue-to-api-platform/migrate-to-api-platform.html&quot;&gt;Migrate to API Platform&lt;/a&gt;.&lt;/p&gt;
</description>
            <pubDate>Fri, 11 Sep 2026 08:04:21 +0000</pubDate>
            <link>https://docs.spryker.com/docs/integrations/spryker-api/api-platform/api-platform.html</link>
            <guid isPermaLink="true">https://docs.spryker.com/docs/integrations/spryker-api/api-platform/api-platform.html</guid>
            
            
        </item>
        
        <item>
            <title>Install the Products Backend API</title>
            <description>This document describes how to install the [Products Backend API](/docs/pbc/all/product-experience-management/latest/products-backend-api.html), which exposes concrete and abstract product data at `/products` on the Glue Backend API.

## Prerequisites

Install the required features:

| NAME | VERSION | INSTALLATION GUIDE |
| --- | --- | --- |
| Spryker Core | {{page.release_tag}} | [Install the Spryker Core feature](/docs/pbc/all/miscellaneous/latest/install-and-upgrade/install-features/install-the-spryker-core-feature.html) |
| Self-Service Portal | {{page.release_tag}} | [Install Self-Service Portal](/docs/pbc/all/self-service-portal/latest/install/install-self-service-portal.html) |

The Self-Service Portal feature is optional. Install it only to use the `productClass` and `shipmentType` attributes.

Step 1 installs the remaining modules the API depends on, so you do not have to install them beforehand.

The API resource is generated by API Platform. If your project does not use API Platform yet, follow [Integrate API Platform](/docs/integrations/spryker-api/migrate-from-glue-to-api-platform/integrate-api-platform.html) before you continue.

## Install feature core

### 1) Install the required modules

Install the feature and API Platform using Composer:

```bash
composer require spryker-feature/product-experience-management:&quot;{{page.release_tag}}&quot; spryker/api-platform:&quot;^1.0.0&quot; --update-with-dependencies
```

{% info_block warningBox &quot;spryker/api-platform&quot; %}

The Product Experience Management feature declares `spryker/api-platform` as a suggested dependency, not a required one. Install it explicitly, as shown above. Without it, the `/products` resource is never registered and the endpoint returns `404`.

{% endinfo_block %}

The API registers plugins that ship in modules the feature does not constrain to the versions providing them. Update those modules explicitly:

```bash
composer require \
  spryker/category:&quot;^5.25.0&quot; \
  spryker/price-product:&quot;^4.54.0&quot; \
  spryker/product:&quot;^6.57.0&quot; \
  spryker/product-attribute:&quot;^1.22.0&quot; \
  spryker/product-bundle:&quot;^7.29.0&quot; \
  spryker/product-category:&quot;^4.34.0&quot; \
  spryker/product-extension:&quot;^1.6.0&quot; \
  spryker/product-image:&quot;^3.22.0&quot; \
  spryker/shipment-type:&quot;^1.3.0&quot; \
  spryker/stock:&quot;^8.17.0&quot; \
  spryker/tax:&quot;^5.20.0&quot; \
  spryker/tax-product-connector:&quot;^4.13.0&quot; \
  spryker/uuid-behavior:&quot;^1.3.0&quot; \
  --update-with-dependencies
```

&lt;details&gt;
&lt;summary&gt;What each version provides&lt;/summary&gt;

| MODULE | MINIMUM VERSION | PROVIDES |
| --- | --- | --- |
| spryker/product | ^6.57.0 | Collection writers and the plugin stacks registered in step 4 |
| spryker/product-extension | ^1.6.0 | Interfaces of the collection validator plugins |
| spryker/category | ^5.25.0 | `CategoryExistsProductAbstractCollection*ValidatorPlugin`, `spy_category.uuid` |
| spryker/tax | ^5.20.0 | `spy_tax_set.uuid` |
| spryker/tax-product-connector | ^4.13.0 | `TaxSetExistsProductAbstractCollection*ValidatorPlugin` |
| spryker/price-product | ^4.54.0 | Price collection validator plugins, `spy_price_product_store.uuid` |
| spryker/product-image | ^3.22.0 | Image set expander and validator plugins, `spy_product_image_set.uuid` |
| spryker/product-category | ^4.34.0 | `ProductCategoryAbstractCollectionExpanderPlugin` |
| spryker/product-attribute | ^1.22.0 | `SuperAttributeProductConcreteExpanderPlugin` |
| spryker/product-bundle | ^7.29.0 | `ProductBundleProductConcreteCollection*ValidatorPlugin` |
| spryker/shipment-type | ^1.3.0 | `ShipmentTypeExistsProductConcreteCollection*ValidatorPlugin` |
| spryker/stock | ^8.17.0 | `StockProductConcreteCollectionAfterUpdatePlugin`, stock validator plugins |
| spryker/uuid-behavior | ^1.3.0 | UUID behavior used by the new schema definitions |

&lt;/details&gt;

To use the `productClass` and `shipmentType` attributes, install the Self-Service Portal feature as well:

```bash
composer require spryker-feature/self-service-portal:&quot;{{page.release_tag}}&quot; --update-with-dependencies
```

{% info_block warningBox &quot;Verification&quot; %}

Make sure the following modules have been installed:

| MODULE | EXPECTED DIRECTORY |
| --- | --- |
| ProductExperienceManagement | vendor/spryker-feature/product-experience-management |
| ApiPlatform | vendor/spryker/api-platform |

Make sure the installed module versions are at least the ones listed above. If a validator plugin class cannot be found in step 4, the corresponding module is below its minimum version.

{% endinfo_block %}

### 2) Set up configuration

The API references categories, tax sets, prices, and image sets by UUID. Enable the UUID columns for those entities.

**src/Pyz/Zed/Category/CategoryConfig.php**

```php
&lt;?php

namespace Pyz\Zed\Category;

use Spryker\Zed\Category\CategoryConfig as SprykerCategoryConfig;

class CategoryConfig extends SprykerCategoryConfig
{
    /**
     * @return bool
     */
    public function isCategoryUuidEnabled(): bool
    {
        return true;
    }
}
```

**src/Pyz/Zed/PriceProduct/PriceProductConfig.php**

```php
&lt;?php

namespace Pyz\Zed\PriceProduct;

use Spryker\Zed\PriceProduct\PriceProductConfig as SprykerPriceProductConfig;

class PriceProductConfig extends SprykerPriceProductConfig
{
    /**
     * @return bool
     */
    public function isPriceProductStoreUuidEnabled(): bool
    {
        return true;
    }
}
```

**src/Pyz/Zed/ProductImage/ProductImageConfig.php**

```php
&lt;?php

namespace Pyz\Zed\ProductImage;

use Spryker\Zed\ProductImage\ProductImageConfig as SprykerProductImageConfig;

class ProductImageConfig extends SprykerProductImageConfig
{
    /**
     * @return bool
     */
    public function isProductImageSetUuidEnabled(): bool
    {
        return true;
    }
}
```

**src/Pyz/Zed/Tax/TaxConfig.php**

```php
&lt;?php

namespace Pyz\Zed\Tax;

use Spryker\Zed\Tax\TaxConfig as SprykerTaxConfig;

class TaxConfig extends SprykerTaxConfig
{
    /**
     * @return bool
     */
    public function isTaxSetUuidEnabled(): bool
    {
        return true;
    }
}
```

### 3) Set up the database schema and transfer objects

Apply database changes and generate entity and transfer changes:

```bash
console propel:install
console transfer:generate
```

{% info_block warningBox &quot;Verification&quot; %}

Make sure that the following changes have occurred in the database:

| DATABASE ENTITY | TYPE | EVENT |
| --- | --- | --- |
| spy_category.uuid | column | created |
| spy_tax_set.uuid | column | created |
| spy_price_product_store.uuid | column | created |
| spy_product_image_set.uuid | column | created |
| spy_stock.uuid | column | created |

{% endinfo_block %}

{% info_block warningBox &quot;Verification&quot; %}

Make sure that the following changes have been applied in transfer objects:

| TRANSFER | TYPE | EVENT | PATH |
| --- | --- | --- | --- |
| Category.uuid | property | created | src/Generated/Shared/Transfer/CategoryTransfer |
| MoneyValue.uuid | property | created | src/Generated/Shared/Transfer/MoneyValueTransfer |
| ProductImageSet.uuid | property | created | src/Generated/Shared/Transfer/ProductImageSetTransfer |
| StockProduct.stockUuid | property | created | src/Generated/Shared/Transfer/StockProductTransfer |
| ProductConcrete.validFrom | property | created | src/Generated/Shared/Transfer/ProductConcreteTransfer |
| ProductConcrete.validTo | property | created | src/Generated/Shared/Transfer/ProductConcreteTransfer |
| ProductConcrete.productBundle | property | created | src/Generated/Shared/Transfer/ProductConcreteTransfer |
| ProductConcrete.productClasses | property | created | src/Generated/Shared/Transfer/ProductConcreteTransfer |
| ProductConcrete.shipmentTypes | property | created | src/Generated/Shared/Transfer/ProductConcreteTransfer |
| ProductClass | class | created | src/Generated/Shared/Transfer/ProductClassTransfer |
| ProductForBundle | class | created | src/Generated/Shared/Transfer/ProductForBundleTransfer |
| ProductAbstractTaxSetCollection | class | created | src/Generated/Shared/Transfer/ProductAbstractTaxSetCollectionTransfer |
| ProductAbstractRelations.withTaxSet | property | created | src/Generated/Shared/Transfer/ProductAbstractRelationsTransfer |

{% endinfo_block %}

`propel:install` adds the UUID columns, but it leaves them empty for rows that already exist. Generate the missing values:

```bash
console uuid:generate Category spy_category
console uuid:generate Tax spy_tax_set
console uuid:generate PriceProduct spy_price_product_store
console uuid:generate ProductImage spy_product_image_set
console uuid:generate Stock spy_stock
```

Each command reports how many records it updated. Commands for tables that already have UUIDs report zero records and make no changes.

{% info_block warningBox &quot;Existing records&quot; %}

Until you run these commands, existing categories, tax sets, prices, image sets, and warehouses have no UUID. Requests that reference them by UUID fail with a `422` validation error, and read responses return those UUIDs as `null`.

{% endinfo_block %}

### 4) Set up behavior

Register the plugins that validate references and apply cross-module changes for collection operations.

| PLUGIN | SPECIFICATION | PREREQUISITES | NAMESPACE |
| --- | --- | --- | --- |
| SuperAttributeProductConcreteExpanderPlugin | Expands concrete products with their super attribute values. | None | Spryker\Zed\ProductAttribute\Communication\Plugin\Product |
| ProductImageAbstractCollectionExpanderPlugin | Expands abstract product collections with image sets. | None | Spryker\Zed\ProductImage\Communication\Plugin\Product |
| ProductCategoryAbstractCollectionExpanderPlugin | Expands abstract product collections with category assignments. | None | Spryker\Zed\ProductCategory\Communication\Plugin\Product |
| StockProductConcreteCollectionAfterUpdatePlugin | Persists stock changes after a concrete product collection is updated. | None | Spryker\Zed\Stock\Communication\Plugin\Product |
| PriceProductConcreteCollectionCreateValidatorPlugin | Validates prices when concrete products are created. | None | Spryker\Zed\PriceProduct\Communication\Plugin\Product |
| ProductImageSetExistsProductConcreteCollectionCreateValidatorPlugin | Validates that referenced image sets exist. | None | Spryker\Zed\ProductImage\Communication\Plugin\Product |
| StockExistsProductConcreteCollectionCreateValidatorPlugin | Validates that referenced warehouses exist. | None | Spryker\Zed\Stock\Communication\Plugin\Product |
| ShipmentTypeExistsProductConcreteCollectionCreateValidatorPlugin | Validates that referenced shipment types exist. | None | Spryker\Zed\ShipmentType\Communication\Plugin\Product |
| ProductClassExistsProductConcreteCollectionCreateValidatorPlugin | Validates that referenced product classes exist. | Self-Service Portal feature | SprykerFeature\Zed\SelfServicePortal\Communication\Plugin\Product |
| ProductBundleProductConcreteCollectionCreateValidatorPlugin | Validates bundled product assignments. | None | Spryker\Zed\ProductBundle\Communication\Plugin\Product |
| CategoryExistsProductAbstractCollectionCreateValidatorPlugin | Validates that referenced categories exist. | None | Spryker\Zed\Category\Communication\Plugin\Product |
| TaxSetExistsProductAbstractCollectionCreateValidatorPlugin | Validates that referenced tax sets exist. | None | Spryker\Zed\TaxProductConnector\Communication\Plugin\Product |
| PriceProductAbstractCollectionCreateValidatorPlugin | Validates prices when abstract products are created. | None | Spryker\Zed\PriceProduct\Communication\Plugin\Product |

The update validator plugins mirror the create validator plugins and live in the same namespaces.

Add the plugins to `ProductDependencyProvider`. `getProductConcreteExpanderPlugins()` and `getProductAbstractCollectionExpanderPlugins()` already exist in most projects — add the listed plugins to the stacks you have instead of replacing them.

**src/Pyz/Zed/Product/ProductDependencyProvider.php**

```php
&lt;?php

namespace Pyz\Zed\Product;

use Spryker\Zed\Category\Communication\Plugin\Product\CategoryExistsProductAbstractCollectionCreateValidatorPlugin;
use Spryker\Zed\Category\Communication\Plugin\Product\CategoryExistsProductAbstractCollectionUpdateValidatorPlugin;
use Spryker\Zed\PriceProduct\Communication\Plugin\Product\PriceProductAbstractCollectionCreateValidatorPlugin;
use Spryker\Zed\PriceProduct\Communication\Plugin\Product\PriceProductConcreteCollectionCreateValidatorPlugin;
use Spryker\Zed\PriceProduct\Communication\Plugin\Product\PriceProductConcreteCollectionUpdateValidatorPlugin;
use Spryker\Zed\Product\ProductDependencyProvider as SprykerProductDependencyProvider;
use Spryker\Zed\ProductAttribute\Communication\Plugin\Product\SuperAttributeProductConcreteExpanderPlugin;
use Spryker\Zed\ProductBundle\Communication\Plugin\Product\ProductBundleDeactivatorProductConcreteAfterUpdatePlugin;
use Spryker\Zed\ProductBundle\Communication\Plugin\Product\ProductBundleProductConcreteAfterUpdatePlugin;
use Spryker\Zed\ProductBundle\Communication\Plugin\Product\ProductBundleProductConcreteCollectionCreateValidatorPlugin;
use Spryker\Zed\ProductBundle\Communication\Plugin\Product\ProductBundleProductConcreteCollectionUpdateValidatorPlugin;
use Spryker\Zed\ProductCategory\Communication\Plugin\Product\ProductCategoryAbstractCollectionExpanderPlugin;
use Spryker\Zed\ProductImage\Communication\Plugin\Product\ProductImageAbstractCollectionExpanderPlugin;
use Spryker\Zed\ProductImage\Communication\Plugin\Product\ProductImageSetExistsProductConcreteCollectionCreateValidatorPlugin;
use Spryker\Zed\ProductImage\Communication\Plugin\Product\ProductImageSetExistsProductConcreteCollectionUpdateValidatorPlugin;
use Spryker\Zed\PriceProduct\Communication\Plugin\ProductConcrete\PriceProductConcreteAfterUpdatePlugin;
use Spryker\Zed\ProductImage\Communication\Plugin\ProductConcreteAfterUpdatePlugin as ImageSetProductConcreteAfterUpdatePlugin;
use Spryker\Zed\ProductSearch\Communication\Plugin\ProductConcrete\ProductSearchProductConcreteAfterUpdatePlugin;
use Spryker\Zed\ProductValidity\Communication\Plugin\ProductValidityUpdatePlugin;
use Spryker\Zed\ShipmentType\Communication\Plugin\Product\ShipmentTypeExistsProductConcreteCollectionCreateValidatorPlugin;
use Spryker\Zed\ShipmentType\Communication\Plugin\Product\ShipmentTypeExistsProductConcreteCollectionUpdateValidatorPlugin;
use Spryker\Zed\Stock\Communication\Plugin\Product\StockExistsProductConcreteCollectionCreateValidatorPlugin;
use Spryker\Zed\Stock\Communication\Plugin\Product\StockExistsProductConcreteCollectionUpdateValidatorPlugin;
use Spryker\Zed\Stock\Communication\Plugin\Product\StockProductConcreteCollectionAfterUpdatePlugin;
use Spryker\Zed\TaxProductConnector\Communication\Plugin\Product\TaxSetExistsProductAbstractCollectionCreateValidatorPlugin;
use Spryker\Zed\TaxProductConnector\Communication\Plugin\Product\TaxSetExistsProductAbstractCollectionUpdateValidatorPlugin;
use Spryker\Zed\TaxProductConnector\Communication\Plugin\Product\TaxSetProductAbstractCollectionExpanderPlugin;
use SprykerFeature\Zed\SelfServicePortal\Communication\Plugin\Product\ProductClassExistsProductConcreteCollectionCreateValidatorPlugin;
use SprykerFeature\Zed\SelfServicePortal\Communication\Plugin\Product\ProductClassExistsProductConcreteCollectionUpdateValidatorPlugin;
use SprykerFeature\Zed\SelfServicePortal\Communication\Plugin\Product\ProductClassProductConcreteAfterUpdatePlugin;
use SprykerFeature\Zed\SelfServicePortal\Communication\Plugin\Product\ShipmentTypeProductConcretePostUpdatePlugin;

class ProductDependencyProvider extends SprykerProductDependencyProvider
{
    /**
     * @return array&lt;\Spryker\Zed\ProductExtension\Dependency\Plugin\ProductConcreteExpanderPluginInterface&gt;
     */
    protected function getProductConcreteExpanderPlugins(): array
    {
        return [
            // Keep the plugins your project already registers.
            new SuperAttributeProductConcreteExpanderPlugin(),
        ];
    }

    /**
     * @return array&lt;\Spryker\Zed\ProductExtension\Dependency\Plugin\ProductAbstractCollectionExpanderPluginInterface&gt;
     */
    protected function getProductAbstractCollectionExpanderPlugins(): array
    {
        return [
            new TaxSetProductAbstractCollectionExpanderPlugin(),
            new ProductImageAbstractCollectionExpanderPlugin(),
            new ProductCategoryAbstractCollectionExpanderPlugin(),
        ];
    }

    /**
     * @return array&lt;\Spryker\Zed\Product\Dependency\Plugin\ProductConcretePluginUpdateInterface&gt;
     */
    protected function getProductConcreteAfterUpdateCollectionPlugins(): array
    {
        return [
            new ImageSetProductConcreteAfterUpdatePlugin(),
            new PriceProductConcreteAfterUpdatePlugin(),
            new StockProductConcreteCollectionAfterUpdatePlugin(),
            new ProductSearchProductConcreteAfterUpdatePlugin(),
            new ProductValidityUpdatePlugin(),
            new ProductClassProductConcreteAfterUpdatePlugin(),
            new ShipmentTypeProductConcretePostUpdatePlugin(),
            new ProductBundleProductConcreteAfterUpdatePlugin(),
            new ProductBundleDeactivatorProductConcreteAfterUpdatePlugin(),
        ];
    }

    /**
     * @return array&lt;\Spryker\Zed\ProductExtension\Dependency\Plugin\ProductConcreteCollectionCreateValidatorPluginInterface&gt;
     */
    protected function getProductConcreteCollectionCreateValidatorPlugins(): array
    {
        return [
            new PriceProductConcreteCollectionCreateValidatorPlugin(),
            new ProductImageSetExistsProductConcreteCollectionCreateValidatorPlugin(),
            new StockExistsProductConcreteCollectionCreateValidatorPlugin(),
            new ShipmentTypeExistsProductConcreteCollectionCreateValidatorPlugin(),
            new ProductClassExistsProductConcreteCollectionCreateValidatorPlugin(),
            new ProductBundleProductConcreteCollectionCreateValidatorPlugin(),
        ];
    }

    /**
     * @return array&lt;\Spryker\Zed\ProductExtension\Dependency\Plugin\ProductConcreteCollectionUpdateValidatorPluginInterface&gt;
     */
    protected function getProductConcreteCollectionUpdateValidatorPlugins(): array
    {
        return [
            new PriceProductConcreteCollectionUpdateValidatorPlugin(),
            new ProductImageSetExistsProductConcreteCollectionUpdateValidatorPlugin(),
            new StockExistsProductConcreteCollectionUpdateValidatorPlugin(),
            new ShipmentTypeExistsProductConcreteCollectionUpdateValidatorPlugin(),
            new ProductClassExistsProductConcreteCollectionUpdateValidatorPlugin(),
            new ProductBundleProductConcreteCollectionUpdateValidatorPlugin(),
        ];
    }

    /**
     * @return array&lt;\Spryker\Zed\ProductExtension\Dependency\Plugin\ProductAbstractCollectionCreateValidatorPluginInterface&gt;
     */
    protected function getProductAbstractCollectionCreateValidatorPlugins(): array
    {
        return [
            new CategoryExistsProductAbstractCollectionCreateValidatorPlugin(),
            new TaxSetExistsProductAbstractCollectionCreateValidatorPlugin(),
            new PriceProductAbstractCollectionCreateValidatorPlugin(),
        ];
    }

    /**
     * @return array&lt;\Spryker\Zed\ProductExtension\Dependency\Plugin\ProductAbstractCollectionUpdateValidatorPluginInterface&gt;
     */
    protected function getProductAbstractCollectionUpdateValidatorPlugins(): array
    {
        return [
            new CategoryExistsProductAbstractCollectionUpdateValidatorPlugin(),
            new TaxSetExistsProductAbstractCollectionUpdateValidatorPlugin(),
        ];
    }
}
```

{% info_block warningBox &quot;Verification&quot; %}

If a validator plugin is missing, the API accepts references to entities that do not exist and the write fails later with a database error instead of a `422` validation error. If an expander plugin is missing, the corresponding data is absent from read responses.

{% endinfo_block %}

### 5) Generate the API resources

Clear the Glue Backend cache and regenerate the API resources:

```bash
rm -rf data/cache/GlueBackend/&lt;environment&gt;
docker/sdk cli &quot;GLUE_APPLICATION=GLUE_BACKEND vendor/bin/glue api:generate&quot;
docker/sdk cli &quot;GLUE_APPLICATION=GLUE_BACKEND vendor/bin/glue cache:clear&quot;
```

Replace `&lt;environment&gt;` with your environment name, for example, `development`.

## Verification

Request a Back Office access token as described in [Authenticate as a Back Office user](/docs/pbc/all/identity-access-management/latest/manage-using-glue-api/glue-api-authenticate-as-a-back-office-user.html), then retrieve a product collection:

```bash
curl &quot;https://glue-backend.mysprykershop.com/products?page[limit]=1&quot; \
  -H &quot;Authorization: Bearer {access_token}&quot;
```

The integration is successful when the request returns `200` with a `data` array. A `404` means the resource was not generated — repeat step 5 and confirm that `spryker/api-platform` is installed. A `401` means the access token is missing or expired.
</description>
            <pubDate>Thu, 10 Sep 2026 15:49:24 +0000</pubDate>
            <link>https://docs.spryker.com/docs/pbc/all/product-experience-management/latest/install/install-the-products-backend-api.html</link>
            <guid isPermaLink="true">https://docs.spryker.com/docs/pbc/all/product-experience-management/latest/install/install-the-products-backend-api.html</guid>
            
            
        </item>
        
    </channel>
</rss>
