<?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>Fri, 05 Jun 2026 20:57:57 +0000</lastBuildDate>
        <generator>Jekyll v4.2.2</generator>
        
        
        <item>
            <title>Architecture as Code</title>
            <description>&lt;p&gt;Well-documented project architecture enables faster internal and external onboarding, passes audits cleanly and aligns teams on requirements. Without it, your system becomes a black box that only the original developers understand.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Architecture as Code&lt;/strong&gt; treats architecture documentation like code, similarly to the Infrastructure-as-Code concept. Instead of storing architecture in binary formats or external tools (Word, PowerPoint, Visio, Confluence) that become outdated quickly, you store and maintain architecture documentation in version control alongside your implementation.&lt;/p&gt;
&lt;p&gt;Spryker ships the architecture/ folder with a complete Architecture as Code structure with its demoshop codebase. You can refer to &lt;code&gt;architecture/README.md&lt;/code&gt; that describes how to work with it. Below, you find all the general information about the structure and principles. We go into the details of each aspect to help you understand and extend this architecture documentation in your project.&lt;/p&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;Existing projects&lt;/div&gt;
&lt;p&gt;If your project is based on a Spryker release before 202602.0, refer to the &lt;a href=&quot;https://github.com/spryker-shop/b2b-demo-marketplace&quot;&gt;Spryker B2B Demo Marketplace&lt;/a&gt; master branch source code to see how the Architecture as Code structure is implemented and to copy the “architecture/” folder into your project.&lt;/p&gt;
&lt;/div&gt;&lt;/section&gt;
&lt;h2 id=&quot;why-architecture-as-code&quot;&gt;Why Architecture as Code&lt;/h2&gt;
&lt;p&gt;Traditional architecture documentation suffers from several challenges:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Documentation drift&lt;/strong&gt; - Binary formats stored separately from code become outdated quickly&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version control gaps&lt;/strong&gt; - Specialized tools do not integrate with Git workflows&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Collaboration barriers&lt;/strong&gt; - Requires specialized software and binary file merging&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI limitations&lt;/strong&gt; - AI tools understand code and Markdown effectively but struggle with proprietary formats and binary documents&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Architecture as Code solves these problems:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Version controlled&lt;/strong&gt; - Store in Git alongside implementation. Track every change with full history.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI-ready&lt;/strong&gt; - Markdown and diagrams-as-code enable AI assistance, automated validation, and intelligent analysis.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Standard formats&lt;/strong&gt; - Use industry standards (arc42, C4, ADRs, Mermaid) that external teams understand immediately.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Collaborative&lt;/strong&gt; - Review through pull requests. No special tools required — only a text editor.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Living documentation&lt;/strong&gt; - Update during development, not after. Documentation evolves with your system.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;core-principles&quot;&gt;Core Principles&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Standards over invention&lt;/strong&gt; - Use industry-proven formats and standards (arc42, C4, ADRs, Mermaid) instead of custom documentation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Minimal but sufficient&lt;/strong&gt; - Start simple. Prioritize clarity and expansion over completeness on day one.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Architecture is code&lt;/strong&gt; - Write in plain text, store in Git, review through pull requests, deploy automatically.&lt;/p&gt;
&lt;h2 id=&quot;concepts-and-choices&quot;&gt;Concepts and Choices&lt;/h2&gt;
&lt;h3 id=&quot;arc42&quot;&gt;arc42&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://arc42.org/&quot;&gt;arc42&lt;/a&gt; is a proven template for architecture documentation used globally across industries.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why arc42 fits Spryker projects:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Spryker projects vary dramatically in complexity — from simple B2C shops to complex B2B marketplaces with extensive integrations and order management systems. arc42 is flexible enough to cover architecture of any complexity and scales as your Spryker implementation grows. You can start simple with minimal sections and expand as your architecture requires more detail.&lt;/p&gt;
&lt;p&gt;The template includes 12 sections (described below) covering all architectural aspects. Section 4 (Solution Designs) provides RFC-style exploration templates. Section 9 (Architecture Decisions) uses ADRs to document decisions with context and consequences. This workflow — explore with Solution Designs, then document decisions with ADRs — ensures thoughtful architecture evolution.&lt;/p&gt;
&lt;h3 id=&quot;c4-model&quot;&gt;C4 Model&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://c4model.com/&quot;&gt;C4 Model&lt;/a&gt; provides a hierarchical approach to system visualization with four levels of abstraction.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why C4 fits Spryker architecture:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Spryker architecture unfolds naturally through C4 layers — start with the system context, zoom into containers (Yves, Zed, Client, databases, services), then dive deeper into layers and components as needed. This progressive detail matches how Spryker complexity reveals itself. The entire Spryker feature set can be shown using this unfolding approach.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Flexibility advantage:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;You control the depth. Start at C1 (context) and continue only as deep as your documentation needs require. Given limited architect time, this flexibility is essential — stop at the level appropriate for your stakeholders and complexity.&lt;/p&gt;
&lt;h3 id=&quot;mermaid-and-plantuml&quot;&gt;Mermaid and PlantUML&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Mermaid&lt;/strong&gt; - Our primary choice for diagramming-as-code:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Renders automatically in GitHub and GitLab, and can be rendered via plugins in popular IDEs&lt;/li&gt;
&lt;li&gt;No proprietary tools required&lt;/li&gt;
&lt;li&gt;Covers 90% of diagram needs: flowcharts, sequences, C4 diagrams&lt;/li&gt;
&lt;li&gt;Online editor at &lt;a href=&quot;https://mermaid.live/&quot;&gt;mermaid.live&lt;/a&gt; for convenient editing and better visualization than most IDEs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;PlantUML&lt;/strong&gt; - For precision when needed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Essential for Entity-Relationship Diagrams where field-level precision matters&lt;/li&gt;
&lt;li&gt;Online editor at &lt;a href=&quot;https://www.plantuml.com/plantuml/&quot;&gt;plantuml.com&lt;/a&gt; for live editing and preview&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;what-comes-out-of-the-box&quot;&gt;What Comes Out of the Box&lt;/h2&gt;
&lt;p&gt;We provide templates with clear structure and minimal context, plus examples of the most common diagram types (C4, data flow, integration, sequence) and documentation patterns (ADRs, Solution Designs). AI tools work most efficiently when they see the methodology (like arc42), understand the structure, and have examples to follow. This approach enables you to generate architecture documentation much faster with AI assistance.&lt;/p&gt;
&lt;p&gt;Templates provide examples and structural guidance — you should adapt content to your project’s context. You do not need to implement all sections immediately; remove or keep unused sections based on your documentation strategy.&lt;/p&gt;
&lt;h3 id=&quot;folder-structure&quot;&gt;Folder Structure&lt;/h3&gt;
&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;architecture/
├── 01-introduction-and-goals.md           # Requirements, quality goals, stakeholders
├── 02-constraints.md                      # Technical, organizational constraints
├── 03-system-scope-and-context.md         # System boundaries, external interfaces
│                                          # Includes: External systems and integration tables
├── 04-solution-designs/                   # RFC-style exploration documents
│   ├── README.md                          # Workflow explanation
│   └── sd-000-template.md                 # Solution design template
├── 05-building-block-view.md              # System decomposition
├── 06-runtime-view.md                     # Behavior and interactions
├── 07-deployment-view.md                  # Infrastructure topology
├── 08-crosscutting-concepts.md            # Patterns spanning components
├── 09-architecture-decisions/             # Architecture decision records
│   ├── README.md                          # ADR workflow and sources
│   └── adr-000-template.md                # ADR template
├── 10-quality-requirements.md             # Performance, scalability, testing
│                                          # Includes: Volume planning, testing strategy
├── 11-risks-and-technical-debt.md         # Known issues and mitigation
├── 12-glossary.md                         # Domain terminology
├── diagrams/
│   ├── c4/
│   │   ├── c1-system-context.mmd          # System context diagram
│   │   ├── c2-spryker-container.mmd       # Container diagram
│   │   └── c3-component-diagram.mmd       # Component diagram
│   ├── data-flow/
│   │   └── product-price-data-flow.mmd    # Price data flow example
│   ├── integration/
│   │   └── product-price-integration.mmd  # Integration overview with protocols
│   ├── sequence/
│   │   ├── api-payment.mmd                # Payment flow
│   │   ├── publish-sync.mmd               # Publish and Sync process
│   │   └── punchout-greenwing-integration.mmd  # PunchOut example
│   └── erd/                               # Entity-relationship diagrams (PlantUML)
└── README.md                              # Quick start and overview
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;h3 id=&quot;diagram-organization-and-best-practices&quot;&gt;Diagram Organization and Best Practices&lt;/h3&gt;
&lt;p&gt;You have two approaches to managing diagrams. Choose based on your documentation use case.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Approach 1: Inline Diagrams&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Write diagram code directly in markdown using code fences:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&gt;flowchart LR
    A --&amp;gt; B
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Diagrams are visible immediately; self-contained&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; Increases file size; duplicates code if reused&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Approach 2: External Diagram Files&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Store diagram code in &lt;code&gt;/diagrams/&lt;/code&gt; folder and reference via links:&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;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;C1 System Context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;](&lt;/span&gt;&lt;span class=&quot;sx&quot;&gt;diagrams/c4/c1-system-context.mmd&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;Pros:&lt;/strong&gt; Single source-of-truth; cleaner markdown; scalable&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; Requires clicking to view (less immediate)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Choosing Your Approach&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This template uses &lt;strong&gt;Approach 2&lt;/strong&gt; for scalability and maintainability, but projects can mix both — use external files for core views, inline for one-off diagrams.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Universal Color Scheme:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;All diagrams use colors optimized for both light and dark modes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Orange &lt;code&gt;#E67E22&lt;/code&gt; - Communication layer&lt;/li&gt;
&lt;li&gt;Blue &lt;code&gt;#2980B9&lt;/code&gt; - Backend services, APIs&lt;/li&gt;
&lt;li&gt;Green &lt;code&gt;#27AE60&lt;/code&gt; - Web apps, external systems&lt;/li&gt;
&lt;li&gt;Purple &lt;code&gt;#9B59B6&lt;/code&gt; - Storage (databases, caches)&lt;/li&gt;
&lt;li&gt;Gray &lt;code&gt;#95A5A6&lt;/code&gt; - Infrastructure&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;templates-and-guidelines&quot;&gt;Templates and Guidelines&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Solution Design Template&lt;/strong&gt; (&lt;code&gt;04-solution-designs/sd-000-template.md&lt;/code&gt;):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Metadata section (status, date, stakeholders)&lt;/li&gt;
&lt;li&gt;Problem statement&lt;/li&gt;
&lt;li&gt;Goals and requirements&lt;/li&gt;
&lt;li&gt;Proposed solution with diagrams&lt;/li&gt;
&lt;li&gt;Implementation plan&lt;/li&gt;
&lt;li&gt;Trade-offs and alternatives&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;ADR Template&lt;/strong&gt; (&lt;code&gt;09-architecture-decisions/adr-000-template.md&lt;/code&gt;):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Standard ADR structure&lt;/li&gt;
&lt;li&gt;Status tracking&lt;/li&gt;
&lt;li&gt;Context, decision, consequences&lt;/li&gt;
&lt;li&gt;Links to related decisions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Volume Planning Table&lt;/strong&gt; (Section 10):&lt;/p&gt;
&lt;p&gt;Pre-structured table for capacity planning covering:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Catalog entities (products, categories, prices)&lt;/li&gt;
&lt;li&gt;Cart constraints&lt;/li&gt;
&lt;li&gt;User load projections&lt;/li&gt;
&lt;li&gt;B2B customers structure&lt;/li&gt;
&lt;li&gt;Marketplace metrics&lt;/li&gt;
&lt;li&gt;Internationalization requirements&lt;/li&gt;
&lt;li&gt;Orders and infrastructure&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;getting-help&quot;&gt;Getting Help&lt;/h2&gt;
&lt;h3 id=&quot;resources&quot;&gt;Resources&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;arc42 Documentation&lt;/strong&gt;: &lt;a href=&quot;https://arc42.org/&quot;&gt;https://arc42.org/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;C4 Model Guide&lt;/strong&gt;: &lt;a href=&quot;https://c4model.com/&quot;&gt;https://c4model.com/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mermaid Syntax&lt;/strong&gt;: &lt;a href=&quot;https://mermaid.js.org/&quot;&gt;https://mermaid.js.org/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ADR Guidelines&lt;/strong&gt;: &lt;a href=&quot;https://adr.github.io/&quot;&gt;https://adr.github.io/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;common-questions&quot;&gt;Common Questions&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Q: Do I need to fill in every arc42 section?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A: No. Fill in what is relevant for your project. Some sections may remain minimal.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q: Can I add custom sections?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A: First, check all 12 arc42 sections — the standard format covers most architectural concerns. If no existing section fits your content, you can add a custom section. Ensure the section purpose is clear and describe it in the README.md file.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q: How detailed should diagrams be?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A: For C4 diagrams, follow the levels: Context (high-level), Container (more detail), Component (detailed). For other diagram types (data flow, integration, sequence), follow industry standards for that specific notation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q: When should I create an ADR vs Solution Design?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A: Use Solution Design for exploration. Use ADR for documenting the final decision.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q: Can I use different diagram notations?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A: Yes. The choice is yours — we do not limit you to Mermaid or PlantUML. Ensure your notation follows the core principles: viewable by people, understandable by AI tools, and ideally diagrams-as-code (text-based format in version control).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q: Will diagrams-as-code replace whiteboards and visual editing tools?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A: No. The art of creating and drawing diagrams — whether on whiteboards, visual tools, or pen and paper — remains valuable. Diagrams-as-code is for documenting what you have drawn and decided. Use whatever tools help you think and collaborate, then capture the final result as code for version control and documentation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q: Can I use images as diagrams instead of diagrams-as-code?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A: Yes, it is better to have image diagrams than no diagrams at all, but understand the trade-off. Hand-drawn or visually edited diagrams often look more polished than generated ones. However, images are not as understandable or generable by AI (at least currently) and lack version control benefits. You can use both: include beautiful images for presentations and stakeholder communication, alongside diagrams-as-code for AI assistance and documentation processes.&lt;/p&gt;
</description>
            <pubDate>Fri, 05 Jun 2026 20:57:42 +0000</pubDate>
            <link>https://docs.spryker.com/docs/dg/dev/architecture/architecture-as-code.html</link>
            <guid isPermaLink="true">https://docs.spryker.com/docs/dg/dev/architecture/architecture-as-code.html</guid>
            
            
        </item>
        
        <item>
            <title>Migration status - Glue API to API Platform</title>
            <description>This document tracks Spryker&apos;s migration of API-providing modules to the **API Platform** (built on Symfony and the API Platform library). Use it to plan upgrades and check the current status of every module.

{% info_block infoBox &quot;Looking for the integration guide?&quot; %}

This page does **not** describe how to integrate API Platform into your project. For step-by-step integration instructions, see:

- [Migrate to API Platform](/docs/dg/dev/upgrade-and-migrate/migrate-to-api-platform.html)
- [Integrate API Platform](/docs/dg/dev/upgrade-and-migrate/integrate-api-platform.html)
- [Integrate API Platform security](/docs/dg/dev/upgrade-and-migrate/integrate-api-platform-security.html)
- [API Platform architecture](/docs/dg/dev/architecture/api-platform.html)

{% endinfo_block %}

## Why Spryker is moving to API Platform

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.

| Aspect | Previous infrastructure | API Platform |
|---|---|---|
| Bootstrap | Spryker-specific application bootstrap | Symfony Kernel-based routing |
| Resource registration | Manual plugin registration in `GlueApplicationDependencyProvider` | Declarative YAML resource definitions (`*.resource.yml`) |
| Authentication | Custom flows per module | Standard OAuth2 / Symfony Security |
| Coupling | Tight coupling between resource and routing logic | Clean separation: provider + resource schema + validation |
| Testability | Complex to test and extend | Symfony-native, testable with standard PHPUnit patterns |
| OpenAPI | Manual / partial | Automatic OpenAPI schema generation |

## General migration workflow

For any module marked **Migrated**, projects upgrade in three high-level steps. Detailed instructions are in the linked integration guides above.

1. **Update the module to the API Platform-enabled version**

   Pull the new module version that ships the `*.resource.yml` schema and Provider class:

    ```bash
    composer update spryker/&lt;module-name&gt;
    ```

2. **Remove the previous resource plugins**

   In your project&apos;s `GlueApplicationDependencyProvider`, remove the previous plugin registrations for the migrated module - typically a `ResourceRoutePlugin` (and any related `ResourceRelationshipPlugin` / expander plugins) registered in `getResourceRoutePlugins()`.

   For **extension-only** modules, remove or replace the corresponding plugin wiring in the parent module&apos;s dependency provider as indicated in the module&apos;s release notes.

3. **Clear caches and verify**

    ```bash
    vendor/bin/glue cache:clear
    vendor/bin/glue api:generate
    ```

   Confirm the endpoint is served by API Platform by hitting it against your local Glue host - the response is now produced by the new Symfony-based stack.

{% info_block infoBox &quot;Parallel operation&quot; %}

The previous API stack and API Platform run **side by side** during the transition. You can migrate modules incrementally; modules that have not been migrated continue to be served by the previous stack.

{% endinfo_block %}

### Status legend

| Status | Meaning |
|---|---|
| Migrated | Module is available on API Platform and production-ready. |
| Planned | Module is scheduled or queued for migration to API Platform. |

## Storefront API modules

All StorefrontAPI and Extension-only StorefrontAPI modules. Migrated modules are listed first.

| Module | Category | Status | Requires | Key endpoints |
|---|---|---|---|---|
| ContentProductAbstractListsRestApi | StorefrontAPI | Migrated | ProductsRestApi | GET /content-product-abstract-lists/{id}&lt;br&gt;GET /content-product-abstract-lists/{id}/abstract-products |
| MerchantOpeningHoursRestApi | StorefrontAPI | Migrated | — | GET /merchants/{id}/merchant-opening-hours |
| MerchantCategoriesRestApi | Extension-only StorefrontAPI | Migrated | MerchantsRestApi | (extension-only) |
| MerchantProductOffersRestApi | StorefrontAPI | Migrated | — | GET /concrete-products/{id}/product-offers&lt;br&gt;GET /product-offers/{id} |
| MerchantProductOfferServicePointAvailabilitiesRestApi | Extension-only StorefrontAPI | Migrated | ProductOfferServicePointAvailabilitiesRestApi | (extension-only) |
| MerchantsRestApi | StorefrontAPI | Migrated | — | GET /merchants&lt;br&gt;GET /merchants/{id}&lt;br&gt;GET /merchants/{id}/merchant-addresses |
| OrderPaymentsRestApi | StorefrontAPI | Migrated | — | POST /order-payments |
| PaymentsRestApi | StorefrontAPI | Migrated | — | POST /payments&lt;br&gt;POST /payment-cancellations&lt;br&gt;POST /payment-customers |
| ProductAvailabilitiesRestApi | StorefrontAPI | Migrated | ProductsRestApi | GET /abstract-products/{id}/abstract-product-availabilities&lt;br&gt;GET /concrete-products/{id}/concrete-product-availabilities |
| ProductOfferAvailabilitiesRestApi | StorefrontAPI | Migrated | — | GET /product-offers/{id}/product-offer-availabilities |
| ProductOfferServicePointAvailabilitiesRestApi | StorefrontAPI | Migrated | — | POST /product-offer-service-point-availabilities |
| ProductOfferPricesRestApi | StorefrontAPI | Migrated | — | GET /product-offers/{id}/product-offer-prices |
| ProductPricesRestApi | StorefrontAPI | Migrated | ProductsRestApi | GET /abstract-products/{id}/abstract-product-prices&lt;br&gt;GET /concrete-products/{id}/concrete-product-prices |
| ProductTaxSetsRestApi | StorefrontAPI | Migrated | — | GET /abstract-products/{id}/product-tax-sets |
| ProductsRestApi | StorefrontAPI | Migrated | — | GET /abstract-products/{id}&lt;br&gt;GET /concrete-products/{id} |
| ShipmentTypeProductOfferServicePointAvailabilitiesRestApi | Extension-only StorefrontAPI | Migrated | ProductOfferServicePointAvailabilitiesRestApi | (extension-only) |
| StoresApi | StorefrontAPI | Migrated | — | GET /stores |
| AgentAuthRestApi | StorefrontAPI | Migrated | — | POST /agent-access-tokens&lt;br&gt;POST /agent-customer-impersonation-access-tokens&lt;br&gt;GET /agent-customer-search |
| AlternativeProductsRestApi | StorefrontAPI | Migrated | ProductsRestApi | GET /abstract-products/{id}/related-products&lt;br&gt;GET /concrete-products/{id}/abstract-alternative-products&lt;br&gt;GET /concrete-products/{id}/concrete-alternative-products |
| AuthRestApi | StorefrontAPI | Migrated | — | POST /token&lt;br&gt;POST /access-tokens&lt;br&gt;POST /refresh-tokens&lt;br&gt;DELETE /refresh-tokens/{id} |
| AvailabilityNotificationsRestApi | StorefrontAPI | Migrated | — | POST /availability-notifications&lt;br&gt;DELETE /availability-notifications/{id}&lt;br&gt;GET /my-availability-notifications&lt;br&gt;GET /customers/{id}/availability-notifications |
| CartCodesRestApi | StorefrontAPI | Migrated | CartsRestApi | POST /carts/{id}/cart-codes&lt;br&gt;DELETE /carts/{id}/cart-codes/{id}&lt;br&gt;POST /guest-carts/{id}/cart-codes&lt;br&gt;DELETE /guest-carts/{id}/cart-codes/{id} |
| CartPermissionGroupsRestApi | StorefrontAPI | Migrated | — | GET /cart-permission-groups&lt;br&gt;GET /cart-permission-groups/{id} |
| CartReorderRestApi | StorefrontAPI | Migrated | CartsRestApi | POST /cart-reorder |
| CartsRestApi | StorefrontAPI | Migrated | — | GET,POST /carts&lt;br&gt;GET,PATCH,DELETE /carts/{id}&lt;br&gt;POST /carts/{id}/items&lt;br&gt;PATCH,DELETE /carts/{id}/items/{id}&lt;br&gt;GET /guest-carts&lt;br&gt;GET,PATCH /guest-carts/{id}&lt;br&gt;POST /guest-carts/{id}/guest-cart-items&lt;br&gt;PATCH,DELETE /guest-carts/{id}/guest-cart-items/{id}&lt;br&gt;GET /customers/{id}/carts |
| CatalogSearchRestApi | StorefrontAPI | Migrated | — | GET /catalog-search&lt;br&gt;GET /catalog-search-suggestions |
| CategoriesRestApi | StorefrontAPI | Migrated | — | GET /category-trees&lt;br&gt;GET /category-nodes/{id} |
| CheckoutRestApi | StorefrontAPI | Migrated | CartsRestApi | POST /checkout-data&lt;br&gt;POST /checkout |
| CmsPagesRestApi | StorefrontAPI | Migrated | — | GET /cms-pages&lt;br&gt;GET /cms-pages/{id} |
| CompaniesRestApi | StorefrontAPI | Migrated | — | GET /companies&lt;br&gt;GET /companies/{id} |
| CompanyBusinessUnitAddressesRestApi | StorefrontAPI | Migrated | — | GET /company-business-unit-addresses&lt;br&gt;GET /company-business-unit-addresses/{id} |
| CompanyBusinessUnitsRestApi | StorefrontAPI | Migrated | — | GET /company-business-units&lt;br&gt;GET /company-business-units/{id} |
| CompanyRolesRestApi | StorefrontAPI | Migrated | — | GET /company-roles&lt;br&gt;GET /company-roles/{id} |
| CompanyUserAuthRestApi | StorefrontAPI | Migrated | — | POST /company-user-access-tokens |
| CompanyUsersRestApi | StorefrontAPI | Migrated | — | GET /company-users&lt;br&gt;GET /company-users/{id} |
| ConfigurableBundleCartsRestApi | StorefrontAPI | Migrated | CartsRestApi | POST /carts/{id}/configured-bundles&lt;br&gt;PATCH,DELETE /carts/{id}/configured-bundles/{id}&lt;br&gt;POST,PATCH,DELETE /guest-carts/{id}/guest-configured-bundles/{id} |
| ConfigurableBundlesRestApi | StorefrontAPI | Migrated | — | GET /configurable-bundle-templates&lt;br&gt;GET /configurable-bundle-templates/{id} |
| ContentBannersRestApi | StorefrontAPI | Migrated | — | GET /content-banners/{id} |
| CustomerAccessRestApi | StorefrontAPI | Migrated | — | GET /customer-access |
| CustomersRestApi | StorefrontAPI | Migrated | — | GET,POST /customers&lt;br&gt;GET,PATCH,DELETE /customers/{id}&lt;br&gt;GET,POST /customers/{id}/addresses&lt;br&gt;GET,PATCH,DELETE /customers/{id}/addresses/{id}&lt;br&gt;POST /customer-forgotten-password&lt;br&gt;PATCH /customer-restore-password/{id}&lt;br&gt;PATCH /customer-password/{id}&lt;br&gt;POST /customer-confirmation |
| DiscountPromotionsRestApi | Extension-Only-StorefrontAPI | Migrated | CartCodesRestApi, CartsRestApi | (extension-only) |
| DiscountsRestApi | StorefrontAPI | Migrated | — | POST /carts/{id}/vouchers&lt;br&gt;DELETE /carts/{id}/vouchers/{id}&lt;br&gt;POST /guest-carts/{id}/vouchers&lt;br&gt;DELETE /guest-carts/{id}/vouchers/{id} |
| EntityTagsRestApi | Extension-only StorefrontAPI | Planned | — | (extension-only) |
| GiftCardsRestApi | Extension-only StorefrontAPI | Migrated | — | (extension-only) |
| MerchantProductOfferServicePointAvailabilitiesRestApi | Extension-only StorefrontAPI | Planned | ProductOfferServicePointAvailabilitiesRestApi | (extension-only) |
| MerchantProductOfferShoppingListsRestApi | Extension-only StorefrontAPI | Migrated | — | (extension-only) |
| MerchantProductOfferWishlistRestApi | Extension-only StorefrontAPI | Migrated | WishlistsRestApi | (extension-only) |
| MerchantProductShoppingListsRestApi | Extension-only StorefrontAPI | Migrated | — | (extension-only) |
| MerchantProductsRestApi | Extension-only StorefrontAPI | Migrated | CartsRestApi | (extension-only) |
| MerchantRelationshipProductListsRestApi | Extension-only StorefrontAPI | Migrated | CustomersRestApi | (extension-only) |
| MerchantSalesReturnsRestApi | Extension-only StorefrontAPI | Migrated | — | (extension-only) |
| MerchantShipmentsRestApi | Extension-only StorefrontAPI | Migrated | ShipmentsRestApi | (extension-only) |
| MultiCartsRestApi | Extension-only StorefrontAPI | Migrated | CartsRestApi | (extension-only) |
| MultiFactorAuth | StorefrontAPI | Migrated | — | GET /multi-factor-auth-types, POST /multi-factor-auth-trigger, POST /multi-factor-auth-type-activate, POST /multi-factor-auth-type-verify, POST /multi-factor-auth-type-deactivate |
| NavigationsRestApi | StorefrontAPI | Migrated | — | GET /navigations/{id} |
| OauthApi | StorefrontAPI | Migrated | — | POST /token |
| OmsRestApi | Extension-only StorefrontAPI | Migrated | OrdersRestApi | (extension-only) |
| OrderAmendmentsRestApi | Extension-only StorefrontAPI | Migrated | CartReorderRestApi, CartsRestApi, OrdersRestApi | (extension-only) |
| OrdersRestApi | StorefrontAPI | Migrated | — | GET /orders&lt;br&gt;GET /orders/{orderReference}&lt;br&gt;GET /orders/{orderReference}/order-items/{uuid}&lt;br&gt;GET /customers/{customerReference}/orders |
| PriceProductOfferVolumesRestApi | Extension-only StorefrontAPI | Migrated | ProductOfferPricesRestApi | (extension-only) |
| PriceProductVolumesRestApi | Extension-only StorefrontAPI | Migrated | ProductPricesRestApi | (extension-only) |
| ProductAttributesRestApi | StorefrontAPI | Migrated | — | GET /product-management-attributes&lt;br&gt;GET /product-management-attributes/{id} |
| ProductBundleCartsRestApi | Extension-only StorefrontAPI | Migrated | CartsRestApi, ShipmentsRestApi | (extension-only) |
| ProductBundlesRestApi | StorefrontAPI | Migrated | OrdersRestApi | GET /concrete-products/{id}/bundled-products |
| ProductConfigurationShoppingListsRestApi | Extension-only StorefrontAPI | Migrated | ShoppingListsRestApi | (extension-only) |
| ProductConfigurationWishlistsRestApi | Extension-only StorefrontAPI | Migrated | WishlistsRestApi | (extension-only) |
| ProductConfigurationsPriceProductVolumesRestApi | Extension-only StorefrontAPI | Migrated | ProductConfigurationShoppingListsRestApi, ProductConfigurationWishlistsRestApi, ProductConfigurationsRestApi | (extension-only) |
| ProductConfigurationsRestApi | Extension-only StorefrontAPI | Migrated | CartsRestApi, OrdersRestApi, ProductsRestApi | (extension-only) |
| ProductDiscontinuedRestApi | Extension-only StorefrontAPI | Migrated | ProductsRestApi | (extension-only) |
| ProductImageSetsRestApi | StorefrontAPI | Migrated | ProductsRestApi | GET /abstract-products/{id}/abstract-product-image-sets&lt;br&gt;GET /concrete-products/{id}/concrete-product-image-sets |
| ProductLabelsRestApi | StorefrontAPI | Migrated | — | GET /product-labels/{id} |
| ProductMeasurementUnitsRestApi | StorefrontAPI | Migrated | — | GET /product-measurement-units/{id}&lt;br&gt;GET /concrete-products/{id}/sales-units |
| ProductOfferSalesRestApi | Extension-only StorefrontAPI | Migrated | — | (extension-only) |
| ProductOfferShoppingListsRestApi | Extension-only StorefrontAPI | Migrated | — | (extension-only) |
| ProductOffersRestApi | Extension-only StorefrontAPI | Migrated | ProductsRestApi | (extension-only) |
| ProductOptionsRestApi | Extension-only StorefrontAPI | Migrated | CartsRestApi, OrdersRestApi, ProductsRestApi, QuoteRequestsRestApi | (extension-only) |
| ProductReviewsRestApi | StorefrontAPI | Migrated | — | GET,POST /abstract-products/{id}/product-reviews&lt;br&gt;GET /abstract-products/{id}/product-reviews/{id} |
| QuoteRequestAgentsRestApi | StorefrontAPI | Migrated | QuoteRequestsRestApi | GET,POST /agent-quote-requests&lt;br&gt;GET,PATCH /agent-quote-requests/{id}&lt;br&gt;POST /agent-quote-requests/{id}/agent-quote-request-cancel&lt;br&gt;POST /agent-quote-requests/{id}/agent-quote-request-revise&lt;br&gt;POST /agent-quote-requests/{id}/agent-quote-request-send-to-customer |
| QuoteRequestsRestApi | StorefrontAPI | Migrated | CartsRestApi | GET,POST /quote-requests&lt;br&gt;GET,PATCH /quote-requests/{id}&lt;br&gt;POST /quote-requests/{id}/quote-request-cancel&lt;br&gt;POST /quote-requests/{id}/quote-request-revise&lt;br&gt;POST /quote-requests/{id}/quote-request-send-to-user&lt;br&gt;POST /quote-requests/{id}/quote-request-convert-to-quote |
| RelatedProductsRestApi | StorefrontAPI | Migrated | ProductsRestApi | GET /abstract-products/{id}/related-products |
| SalesOrderThresholdsRestApi | Extension-only StorefrontAPI | Migrated | CartsRestApi, CheckoutRestApi | (extension-only) |
| SalesReturnsRestApi | StorefrontAPI | Migrated | — | GET /return-reasons&lt;br&gt;GET,POST /returns&lt;br&gt;GET /returns/{id} |
| SecurityBlockerRestApi | Extension-only StorefrontAPI | Migrated | — | (extension-only) |
| ServicePointCartsRestApi | Extension-only StorefrontAPI | Migrated | CheckoutRestApi | (extension-only) |
| ServicePointsRestApi | StorefrontAPI | Migrated | — | GET /service-points&lt;br&gt;GET /service-points/{id}&lt;br&gt;GET /service-points/{id}/service-point-addresses/{id} |
| SharedCartsRestApi | StorefrontAPI | Migrated | — | POST /carts/{id}/shared-carts&lt;br&gt;PATCH,DELETE /shared-carts/{id} |
| ShipmentTypeServicePointsRestApi | Extension-only StorefrontAPI | Migrated | CheckoutRestApi, ServicePointsRestApi, ShipmentTypesRestApi, ShipmentsRestApi | (extension-only) |
| ShipmentTypesRestApi | StorefrontAPI | Migrated | — | GET /shipment-types&lt;br&gt;GET /shipment-types/{id} |
| ShipmentsRestApi | Extension-only StorefrontAPI | Migrated | CheckoutRestApi, OrdersRestApi, QuoteRequestsRestApi | (extension-only) |
| ShoppingListsRestApi | StorefrontAPI | Migrated | — | GET,POST /shopping-lists&lt;br&gt;GET,PATCH,DELETE /shopping-lists/{id}&lt;br&gt;POST /shopping-lists/{id}/shopping-list-items&lt;br&gt;PATCH,DELETE /shopping-lists/{id}/shopping-list-items/{id} |
| TaxAppRestApi | StorefrontAPI | Planned | — | POST /tax-id-validate |
| UpSellingProductsRestApi | StorefrontAPI | Migrated | CartsRestApi, ProductsRestApi | GET /carts/{id}/up-selling-products&lt;br&gt;GET /guest-carts/{id}/up-selling-products |
| UrlsRestApi | StorefrontAPI | Migrated | — | GET /url-resolver |
| Vertex | StorefrontAPI | Migrated | — | POST /tax-id-validate |
| WishlistsRestApi | StorefrontAPI | Migrated | — | GET,POST /wishlists&lt;br&gt;GET,PATCH,DELETE /wishlists/{id}&lt;br&gt;POST /wishlists/{id}/wishlist-items&lt;br&gt;PATCH,DELETE /wishlists/{id}/wishlist-items/{id} |

## Backend API modules

All BackendAPI modules tracked in the migration scope.

| Module | Category | Status | Requires | Key endpoints |
|---|---|---|---|---|
| CartNotesBackendApi | Extension-Only BackendAPI | Planned | SalesOrdersBackendApi | (extension-only) |
| CategoriesBackendApi | BackendAPI | Planned | — | GET,POST /categories&lt;br&gt;GET,PATCH /categories/{id} |
| DynamicEntityBackendApi | BackendAPI | Planned | — | GET,POST,PATCH,PUT /dynamic-entity/{entity-name} (~62 auto-generated entity endpoints) |
| OauthBackendApi | BackendAPI | Planned | — | POST /token |
| PickingListsBackendApi | BackendAPI | Planned | — | GET /picking-lists&lt;br&gt;GET /picking-lists/{id}&lt;br&gt;PATCH /picking-lists/{id}/picking-list-items/{id}&lt;br&gt;POST /start-picking |
| PickingListsUsersBackendApi | Extension-Only BackendAPI | Planned | PickingListsBackendApi, UsersBackendApi | (extension-only) |
| PickingListsWarehousesBackendApi | Extension-Only BackendAPI | Planned | PickingListsBackendApi, WarehousesBackendApi | (extension-only) |
| ProductAttributesBackendApi | BackendAPI | Planned | — | GET,POST /product-attributes&lt;br&gt;GET,PATCH /product-attributes/{id} |
| ProductImageSetsBackendApi | BackendAPI | Planned | — | GET /concrete-product-image-sets |
| ProductPackagingUnitsBackendApi | Extension-Only BackendAPI | Planned | PickingListsBackendApi | (extension-only) |
| ProductsBackendApi | BackendAPI | Planned | — | GET,POST /product-abstract&lt;br&gt;DELETE,GET,PATCH /product-abstract/{id} |
| PushNotificationsBackendApi | BackendAPI | Planned | — | GET,POST /push-notification-providers&lt;br&gt;PATCH,DELETE /push-notification-providers/{id}&lt;br&gt;POST /push-notification-subscriptions |
| SalesOrdersBackendApi | BackendAPI | Planned | — | GET /sales-orders |
| ServicePointsBackendApi | BackendAPI | Planned | — | GET,POST /service-points&lt;br&gt;GET,PATCH /service-points/{id}&lt;br&gt;GET,POST /service-point-addresses&lt;br&gt;PATCH /service-points/{id}/service-point-addresses/{id}&lt;br&gt;GET,POST /service-types&lt;br&gt;GET,PATCH /service-types/{id}&lt;br&gt;GET,POST /services&lt;br&gt;GET,PATCH /services/{id} |
| ShipmentTypesBackendApi | BackendAPI | Planned | — | GET,POST /shipment-types&lt;br&gt;GET,PATCH /shipment-types/{id} |
| ShipmentsBackendApi | BackendAPI | Planned | — | GET /sales-shipments |
| StoresBackendApi | BackendAPI | Planned | — | GET,POST,PATCH /stores |
| UsersBackendApi | BackendAPI | Planned | — | GET /users |
| WarehouseOauthBackendApi | BackendAPI | Planned | — | POST /warehouse-tokens |
| WarehouseUsersBackendApi | BackendAPI | Planned | — | GET,POST /warehouse-user-assignments&lt;br&gt;GET,PATCH,DELETE /warehouse-user-assignments/{id} |
| WarehousesBackendApi | BackendAPI | Planned | — | GET /warehouses |
</description>
            <pubDate>Tue, 02 Jun 2026 07:31:29 +0000</pubDate>
            <link>https://docs.spryker.com/docs/dg/dev/architecture/api-platform/migrate-to-api-platform-status.html</link>
            <guid isPermaLink="true">https://docs.spryker.com/docs/dg/dev/architecture/api-platform/migrate-to-api-platform-status.html</guid>
            
            
        </item>
        
        <item>
            <title>Search index deduplication</title>
            <description>## Overview

By default, Spryker indexes both `product_abstract` and `product_concrete` documents into the search index. The index is partitioned by store and locale, which causes a multiplicative increase in the number of indexed documents. Not all projects use the full capabilities of the product concrete search, which means this indexing can result in unnecessary index growth without benefits.

This guide describes how to remove `product_concrete` documents from OpenSearch or Elasticsearch to reduce the search index size and improve Publish and Sync (P&amp;S) performance.

## Full migration

Full migration is the safest option and restores equivalent behavior using key-value storage instead of the search index. All steps preserve the previous behavior while the transition is in progress — no downtime is required.

If you do not use all product concrete search features, you can skip steps that do not apply to your setup. See [Specific use cases](#specific-use-cases) to identify which steps are required for your project.

### Step 1: Install the required packages

```bash
composer update spryker/catalog:&quot;^5.13.0&quot; spryker/catalog-extension:&quot;^1.2.0&quot; spryker/product-page-search:&quot;^3.48.0&quot; spryker/product-storage:&quot;^1.52.0&quot; spryker-shop/configurable-bundle-page:&quot;^1.4.0&quot;
```

### Step 2: Enable product concrete search in storage

Add the following configuration to `config/Shared/config_default.php`:

```php
$config[ProductPageSearchConstants::PRODUCT_CONCRETE_SEARCH_IN_STORAGE_ENABLED] = true;
```

### Step 3: Update the catalog query expander plugins

In `src/Pyz/Client/Catalog/CatalogDependencyProvider.php`, add `ProductListSearchProductListQueryExpanderPlugin` to the `createCatalogSearchQueryExpanderPlugins()` method:

```php
protected function createCatalogSearchQueryExpanderPlugins(): array
{
    return [
        // existing plugins
        new ProductListSearchProductListQueryExpanderPlugin(),
    ];
}
```

### Step 4: Add the product concrete suggestion enricher plugins

In `src/Pyz/Client/Catalog/CatalogDependencyProvider.php`, add the following method:

```php
/**
 * @return array&lt;\Spryker\Client\CatalogExtension\Dependency\Plugin\ProductConcreteSuggestionEnricherPluginInterface&gt;
 */
protected function getProductConcreteSuggestionEnricherPlugins(): array
{
    return [
        new ProductConcreteSuggestionEnricherPlugin(),
    ];
}
```

### Step 5: Add the product concrete storage search plugins

In `src/Pyz/Client/Catalog/CatalogDependencyProvider.php`, add the following method:

```php
/**
 * @return array&lt;\Spryker\Client\CatalogExtension\Dependency\Plugin\ProductConcreteStorageSearchPluginInterface&gt;
 */
protected function getProductConcreteStorageSearchPlugins(): array
{
    return [
        new ProductConcreteStorageSearchPlugin(),
    ];
}
```

### Step 6: Optional: Enable the cleanup console command

This command is needed if your project already has some data in the product concrete search index, and you want to clean it up.

In `src/Pyz/Zed/Console/ConsoleDependencyProvider.php`, add `ProductConcretePageSearchCleanupConsole` to the `getConsoleCommands()` method:

```php
protected function getConsoleCommands(Container $container): array
{
    $commands = [
        // existing commands
        new ProductConcretePageSearchCleanupConsole(),
    ];

    return $commands;
}
```

At this point, the project is fully compatible with the previous behavior. Data still exists in the search index. After verifying that the behavior is correct, clean up the search index data.

### Step 7: Optional: Clean up search index data

```bash
vendor/bin/console product-concrete-page-search:cleanup [options]
```

| Option | Required | Default | Description |
|---|---|---|---|
| `--limit` | No | (all) | Maximum number of concrete products to process. Omit to process everything. |
| `--offset` | No | 0 | Starting position — skip this many products from the beginning. Useful for resuming an interrupted run. |

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

- The command displays a progress bar with time and memory estimates during execution.
- On completion, it prints a summary of the number of processed items and peak memory usage.
- The command is safe to re-run. Unpublishing an already-absent index entry is a no-op.

{% endinfo_block %}

## Specific use cases

The full migration is the safest option, but you can skip steps that are not relevant to your setup. In all cases, complete steps 1, 2, 6, and 7. The following sections describe which additional steps to apply for each feature.

### Quick order and quick add to cart

If your project uses the [Quick Add to Cart](/docs/pbc/all/cart-and-checkout/latest/base-shop/feature-overviews/quick-add-to-cart-feature-overview.html) feature, complete step 5.

### Catalog search suggestions via the Glue API

If you use the Glue API endpoint `{domain}/catalog-search-suggestions?q={sku}` and expect product concrete SKUs in the completion response, complete step 4.

Expected response after migration:

```json
{
    &quot;data&quot;: [
        {
            &quot;id&quot;: null,
            &quot;type&quot;: &quot;catalog-search-suggestions&quot;,
            &quot;attributes&quot;: {
                &quot;catalogSearchSuggestionId&quot;: &quot;catalog-search-suggestions&quot;,
                &quot;completion&quot;: [
                    &quot;{product_concrete_sku}&quot;
                ]
            }
        }
    ]
}
```

### Configurable Bundle with Product Lists

If you use the [Configurable Bundle](/docs/pbc/all/product-information-management/latest/base-shop/feature-overviews/configurable-bundle-feature-overview.html) feature together with [Product Lists](/docs/pbc/all/product-information-management/latest/base-shop/feature-overviews/product-lists-feature-overview.html) — for example, when using the `/en/configurable-bundle/configurator/template-selection` page — complete steps 3 and 4.

## Expected benefits

### Faster Publish and Sync and less index churn

Removing `product_concrete` documents from the search index produces the following results:

- Faster P&amp;S execution
- Fewer entities to export, transform, and bulk-index
- Less refresh and merge pressure on OpenSearch or Elasticsearch
- Less nested-document overhead, since each root document can spawn multiple Lucene nested documents

### Smaller OpenSearch footprint

Reducing the number of indexed documents can lower the following:

- Required EBS storage
- IOPS and throughput needs for writes and merges
- Heap pressure from segments and caches
- Operational risk during reindexing windows

## Related guides

- [Search performance guidelines](/docs/dg/dev/guidelines/performance-guidelines/search-performance-guidelines.html)
- [Performance guidelines](/docs/dg/dev/guidelines/performance-guidelines/performance-guidelines.html)
</description>
            <pubDate>Mon, 01 Jun 2026 08:40:07 +0000</pubDate>
            <link>https://docs.spryker.com/docs/dg/dev/guidelines/performance-guidelines/search-index-deduplication.html</link>
            <guid isPermaLink="true">https://docs.spryker.com/docs/dg/dev/guidelines/performance-guidelines/search-index-deduplication.html</guid>
            
            
        </item>
        
        <item>
            <title>Performance guidelines</title>
            <description>These performance guidelines originate from Spryker&apos;s years of experience across all kinds of projects, environments, and setups. They cover topics that are often missed at the project level, leading to poor performance or other related issues. The guidelines help you analyze and optimize performance of your website from different perspectives:

- [Keeping dependencies updated](/docs/dg/dev/guidelines/performance-guidelines/keeping-dependencies-updated.html) to maintain optimal performance and security by staying current with Spryker module updates.
- [Monitoring](/docs/dg/dev/guidelines/performance-guidelines/monitoring.html) to ensure effective application monitoring using APM tools.
- [General performance guidelines](/docs/dg/dev/guidelines/performance-guidelines/general-performance-guidelines.html) for general approaches to optimizing the server-side execution time.
- [Architecture performance guidelines](/docs/dg/dev/guidelines/performance-guidelines/architecture-performance-guidelines.html) to optimize performance in the very end servers.
- [Frontend performance guidelines](/docs/dg/dev/guidelines/performance-guidelines/front-end-performance-guidelines.html) to do the frontend-specific optimization.
- [Twig performance best practices](/docs/dg/dev/guidelines/performance-guidelines/twig-performance-best-practices.html) to optimize Twig templating engine performance.
- [Session locks](/docs/dg/dev/guidelines/performance-guidelines/session-locks.html) to optimize session locking mechanisms and reduce performance impact. Also see [Redis session lock](/docs/dg/dev/troubleshooting/troubleshooting-performance-issues/redis-session-lock.html) for troubleshooting session lock issues.
- [Bot control](/docs/dg/dev/guidelines/performance-guidelines/bot-control.html) to manage honest and malicious bot traffic effectively.
- [Batch processing of Propel entities](/docs/dg/dev/guidelines/performance-guidelines/performance-guidelines-batch-processing-propel-entities.html) for efficient batch processing, reduced database load, and support for complex entity relationships.
- [Database performance guidelines](/docs/dg/dev/guidelines/performance-guidelines/database-performance-guidelines.html) to optimize database operations through proper indexing, query optimization, and avoiding common database anti-patterns.
- [Key-Value storage performance guidelines](/docs/dg/dev/guidelines/performance-guidelines/key-value-storage-performance-guidelines.html) to optimize Redis/ValKey usage by limiting operations, avoiding admin commands in runtime, and implementing proper caching strategies.
- [External HTTP requests](/docs/dg/dev/guidelines/performance-guidelines/external-http-requests.html) to understand Spryker&apos;s architecture principle of reading from fast storage and learn how to manage external HTTP requests when they&apos;re necessary.
- [Search performance guidelines](/docs/dg/dev/guidelines/performance-guidelines/search-performance-guidelines.html) to optimize Elasticsearch and OpenSearch performance, avoid common search anti-patterns, and implement efficient search queries.
- [Infrastructure and worker configuration guidelines](/docs/dg/dev/guidelines/performance-guidelines/infrastructure-worker-configuration-guidelines.html) to optimize nginx configuration and worker orchestration for multi-store setups.
- [CDN and traffic management integration](/docs/dg/dev/guidelines/performance-guidelines/cdn-and-traffic-management-integration.html) to configure CDN solutions like Akamai or Cloudflare to work correctly with Spryker&apos;s nginx compression and avoid unnecessary data transfer overhead.
- [Split publish queues for performance](/docs/dg/dev/guidelines/performance-guidelines/split-queues-performance.html) to migrate from a single generic publish queue to dedicated per-module queues for improved throughput, fault isolation, and resource utilization.
- [Custom code performance guidelines](/docs/dg/dev/guidelines/performance-guidelines/custom-code-performance-guidelines.html) to implement performant custom code including caching strategies, background processing, and Quote calculator optimization.
- [Common pitfalls in OMS design](/docs/pbc/all/order-management-system/latest/base-shop/datapayload-conversion/state-machine/common-pitfalls-in-oms-design.html) to avoid performance bottlenecks in Order Management System processes, especially ensuring heavy operations run in CLI context rather than during web requests.
- [Order management system multi-thread](/docs/pbc/all/order-management-system/latest/base-shop/datapayload-conversion/state-machine/order-management-system-multi-thread.html) to process OMS timeouts and conditions in parallel for improved order processing performance.
- [Troubleshooting performance issues](/docs/dg/dev/troubleshooting/troubleshooting-performance-issues/troubleshooting-performance-issues.html) for detecting and fixing common performance problems.
- [Order details page performance guidance](/docs/pbc/all/order-management-system/latest/base-shop/order-management-feature-overview/order-details-page-performance-overview.html) to optimize the order detail page in the Back Office
- [KV storage deduplication](/docs/dg/dev/guidelines/performance-guidelines/kv-storage-deduplication.html) to reduce Redis/ValKey memory usage by eliminating duplicated URL and product abstract data.
- [Search index deduplication](/docs/dg/dev/guidelines/performance-guidelines/search-index-deduplication.html) to reduce search index size and improve Publish and Sync performance by removing product concrete documents from OpenSearch and Elasticsearch.
</description>
            <pubDate>Mon, 01 Jun 2026 08:40:07 +0000</pubDate>
            <link>https://docs.spryker.com/docs/dg/dev/guidelines/performance-guidelines/performance-guidelines.html</link>
            <guid isPermaLink="true">https://docs.spryker.com/docs/dg/dev/guidelines/performance-guidelines/performance-guidelines.html</guid>
            
            
        </item>
        
        <item>
            <title>PunchOut Protocols Coverage</title>
            <description>PunchOut flow protocols cXML and OCI contain a big range of features, which also consist of different functional elements, XML and form fields.

Find out below which parts are supported out of the box and plan extension of the missing ones on the project accordingly.

## cXML Flow fields mapping

This section lists the cXML elements that the PunchOut Gateway interprets on the inbound `PunchOutSetupRequest` and emits on the outbound `PunchOutSetupResponse` and `PunchOutOrderMessage`. Elements that are not listed are not parsed by the default flow.

### Received elements (buyer to Spryker)

The buyer&apos;s eProcurement system posts a `PunchOutSetupRequest` cXML document to the connection&apos;s `request_url`. The raw XML body is parsed by `DefaultCxmlContentParser` into `PunchoutCxmlSetupRequestTransfer`.

#### cXML Header

| Element | Required | Purpose                                                                                                       |
| --- | --- |---------------------------------------------------------------------------------------------------------------|
| `cXML/@payloadID` | Yes | Stored as `payloadId`. Echoed in logs.                                                                        |
| `cXML/@timestamp` | Yes | Stored as `timestamp` in ISO 8601 format.                                                                     |
| `Header/From/Credential/Identity` | Yes | Stored as `fromIdentity` and `buyerIdentity`. Used as `To` in the cart return message.                        |
| `Header/To/Credential/Identity` | Yes | Stored as `toIdentity`. Used as `From` and `Sender` in the cart return message.                               |
| `Header/Sender/Credential/Identity` | Yes | Stored as `senderIdentity`. Used for connection identification, lookup .                                      |
| `Header/Sender/Credential/SharedSecret` | Yes | Stored as `senderSharedSecret`. Verified by `PunchoutCxmlAuthenticator` against the connection&apos;s stored hash. |

#### cXML Payload

| Element | Required | Purpose |
| --- | --- | --- |
| `PunchOutSetupRequest/@operation` | Yes | Either `create` or `edit`. For `edit` the existing items are parsed and used to find the matching session quote. |
| `PunchOutSetupRequest/BuyerCookie` | Yes | Stored as `buyerCookie`. Echoed verbatim in the cart return message. |
| `PunchOutSetupRequest/BrowserFormPost/URL` | Yes | Stored as `browserFormPostUrl`. The cart return message is POSTed to this URL. |
| `PunchOutSetupRequest/Extrinsic` | No | All `Extrinsic` children are collected into `extrinsicFields` as a key/value map for project-level customization. |

#### Ship-to address

When `PunchOutSetupRequest/ShipTo/Address` is present, the following are mapped to `PunchoutAddressTransfer`:

| Element | Maps to |
| --- | --- |
| `Address/Name` | `addressName` |
| `Address/PostalAddress/Street` | `streetLines[]` (multiple lines supported) |
| `Address/PostalAddress/City` | `city` |
| `Address/PostalAddress/State` | `state` |
| `Address/PostalAddress/PostalCode` | `postalCode` |
| `Address/PostalAddress/Country` | `country` (text) |
| `Address/PostalAddress/Country/@isoCountryCode` | `countryCode` |

#### Item list (only when `operation=&quot;edit&quot;`)

For each `ItemOut`, a `PunchoutItemTransfer` is built:

| Element | Maps to |
| --- | --- |
| `ItemOut/@lineNumber` | `lineNumber` |
| `ItemOut/@quantity` | `quantity` |
| `ItemOut/ItemID/SupplierPartID` | `supplierPartId` |
| `ItemOut/ItemID/SupplierPartAuxiliaryID` | `supplierPartAuxiliaryId` |
| `ItemOut/ItemDetail/Description` | `description` |
| `ItemOut/ItemDetail/UnitOfMeasure` | `unitOfMeasure` |
| `ItemOut/ItemDetail/UnitPrice/Money` | `unitPrice` |
| `ItemOut/ItemDetail/UnitPrice/Money/@currency` | `currency` |
| `ItemOut/ItemDetail/Classification` (first) | `classification` |
| `ItemOut/ItemDetail/ManufacturerPartID` | `manufacturerPartId` |
| `ItemOut/ItemDetail/ManufacturerName` | `manufacturerName` |

### Returned elements (Spryker to buyer)

The PunchOut Gateway emits two outbound cXML documents.

#### `PunchOutSetupResponse` — synchronous reply to `PunchOutSetupRequest`

Returned with HTTP `200 OK` and `Content-Type: text/xml`. Built by `CxmlResponseBuilder`.

| Element | Source | Notes |
| --- | --- | --- |
| `Response/PunchOutSetupResponse/StartPage/URL` | `PunchoutSetupResponseTransfer.startPageUrl` | Absolute URL the buyer&apos;s browser opens to launch the shopping session. Includes the session token query parameter. |

On authentication, validation, or processing errors a cXML `Status` document is returned instead, with a non-`200` status code:

| Element | Source |
| --- | --- |
| `Response/Status/@code` | `statusCode` |
| `Response/Status/@text` | `statusText` |
| `Response/Status` (text) | `errorMessage` |

#### `PunchOutOrderMessage` — cart return to the buyer

Built by `CxmlPunchoutOrderMessageMapper` and POSTed to `BrowserFormPost.URL` from the original setup request when the buyer transfers the cart back.

Header:

| Element | Source | Notes |
| --- | --- | --- |
| `cXML/@xml:lang` | Constant `en-US` | `PunchoutGatewayConfig::DEFAULT_CXML_LANGUAGE`. |
| `Header/From/Credential` | `toIdentity` from setup request | Domain fixed to `DUNS`. |
| `Header/To/Credential` | `fromIdentity` from setup request | Domain fixed to `DUNS`. |
| `Header/Sender/Credential` | `toIdentity` + stored `senderSharedSecret` | Domain fixed to `DUNS`. |
| `cXML/@payloadID`, `cXML/@timestamp` | Generated | Set by the `cxml-lib` builder. |

Message payload:

| Element | Source                                                             | Notes                                                                                                                 |
| --- |--------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------|
| `PunchOutOrderMessage/BuyerCookie` | `PunchoutSessionTransfer.buyerCookie`                              | Echoed from the setup request.                                                                                        |
| `PunchOutOrderMessageHeader/@operationAllowed` | `PunchoutSessionTransfer.operation`                                | Either `create` or `edit`, the value is taken form the Setup request.                                                 |
| `PunchOutOrderMessageHeader/Total/Money/@currency` | Currency of the cart, taken from `QuoteTransfer.currency.code`     |                                                                                                                       |
| `ItemIn/ItemID/SupplierPartID` (per item) | Product concrete SKU, taken from `ItemTransfer.sku`                |                                                                                                                       |
| `ItemIn/@quantity` (per item) | Quantity of this item, taken from`ItemTransfer.quantity`           |                                                                                                                       |
| `ItemIn/ItemDetail/Description` (per item) | Product name, taken from`ItemTransfer.name`                        |                                                                                                                       |
| `ItemIn/ItemDetail/UnitOfMeasure` (per item) | Constant `EA`                                                      | `PunchoutGatewayConfig::DEFAULT_UNIT_OF_MEASURE`.                                                                     |
| `ItemIn/ItemDetail/UnitPrice/Money` (per item) | Product price, taken from `ItemTransfer.unitPrice`                 |                                                                                                                       |
| `ItemIn/ItemDetail/Classification@domain` (per item) | Constant `UNSPSC`                                                  |                                                                                                                       |
| `PunchOutOrderMessageHeader/ShipTo/Address/Name` | `QuoteTransfer.shippingAddress.firstName + lastName`               | Falls back to `Ship To` when both are empty.                                                                          |
| `PunchOutOrderMessageHeader/ShipTo/Address/PostalAddress/Street` | `address1`, `address2`, `address3`                                 | Empty lines are skipped.                                                                                              |
| `PunchOutOrderMessageHeader/ShipTo/Address/PostalAddress/City` | `QuoteTransfer.shippingAddress.city`                               |                                                                                                                       |
| `PunchOutOrderMessageHeader/ShipTo/Address/PostalAddress/State` | `QuoteTransfer.shippingAddress.region`, with fallback to `QuoteTransfer.shippingAddress.state` |                                                                                                                       |
| `PunchOutOrderMessageHeader/ShipTo/Address/PostalAddress/PostalCode` | `QuoteTransfer.shippingAddress.zipCode`                            |                                                                                                                       |
| `PunchOutOrderMessageHeader/ShipTo/Address/PostalAddress/Country/@isoCountryCode` | `QuoteTransfer.shippingAddress.iso2Code`                           |                                                                                                                       |
| `PunchOutOrderMessageHeader/Shipping/Money` + `Description` | `QuoteTransfer.totals.expenseTotal`                                | Description fixed to `Shipping`. Omitted when no expense total is set.                                                |
| `PunchOutOrderMessageHeader/Tax/Money` + `Description` | `QuoteTransfer.totals.taxTotal.amount`                             | Description fixed to `Tax`. Omitted when no tax total is set.                                                         |
| `PunchOutOrderMessageHeader/Extrinsic` (per field) | `extrinsicFields` from the original `PunchOutSetupRequest`         | Echoed back inside `PunchOutOrderMessageHeader`, with the keys in `PunchoutGatewayConfig::EXTRINSIC_BLACKLIST` removed. See [Extrinsic blacklist](#extrinsic-blacklist) below. |

To extend or override the field set, replace the parser or message-mapper services, or set a custom cXML processor plugin FQCN on the connection&apos;s `processor_plugin_class` column. Processor plugins are loaded at runtime by class name; no dependency-provider registration is required. For details, see [Project configuration for PunchOut Gateway](/docs/pbc/all/punchout-gateway/project-configuration-for-punchout-gateway.html).

### Extrinsic blacklist

`CxmlPunchoutOrderMessageMapper::filterExtrinsics()` removes any extrinsic whose key matches `PunchoutGatewayConfig::EXTRINSIC_BLACKLIST` before echoing the remaining values back inside each `PunchOutOrderMessageHeader`. The blacklist guards against leaking personally identifiable information that the buyer&apos;s procurement system sent for customer resolution. The default list is:

`User`, `UniqueUsername`, `UniqueName`, `UserId`, `UserEmail`, `UserFullName`, `UserPrintableName`, `FirstName`, `LastName`, `PhoneNumber`, `UserPhoneNumber`.

## OCI Flow fields mapping

This section lists the OCI form fields that the PunchOut Gateway interprets on the inbound login and emits on the outbound cart return.
Any inbound fields that are not listed are preserved in `PunchoutOciLoginRequestTransfer.formData` for a potential project level customisation.

### Received fields (buyer to Spryker)

The buyer&apos;s eProcurement system posts an HTML form to the connection&apos;s `request_url`. The default OCI flow reads the following fields:

| Field | Required | Purpose |
| --- | --- | --- |
| `USERNAME` | Yes | Identifies the buyer user. The field name is configurable per connection through `usernameField`; `USERNAME` is the default. Matched against `spy_punchout_credential.username`. |
| `PASSWORD` | Yes | Authenticates the buyer user. The field name is configurable per connection through `passwordField`; `PASSWORD` is the default. Verified against the stored password hash. |
| `HOOK_URL` | Yes | Target URL the cart return form is posted to at checkout. Must start with `https://`. Stored on the session as `browserFormPostUrl`. |
| `~TARGET` | No | Frame target echoed back to the buyer. |
| `~OkCode` | No | SAP control field. |
| `~CALLER` | No | SAP control field. |

### Returned fields (Spryker to buyer)

When the Spryker Shop transfers the cart back, HTML form with a button `Transfer Cart` is rendered, whose `action` is the `HOOK_URL` received at login.
`~TARGET` rendered as the `target` attribute of the form.

The form contains following fields:

| Field | Source | Notes                                                                                                          |
| --- | --- |----------------------------------------------------------------------------------------------------------------|
| `NEW_ITEM-DESCRIPTION[N]` | `ItemTransfer.name` | Item name.                                                                                                     |
| `NEW_ITEM-QUANTITY[N]` | `ItemTransfer.quantity` | Quantity as integer string.                                                                                    |
| `NEW_ITEM-UNIT[N]` | Constant `EA` | Unit of measure. Fixed to `EA` (see `PunchoutGatewayConfig::DEFAULT_UNIT_OF_MEASURE`).                         |
| `NEW_ITEM-PRICE[N]` | `ItemTransfer.unitPrice` | Unit price. Converted from cents using the currency&apos;s fraction digits and formatted with three decimal places. |
| `NEW_ITEM-CURRENCY[N]` | `QuoteTransfer.currency.code` | ISO currency code of the quote.                                                                                |
| `NEW_ITEM-VENDORMAT[N]` | `ItemTransfer.sku` | Vendor material number.                                                                                        |
| `~OkCode` | Echoed from login | Echoed only when present in the original login.                                                                |
| `~CALLER` | Echoed from login | Echoed only when present in the original login.                                                                |

To extend or override the field set, replace the form-builder service or set a custom OCI processor plugin FQCN on the connection&apos;s `processor_plugin_class` column. Processor plugins are loaded at runtime by class name; no dependency-provider registration is required. For details, see [Project configuration for PunchOut Gateway](/docs/pbc/all/punchout-gateway/project-configuration-for-punchout-gateway.html).
</description>
            <pubDate>Mon, 01 Jun 2026 07:03:08 +0000</pubDate>
            <link>https://docs.spryker.com/docs/pbc/all/punchout-gateway/punchout-protocol-coverage.html</link>
            <guid isPermaLink="true">https://docs.spryker.com/docs/pbc/all/punchout-gateway/punchout-protocol-coverage.html</guid>
            
            
        </item>
        
        <item>
            <title>PunchOut Gateway</title>
            <description>{% info_block infoBox &quot;Disclaimer&quot; %}

This module is part of the [Spryker Early Access program](/docs/about/all/releases/early-access-program.html). Functionality may be incomplete and is subject to change.

We welcome your feedback.

{% endinfo_block %}

The PunchOut Gateway module provides a basic implementation of OCI and cXML PunchOut flows for Spryker shops.

## Supported use cases

The current implementation supports any number of simultaneously active OCI and cXML connections in a single Spryker shop.

Support for the shop integration to iFrame can only be enabled globally for the whole shop, following [this guideline](/docs/pbc/all/punchout-gateway/integrate-punchout-gateway.html#support-iframe-embedding).  

For OCI flow, the cart named `Punchout` is created for every new request.
Cart cleanup functionality will be implemented in the following releases.

For cXML flow, a single cart is created or reused for each `BuyerCookie` value. 



### Additional links

[Integration guide](/docs/pbc/all/punchout-gateway/integrate-punchout-gateway.html) explains how to enable cXML and OCI PunchOut flows in your Spryker shop.

[Project Configuration](/docs/pbc/all/punchout-gateway/project-configuration-for-punchout-gateway.html) contains details about fine-tuning the integration on the project level.

</description>
            <pubDate>Mon, 01 Jun 2026 07:03:08 +0000</pubDate>
            <link>https://docs.spryker.com/docs/pbc/all/punchout-gateway/punchout-gateway.html</link>
            <guid isPermaLink="true">https://docs.spryker.com/docs/pbc/all/punchout-gateway/punchout-gateway.html</guid>
            
            
        </item>
        
        <item>
            <title>Project configuration for PunchOut Gateway</title>
            <description>This document describes the project configuration to enable eProcurement systems support via PunchOut flow.

## PunchOut connection configuration

Configure connections through the Back Office UI. For details, see [Manage PunchOut connections](/docs/pbc/all/punchout-gateway/manage-punchout-connections.html).

To bootstrap a working configuration for local development, the module also provides two demo console commands. Each command creates a single, hardcoded connection for store `DE`. Do not use the demo data in production.

- OCI flow:

```bash
vendor/bin/console punchout-gateway:oci:demo-connection:create
```

- cXML flow:

```bash
vendor/bin/console punchout-gateway:cxml:demo-connection:create
```

The rest of this section describes the persisted shape of a connection. The same fields are exposed in the Back Office form.

To configure a connection manually, create a row in the `spy_punchout_connection` table:

| Column | Value                                    | Comments                                                                                                                                                                                                                   |
|--------|------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `fk_store` | Store ID (for example, 1)                | ID of the store the customer must be logged in to.                                                                                                                                                                         |
| `name` | Human-readable label                     | Used only for readability                                                                                                                                                                                                  |
| `is_active` | `true`                                   | Determines whether the connection can be used.                                                                                                                                                                             |
| `allow_iframe` | `true` / `false`                         | Enforces iframe-specific headers when the PunchOut session is active. If **~TARGET** is sent during the request, the headers are sent regardless of this value. Make sure to adjust [configuration for session cookies](/docs/pbc/all/punchout-gateway/integrate-punchout-gateway.html#support-iframe-embedding). |
| `protocol_type` | `&apos;oci&apos;` or `&apos;cxml&apos;`                      | Flow type.                                                                                                                                                                                                                 |
| `processor_plugin_class` | Full class name of the processor plugin. | Processor to be used.                                                                                                                                                                                                      |

### OCI connection configuration

OCI-specific configuration

| Column | Value                                    | Comments                                                                                                                                                                                                          |
|--------|------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `request_url` | `&apos;/punchout-gateway/oci/my-company&apos;`     | Endpoint path the buyer posts the OCI login form to. This URL without a domain is the unique identifier of each connection, and can be anything that starts with ``https://&lt;shop-domain&gt;/punchout-gateway/oci/``. |
| `configuration` | JSON configuration                       | See the *OCI Login configuration* section below.                                                                                                                                                                   |
| `protocol_type` | `&apos;oci&apos;`                       | Flow type.                                                                                                                                               |
| `processor_plugin_class` | Full class name of the processor plugin. | `\SprykerEco\Zed\PunchoutGateway\Communication\Plugin\PunchoutGateway\DefaultOciProcessorPlugin` or a project&apos;s implementation.                                                                                   |

The triplet `protocol_type`, `fk_store` and `request_url` must be unique.

Column `configuration` contains JSON with the following optional keys. Override only when the value differs from the default.

| Key | Default | Purpose                                                     |
|-----|---------|-------------------------------------------------------------|
| `usernameField` | `USERNAME` | Form field name carrying the username during login request. |
| `passwordField` | `PASSWORD` | Form field name carrying the password during login request. |
| `formMethod`    | `POST`     | HTTP method the buyer uses to post the OCI login form (`POST` or `GET`). |

Additionally, configure credentials for customers who will access the shop.
To do this, create rows in the `spy_punchout_credential` table:

| Column                 | Value           | Comment                                                                                                                                                       |
|------------------------|-----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `fk_punchout_connection` | integer         | ID of the connection                                                                                                                                          |
| `fk_customer`            | int             | ID of the customer                                                                                                                                            |
| `username`               | string          | Username, that&apos;s sent in the `username` field                                                                                                                 |
| `password_hash`          | hashed password | Hashed password, that&apos;s sent in the `password` field. Validation happens using [password_verify](https://www.php.net/manual/en/function.password-verify.php). |
| `is_active`              | `true`/`false`  | Active flag of this customer                                                                                                                                  |

Customers used for an OCI connection are expected to be fully configured in the shop so that only permitted products and prices are accessible.

### cXML connection configuration

cXML-specific configuration columns:

| Column | Value                                    | Comments                                                                                                                         |
|--------|------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------|
| `sender_identity` | `Sender Identity`                        | Sender identity serves as an identifier for the connection. Unique within the database.                                  |
| `configuration` | JSON configuration                       | See the *configuration* section below.                                                                                      |
| `protocol_type` | `&apos;cxml&apos;`                                 | Flow type.                                                                                                                       |
| `processor_plugin_class` | Full class name of the processor plugin. | `\SprykerEco\Zed\PunchoutGateway\Communication\Plugin\PunchoutGateway\DefaultCxmlProcessorPlugin` or a project&apos;s implementation. |

The `sender_identity` column value must be globally unique—each cXML system maps to exactly one connection. The sender identity is stored as a top-level column on `spy_punchout_connection`, not inside the JSON `configuration` blob.

Column `configuration` contains JSON with the following keys:

| Key | Required | Purpose                                          |
|-----|----------|--------------------------------------------------|
| `senderSharedSecret` | yes | Shared secret used to authenticate the request. The stored value is the hash, generated with `password_hash()`. The incoming `SharedSecret` is checked against this hash with [password_verify](https://www.php.net/manual/en/function.password-verify.php). |

For a cXML connection, no additional PunchOut-related configuration is required.
The logged-in customer is identified by the `UserEmail` extrinsic field.
Customers used for a cXML connection are expected to be fully configured in the shop so that only permitted products and prices are accessible.


## PunchOut flow processor plugin

Each PunchOut connection resolves its processor plugin at runtime using the fully qualified class name stored in `spy_punchout_connection.processor_plugin_class`.
The plugin must implement one of the following interfaces:
- for OCI flow - `\SprykerEco\Zed\PunchoutGateway\Dependency\Plugin\PunchoutProcessorPluginInterface`
- for cXML flow - `\SprykerEco\Zed\PunchoutGateway\Dependency\Plugin\PunchoutCxmlProcessorPluginInterface`.

This module provides default functionality:
- \SprykerEco\Zed\PunchoutGateway\Communication\Plugin\PunchoutGateway\DefaultCxmlProcessorPlugin - for cXML flow,
- \SprykerEco\Zed\PunchoutGateway\Communication\Plugin\PunchoutGateway\DefaultOciProcessorPlugin - for OCI flow.

No dependency injection registration is required. The plugin is loaded at runtime.

### Creation of a custom plugin

Place the plugin in your project&apos;s Zed communication layer, for example:

**src/Pyz/Zed/ProjectPunchoutGateway/Communication/Plugin/PunchoutGateway/CustomOciProcessorPlugin.php**

The simplest approach is to extend the default OCI plugin and override only the methods you need:

```php
namespace Pyz\Zed\ProjectPunchoutGateway\Communication\Plugin\PunchoutGateway;

use SprykerEco\Zed\PunchoutGateway\Communication\Plugin\PunchoutGateway\DefaultOciProcessorPlugin;

class CustomOciProcessorPlugin extends DefaultOciProcessorPlugin
{
    // Override only the methods you need to customize.
}
```

Set `spy_punchout_connection.processor_plugin_class` on the connection that uses this plugin to `\Pyz\Zed\ProjectPunchoutGateway\Communication\Plugin\PunchoutGateway\CustomOciProcessorPlugin`.

### Global plugin methods

| Method | Called when                                                  | Functionality                                                                                                     |
|--------|--------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------|
| `getType` | At plugin resolution                                         | Reports the connection protocol type this plugin handles (`oci` or `cxml`). Used by `ProcessorPluginResolver` to validate that the FQCN stored on a connection matches the connection&apos;s `protocol_type`. |
| `authenticate` | First step of the login flow                                 | Finds a connection based on the setup request. Returns `null` if no valid connection is found.                    |
| `resolveCustomer` | After a valid connection was found                           | Finds the customer to use for the PunchOut session. Returns `null` if no valid customer is found.                 |
| `resolveQuote` | After a valid customer is resolved                           | Creates a new quote or reuses an existing one. An empty `QuoteTransfer` can be returned.                          |
| `expandQuote` | After the quote is resolved                                  | Allows adjusting the quote after PunchOut-specific preparations are done.                                         |
| `resolveSession` | After the quote is expanded, before the session is persisted | Additional session validation logic can be added here.                                                            |

#### cXML-specific plugin methods

| Method | Called when                                      | Functionality                                                                                           |
|--------|--------------------------------------------------|---------------------------------------------------------------------------------------------------------|
| `parseCxmlRequest` | XML was parsed and a valid connection was found. | Additional mapping of the cXML data onto setup request.                                                 |
| `expandResponse` | After successful session creation                | Response to the login request can be expanded, for example, with the default start URL for the customer. |


## Form handler plugin

Projects extend or replace the storefront &quot;Transfer Cart&quot; form via plugins implementing `\SprykerEco\Yves\PunchoutGateway\Plugin\Form\PunchoutFormHandlerPluginInterface`.
At render time, `PunchoutFormDataBuilder::build()` iterates the registered handlers in order and returns the result of the first whose `isApplicable()` returns `true`.

The module ships two defaults, both registered in `\SprykerEco\Yves\PunchoutGateway\PunchoutGatewayDependencyProvider::getPunchoutFormHandlerPlugins()`:

- `\SprykerEco\Yves\PunchoutGateway\Plugin\Form\DefaultCxmlPunchoutFormHandlerPlugin` — for cXML sessions.
- `\SprykerEco\Yves\PunchoutGateway\Plugin\Form\DefaultOciPunchoutFormHandlerPlugin` — for OCI sessions.

### Plugin methods

| Method | Called when                                                              | Functionality                                                                                                                                                                    |
|--------|--------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `isApplicable` | Before each render of the widget                                         | Returns `true` when the quote&apos;s PunchOut session matches this handler&apos;s protocol.                                                                                                |
| `handle` | After `isApplicable()` returned `true`                                   | Builds the `PunchoutFormDataTransfer` carrying the action URL and all hidden form fields, or returns `null` when the form cannot be built (for example, missing session data). |

### Register a custom handler

Place the plugin in your project&apos;s Yves layer, for example:

**src/Pyz/Yves/ProjectPunchoutGateway/Plugin/Form/CustomCxmlPunchoutFormHandlerPlugin.php**

Then register it before the defaults so it takes precedence for the same protocol:

**src/Pyz/Yves/PunchoutGateway/PunchoutGatewayDependencyProvider.php**

```php
namespace Pyz\Yves\PunchoutGateway;

use Pyz\Yves\ProjectPunchoutGateway\Plugin\Form\CustomCxmlPunchoutFormHandlerPlugin;
use SprykerEco\Yves\PunchoutGateway\PunchoutGatewayDependencyProvider as SprykerEcoPunchoutGatewayDependencyProvider;

class PunchoutGatewayDependencyProvider extends SprykerEcoPunchoutGatewayDependencyProvider
{
    protected function getPunchoutFormHandlerPlugins(): array
    {
        return [
            new CustomCxmlPunchoutFormHandlerPlugin(),
            ...parent::getPunchoutFormHandlerPlugins(),
        ];
    }
}
```


## Punchout-specific security header expander plugin

At PunchOut session start, Yves applies protocol-specific `Content-Security-Policy` (CSP) directives to allow the Storefront to be embedded and to post back to the buyer&apos;s procurement system.

Directive generation is delegated to plugins implementing `\SprykerEco\Yves\PunchoutGateway\Dependency\Plugin\PunchoutSecurityHeaderExpanderPluginInterface`.
`PunchoutSecurityHeaderSessionWriter` walks the registered plugins and accumulates their directives for the active session once, persisting the value into the session.

When `spy_punchout_connection.allow_iframe` is `true`, the correct CSP headers are included in each response.

By default, we provide an OCI-specific plugin only:

- `\SprykerEco\Yves\PunchoutGateway\Plugin\SecurityHeader\DefaultOciSecurityHeaderExpanderPlugin` — adds `frame-ancestors` to the CSP for OCI sessions when the OCI login carries a `~TARGET` form field.

### Plugin method

| Method | Functionality                                                                                                         |
|--------|-----------------------------------------------------------------------------------------------------------------------|
| `expand` | Appends protocol-specific CSP directive strings to the given list. Implementations must not add duplicate directives. |

### Register a custom expander

In case your system requires additional Punchout-specific security headers, add your plugin to the Yves dependency provider. 

**src/Pyz/Yves/PunchoutGateway/PunchoutGatewayDependencyProvider.php**

```php
namespace Pyz\Yves\PunchoutGateway;

use Pyz\Yves\ProjectPunchoutGateway\Plugin\SecurityHeader\CustomCxmlSecurityHeaderExpanderPlugin;
use SprykerEco\Yves\PunchoutGateway\PunchoutGatewayDependencyProvider as SprykerEcoPunchoutGatewayDependencyProvider;

class PunchoutGatewayDependencyProvider extends SprykerEcoPunchoutGatewayDependencyProvider
{
    protected function getPunchoutSecurityHeaderExpanderPlugins(): array
    {
        return [
            ...parent::getPunchoutSecurityHeaderExpanderPlugins(),
            new CustomCxmlSecurityHeaderExpanderPlugin(),
        ];
    }
}
```


## Session-in-quote expander plugin

`PunchoutQuoteExpander` (Zed) loads the PunchOut session that belongs to the current quote and runs it through every registered `\SprykerEco\Zed\PunchoutGateway\Dependency\Plugin\PunchoutSessionInQuoteExpanderPluginInterface` before stamping it on the `QuoteTransfer`.
Use this extension point to enrich or override PunchOut session fields based on the quote (for example, to copy a project-specific field onto the session before it is persisted with the cart).

### Plugin methods

| Method | Called when                                               | Functionality                                                                                           |
|--------|-----------------------------------------------------------|---------------------------------------------------------------------------------------------------------|
| `isApplicable` | For each plugin, before `expand()` runs                   | Returns `true` when the plugin should run for the given session/quote combination.                      |
| `expand` | After `isApplicable()` returned `true`                    | Expands the `PunchoutSessionTransfer` before it is assigned to the `QuoteTransfer`.                     |

## Default implementations

This section describes the behavior of the two shipped default processor plugins and the storefront &quot;Transfer Cart&quot; widget for each lifecycle step.
Use it to understand what you get out-of-the-box and to identify which plugin method to override when customising.

### Shared behavior

Both `DefaultOciProcessorPlugin` and `DefaultCxmlProcessorPlugin` rely on `QuoteCreator` to stamp the following fields on every new quote:

- **Store** — resolved from `spy_punchout_connection.fk_store`.
- **Currency** — set to the default currency of the resolved store.

### OCI

**Customer identification**

`OciCustomerResolver` looks up the customer by the `idCustomer` that is already on the connection transfer.
That value is stamped during authentication: `PunchoutOciAuthenticator` matches the username and password from the OCI form fields against `spy_punchout_credential` and, on success, writes `connection.idCustomer`.
The buyer identity therefore comes entirely from the credential record — nothing from the buyer&apos;s login payload is used.
Override `resolveCustomer` on the processor plugin to source the customer differently (for example, from a custom form field).

**Quote identification**

`OciPunchoutQuoteFinder` always returns a fresh empty `QuoteTransfer` with `DEFAULT_QUOTE_NAME`.
There is no session-to-quote lookup on OCI login; every login starts a new cart.
Store and currency are set by `QuoteCreator` as described in the shared behavior section above.
Override `resolveQuote` to reuse a per-customer cart across sessions.

**Quote fill with items**

OCI login does not carry item data.
`DefaultOciProcessorPlugin::expandQuote` is a pass-through and leaves the quote empty.
Items are transferred back to the buyer&apos;s procurement system via the storefront form POST (see &quot;Transfer Cart button&quot; below), not populated during login.

**Transfer Cart button**

`DefaultOciPunchoutFormHandlerPlugin` (registered in `\SprykerEco\Yves\PunchoutGateway\PunchoutGatewayDependencyProvider`) makes the button visible when the current quote has a PunchOut session whose `punchoutData.ociLoginRequest` is not `null`.
When applicable, the action URL is taken from the `HOOK_URL` form field of the OCI login request (validated to start with `https://` at session creation time).
The hidden form fields are OCI-flat `NEW_ITEM-…` key/value pairs produced by `OciFormFieldBuilder`.
The button is not rendered when no punchout session exists on the quote, when the session was created by a cXML login, or when no handler plugin is registered for the Yves factory.

### cXML

**Customer identification**

`CxmlCustomerResolver` reads the `Extrinsic[name=&quot;UserEmail&quot;]` field from the parsed `PunchOutSetupRequest` and resolves the customer via `CustomerFacade::getCustomer` by email.
Connection authentication is a separate step that runs first: `PunchoutCxmlAuthenticator` verifies the `senderSharedSecret` from the cXML header against the connection record using `password_verify`.
If the `UserEmail` extrinsic is absent or empty, `resolveCustomer` returns `null` and the session is rejected.
Override `resolveCustomer` to read identity from a different extrinsic or from a custom header field.

**Quote identification**

`CxmlPunchoutQuoteFinder` uses `BuyerCookie` from the `PunchOutSetupRequest` to look up an existing PunchOut session via `PunchoutGatewayRepository::findPunchoutSessionByBuyerCookie`.
When a session is found, the linked quote is reused — allowing the buyer to resume an in-progress cart.
If the found quote belongs to a different store than the current connection&apos;s `idStore`, the old quote is deleted and a new empty one is created.
When `BuyerCookie` is missing or no session matches, a new `DEFAULT_QUOTE_NAME` quote is returned.
Store and currency are then set by `QuoteCreator`.
Override `resolveQuote` to change cookie-matching rules or to support cross-store quote reuse.
Quote is always presented as an empty one for a customer after session start.

**Transfer Cart button**

`DefaultCxmlPunchoutFormHandlerPlugin` makes the button visible when the current quote has a PunchOut session whose `punchoutData.cxmlSetupRequest` is not `null`.
The action URL is `punchoutSession.browserFormPostUrl`, captured from the original `PunchOutSetupRequest.BrowserFormPost.URL` at login time.
A single hidden field (`CXML_FORM_FIELD_NAME`) carries the `PunchOutOrderMessage` built by `PunchoutGatewayService::buildCxmlPunchoutOrderMessage`.
The button is not rendered when no punchout session exists, when the session was created by an OCI login, or when no handler plugin is registered.

### Widget visibility summary

`PunchoutCartWidget` is always instantiated on pages where it is embedded.
Actual form visibility is determined by `PunchoutFormDataBuilder::build()`, which calls plugins of interface `PunchoutFormHandlerPluginInterface` and returns the result of the first whose `isApplicable()` returns `true`.
The Twig template renders the `&lt;form&gt;` and submit button only when `formData` is not `null` and `formData.actionUrl` is not empty.
If you register a custom handler plugin, place it before the default plugins in the dependency provider so it takes precedence for the same protocol.

For OCI flow, the button is rendered for the empty cart as well to allow empty-order return to the eProcurement platform.

For cXML flow, no button is rendered for the empty cart. This behavior will be improved in the next version.

## PunchOut endpoints

`PunchoutGatewayRouteProviderPlugin` (Yves) registers five routes consumed by the buyer&apos;s eProcurement system and by the buyer&apos;s browser:

| Method | Path                                       | Purpose                                                                                                   |
|--------|--------------------------------------------|-----------------------------------------------------------------------------------------------------------|
| `POST` | `/punchout-cxml-setup`                     | Inbound cXML `PunchOutSetupRequest` for the single-connection-per-store case.                             |
| `GET`  | `/punchout-cxml-start?session={token}`     | Buyer&apos;s browser opens this URL with the session token returned in the synchronous `PunchOutSetupResponse`. |
| `POST` | `/punchout-gateway/oci/{connectionSlug}`   | Inbound OCI login form (default method).                                                                  |
| `GET`  | `/punchout-gateway/oci/{connectionSlug}`   | Inbound OCI login when the connection is configured with `formMethod=GET`.                                |

The slug accepted in OCI routes matches `[a-zA-Z0-9_-]+`.

## cXML session start lifecycle

The cXML flow is a two-step handshake:

1. The buyer&apos;s procurement system `POST`s a `PunchOutSetupRequest` to a cXML setup route. The shop authenticates the request, resolves the customer and quote, persists a `spy_punchout_session` row, and replies synchronously with a `PunchOutSetupResponse` whose `StartPage/URL` contains a one-shot session token: ``https://&lt;shop-domain&gt;/punchout-cxml-start?session=&lt;token&gt;``.
2. The buyer&apos;s browser follows that URL. `CxmlController::startAction` reads the token from the `session` query parameter, looks up the session, logs the customer in via `LoginModel::loginCustomerFromSession`, stores the protocol-specific Content Security Policy fragment via `PunchoutSecurityHeaderSessionWriter`, and redirects to the shop.

Two settings control this handshake:

| Setting | Default | Bounds | Purpose |
|---------|---------|--------|---------|
| `getCxmlSessionTokenLength()` | `32`  | 16–128 | Length of the generated session token. |
| `getCxmlSessionStartUrlValidityInSeconds()` | `600` | 0–3600 | How long the start URL remains valid after the synchronous response is sent. |

Both are exposed in the Back Office settings panel under *Configuration &gt; Punchout Gateway*.

## spy_punchout_session table

The session table is created by `propel:install` (see [Set up the database schema](/docs/pbc/all/punchout-gateway/integrate-punchout-gateway.html#set-up-the-database-schema)). It links one PunchOut login attempt to the resulting cart:

| Column | Type | Comment |
|--------|------|---------|
| `id_punchout_session` | integer, PK | Auto-increment. |
| `fk_quote` | integer, FK | Quote that belongs to this session. Cascade-deletes with the quote. |
| `fk_customer` | integer, FK | Customer logged in by the PunchOut handshake. Cascade-deletes with the customer. |
| `fk_punchout_connection` | integer, FK | Connection that produced this session. Cascade-deletes with the connection. |
| `buyer_cookie` | varchar(256) | cXML `BuyerCookie` used to resume the cart across logins. |
| `browser_form_post_url` | text | `BrowserFormPost.URL` (cXML) or `HOOK_URL` (OCI). The cart return form posts here. |
| `operation` | varchar(32) | cXML operation that started the session (`create` or `edit`). Empty for OCI. |
| `session_token` | varchar(255), unique | One-shot token embedded in the cXML start URL. |
| `valid_to` | timestamp | Token expiry; computed from `getCxmlSessionStartUrlValidityInSeconds()`. |
| `session_data` | text | Serialized `PunchoutSessionData` (cXML setup request or OCI login request). |
| `created_at`, `updated_at` | timestamp | Maintained by the timestampable behavior. |
</description>
            <pubDate>Mon, 01 Jun 2026 07:03:08 +0000</pubDate>
            <link>https://docs.spryker.com/docs/pbc/all/punchout-gateway/project-configuration-for-punchout-gateway.html</link>
            <guid isPermaLink="true">https://docs.spryker.com/docs/pbc/all/punchout-gateway/project-configuration-for-punchout-gateway.html</guid>
            
            
        </item>
        
        <item>
            <title>Manage PunchOut connections</title>
            <description>This document describes how to manage PunchOut connections through the Back Office. The same data is stored in the `spy_punchout_connection`, `spy_punchout_credential`, and `spy_punchout_session` tables described in [Project configuration for PunchOut Gateway](/docs/pbc/all/punchout-gateway/project-configuration-for-punchout-gateway.html).

## Open the Back Office UI

In the Back Office, open *Punchout Connections*. The connections grid (`PunchoutConnectionTable`) lists every connection across all stores.

The grid shows:

- ID
- Name
- Protocol type (`oci` or `cxml`)
- Status
- Store
- Actions: *View*, *Edit*, *Activate*/*Deactivate*, *Delete*

## Create a connection

1. On the connections grid, select *Create connection*.
2. Fill in the common fields:

   | Field | Notes                                                                                                                                                                                           |
   |-------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
   | **Connection Name** | Human-readable label. Required. Up to 255 characters. Not unique.                                                                                                                               |
   | **Store** | Store the buyer must be logged in to.                                                                                                                                                           |
   | **Protocol Type** | `oci` or `cxml`. Cannot be changed after the connection is created.                                                                                                                             |
   | **Processor Plugin Class** | FQCN of a `PunchoutProcessorPluginInterface` (or `PunchoutCxmlProcessorPluginInterface`) implementation. The dropdown only offers plugins whose `getType()` matches the selected protocol type. |
   | **Active** | When unchecked, requests posted to this connection are rejected.                                                                                                                                |
   | **Allow iFrame** | When checked, the Storefront emits the iframe-friendly CSP headers while the session is active.                                                                                                 |

3. Fill in the protocol-specific fields, shown dynamically, when you select the protocol type.

   **cXML**

   | Field | Notes                                                                                                                                 |
   |-------|---------------------------------------------------------------------------------------------------------------------------------------|
   | **Sender Identity** | Must be unique. The buyer&apos;s `Header/Sender/Credential/Identity` is matched against this value.                                        |
   | **Sender Shared Secret** | The secret is stored hashed with `password_hash()`. The incoming `SharedSecret` is later verified with `password_verify()`. |

   The cXML request URL is fixed at `/punchout-cxml-setup`, optionally followed by a slug. To target a specific connection, post to `/punchout-cxml-setup/&lt;slug&gt;`.

   **OCI**

   | Field | Notes |
   |-------|-------|
   | **Request URL** | A slug appended to `/punchout-gateway/oci/`. Only `_`, `-`, letters, and digits are allowed. |
   | **Form Method** | HTTP method the buyer uses to submit the OCI login form (`POST` or `GET`). |
   | **Username Field Name** | Form field name carrying the username; defaults to `USERNAME`. |
   | **Password Field Name** | Form field name carrying the password; defaults to `PASSWORD`. |

4. Save the form. The new connection appears in the grid.

## Edit a connection

On the grid row, select *Edit*. The form opens with the same fields as the create form, except:

- **Protocol Type** is read-only.
- For cXML connections, the **Sender Shared Secret** field is blank. Leave it blank to keep the existing secret; type a new value to rotate it.

To toggle a connection on or off without opening the form, use the *Activate* / *Deactivate* action in the grid row.

## View a connection

The *View* action opens a read-only summary of the connection plus the credentials grid (`PunchoutCredentialViewTable`) for that connection. Use this view to inspect existing credentials and to add new ones.

## Delete a connection

The *Delete* action removes the connection. The cascade deletes also remove every `spy_punchout_credential` and every `spy_punchout_session` row that belongs to this connection. Deleting a connection ends every in-flight cart that was started from it.

## Manage credentials (OCI)

Credentials are required only for OCI connections. They map a username and password pair to a customer.

### Add a credential

1. Open the connection&apos;s *View* page.
2. Select *Add credential*.
3. Fill in:

   | Field | Notes |
   |-------|-------|
   | **Username** | Sent by the buyer in the `usernameField` form field. |
   | **Password** / **Repeat Password** | Stored as a `password_hash()` value. |
   | **Customer ID** | The Spryker customer to log in when this credential authenticates. Use the customer autocomplete (powered by `CustomerSuggestController`). |
   | **Active** | When unchecked, the credential is rejected even if the username and password match. |

4. Save.

### Edit a credential

Open the credential row and select *Edit*. To rotate the password, type a new value in **Password**; leave it blank to keep the existing hash.

### Toggle and delete

The credentials grid exposes *Activate* / *Deactivate* and *Delete* actions. Deleting a credential leaves the associated customer untouched.

## Console fallback

If the Back Office is not available (for example, in a fresh local environment), use the demo console commands described in [PunchOut connection configuration](/docs/pbc/all/punchout-gateway/project-configuration-for-punchout-gateway.html#punchout-connection-configuration) to seed one cXML and one OCI connection for store `DE`.
</description>
            <pubDate>Mon, 01 Jun 2026 07:03:08 +0000</pubDate>
            <link>https://docs.spryker.com/docs/pbc/all/punchout-gateway/manage-punchout-connections.html</link>
            <guid isPermaLink="true">https://docs.spryker.com/docs/pbc/all/punchout-gateway/manage-punchout-connections.html</guid>
            
            
        </item>
        
        <item>
            <title>Integrate PunchOut Gateway</title>
            <description>This document describes how to integrate the PunchOut Gateway module into a Spryker shop.

## 1. Install the module

Install the PunchOut Gateway module using Composer:

```bash
composer require spryker-eco/punchout-gateway:^0.4.0
```

## 2. Configure the module

To control logging through the AWS Parameter Store, add the following optional configuration:

**config/Shared/config_default.php**

```php
use SprykerEco\Shared\PunchoutGateway\PunchoutGatewayConstants;

$config[PunchoutGatewayConstants::ENABLE_LOGGING] = getenv(&apos;PUNCHOUT_GATEWAY_ENABLE_LOGGING&apos;) ?: false;
```

### Configuration constants

| Constant | Description                                                                                                                                     | Default  |
|----------|-------------------------------------------------------------------------------------------------------------------------------------------------|----------|
| `ENABLE_LOGGING` | Enables or disables logging for PunchOut Gateway. | `false` |

### Logging

When logging is enabled, the module emits structured entries through `\SprykerEco\Shared\PunchoutGateway\Logger\PunchoutLoggerInterface`. The shipped implementation `PunchoutLogger` writes to the standard Spryker log channels and covers, among others, request reception and parsing, authentication attempts and outcomes, response generation, quote and session creation, and uncaught throwables. When logging is disabled, the resolver returns `NullPunchoutLogger` and all calls become no-ops.

## 3. Additional module configuration

`src/Pyz/Zed/PunchoutGateway/PunchoutGatewayConfig.php` provides the following configuration methods:

| Method | Default | Description |
|--------|---------|-------------|
| `isLoggingEnabled()` | `false` | Enables or disables PunchOut Gateway logging. |
| `getCxmlSessionStartUrlValidityInSeconds()` | `600` | Validity period of the cXML session start URL in seconds. |
| `getOciDefaultStartUrl()` | `&apos;/&apos;` | Default redirect URL after OCI session start. |
| `getCxmlSessionTokenLength()` | `32` | Length of the generated cXML session token. |

The same values can be changed at runtime through the Back Office under *Configuration &gt; Punchout Gateway*.

## 4. Update Quote configuration

Update `QuoteConfig` to allow the PunchOut session field to be saved with the quote.

**src/Pyz/Zed/Quote/QuoteConfig.php**

```php
use Generated\Shared\Transfer\QuoteTransfer;

public function getQuoteFieldsAllowedForSaving(): array
{
    return array_merge(parent::getQuoteFieldsAllowedForSaving(), [
        // ...
        QuoteTransfer::PUNCHOUT_SESSION,
    ]);
}
```

## 5. Set up the database schema

Install the database schema:

```bash
vendor/bin/console propel:install
```

This creates the following tables:

| Table | Description |
|-------|-------------|
| `spy_punchout_connection` | Stores PunchOut connection configuration per store. |
| `spy_punchout_credential` | Stores credentials (username/password) linked to a connection and customer. |
| `spy_punchout_session` | Stores active PunchOut sessions linked to a quote. |

## 6. Generate transfer objects

Generate transfer objects for the module:

```bash
vendor/bin/console transfer:generate
```

## 7. Register plugins

### Register the Quote expander plugin

Add the PunchOut session expander plugin:

**src/Pyz/Zed/Quote/QuoteDependencyProvider.php**

```php
use SprykerEco\Zed\PunchoutGateway\Communication\Plugin\Quote\PunchoutSessionQuoteExpanderPlugin;

protected function getQuoteExpanderPlugins(): array
{
    return [
        // ...
        new PunchoutSessionQuoteExpanderPlugin(),
    ];
}
```

### Register the route provider plugin

Add the route provider plugin:

**src/Pyz/Yves/Router/RouterDependencyProvider.php**

```php
use SprykerEco\Yves\PunchoutGateway\Plugin\Router\PunchoutGatewayRouteProviderPlugin;

protected function getRouteProvider(): array
{
    return [
        // ...
        new PunchoutGatewayRouteProviderPlugin(),
    ];
}
```

### Register the security header expander plugin

Add the security header expander plugin:

**src/Pyz/Yves/Application/ApplicationDependencyProvider.php**

```php
use SprykerEco\Yves\PunchoutGateway\Plugin\Application\PunchoutSecurityHeaderExpanderPlugin;

protected function getSecurityHeaderExpanderPlugins(): array
{
    return [
        // ...
        new PunchoutSecurityHeaderExpanderPlugin(),
    ];
}
```

### Support iframe embedding

If your eProcurement system requires embedding into an iframe, you must also adjust this variable in the deploy file for your environments:

```yml
image:
  environment:
    SPRYKER_YVES_SESSION_COOKIE_SAMESITE: &apos;none&apos;
```


## 8. Register the cart widget

Add the PunchOut cart widget:

**src/Pyz/Yves/ShopApplication/ShopApplicationDependencyProvider.php**

```php
use SprykerEco\Yves\PunchoutGateway\Widget\PunchoutCartWidget;

protected function getGlobalWidgets(): array
{
    return [
        // ...
        PunchoutCartWidget::class,
    ];
}
```

The widget is rendered wherever it is embedded by the cart template. If your project uses the stock `spryker-shop/cart-page` template, embed it inside `SprykerShop/Yves/CartPage/Theme/default/templates/page-layout-cart/page-layout-cart.twig` (or your override) so that the &quot;Transfer Cart&quot; button is shown on the cart page.

The following example shows `PunchoutCartWidget` usage:

```twig
{% raw %}
{% widget &apos;PunchoutCartWidget&apos; args [data.cart] only %}{% endwidget %}
{% endraw %}
```

## 9. Import glossary data

The module provides glossary translations used by the PunchOut flow.

**Option 1: Import using the module&apos;s configuration file**

```bash
vendor/bin/console data:import --config=vendor/spryker-eco/punchout-gateway/data/import/punchout-gateway.yml
```

**Option 2: Copy file content and import individually**

Copy content from `vendor/spryker-eco/punchout-gateway/data/import/*.csv` to the corresponding files in `data/import/common/common/`. Then run:

```bash
vendor/bin/console data:import glossary
```

## 10. Translations for the Back Office

The module ships Zed translations for the Back Office UI in `vendor/spryker-eco/punchout-gateway/data/translation/Zed/en_US.csv` and `de_DE.csv`. They are picked up by the standard Zed translator on the next request—no separate import step is required. To override a label, add an entry with the same key to your project&apos;s Zed translation file.

## Verify the integration

After completing the steps above:

- Open *Punchout Connections* in the Back Office. The grid should render empty until you create your first connection.
- Run `vendor/bin/console punchout-gateway:cxml:demo-connection:create` to insert a demo cXML connection for store `DE` and confirm that DB table `spy_punchout_connection` and the grid both reflect it.
- Run `vendor/bin/console punchout-gateway:oci:demo-connection:create` to insert a demo OCI connection for store `DE` and confirm that DB table `spy_punchout_connection` and the grid both reflect it.

## Additional links

- [Manage PunchOut connections](/docs/pbc/all/punchout-gateway/manage-punchout-connections.html) — Back Office workflow for connections and credentials.
- [Project configuration for PunchOut Gateway](/docs/pbc/all/punchout-gateway/project-configuration-for-punchout-gateway.html) — connection columns, processor and form-handler plugins, endpoints.
- [PunchOut Protocols Coverage](/docs/pbc/all/punchout-gateway/punchout-protocol-coverage.html) — cXML and OCI field-by-field mapping.
</description>
            <pubDate>Mon, 01 Jun 2026 07:03:08 +0000</pubDate>
            <link>https://docs.spryker.com/docs/pbc/all/punchout-gateway/integrate-punchout-gateway.html</link>
            <guid isPermaLink="true">https://docs.spryker.com/docs/pbc/all/punchout-gateway/integrate-punchout-gateway.html</guid>
            
            
        </item>
        
        <item>
            <title>How to migrate to API Platform</title>
            <description>{% info_block infoBox &quot;Start here for batch migration&quot; %}

If you&apos;re migrating multiple modules in one go (the default), follow the [API Platform migration overview](/docs/dg/dev/upgrade-and-migrate/migrate-to-api-platform-overview.html) first — it covers the shop-baseline upgrade, project-config checklist, and batch cleanup. This document is the per-module deep dive referenced from that overview.

{% endinfo_block %}

This document describes how to migrate existing Glue API resources to the API-Platform while maintaining backward compatibility.

## Overview

Migrating from Glue API to API Platform provides several benefits:

- **Schema-based development**: Define resources declaratively in YAML instead of PHP code
- **Automatic OpenAPI documentation**: Interactive API docs generated from schemas
- **Reduced boilerplate**: No need for manual resource builders, mappers, and route definitions
- **Built-in validation**: Declarative validation rules with operation-specific constraints
- **Standardized pagination**: Consistent pagination across all resources
- **Better maintainability**: Clearer separation of concerns with providers and processors

The migration can be done gradually, resource by resource, without breaking existing API consumers.

## Prerequisites

Before migrating resources, ensure you have:

- Integrated API Platform as described in [How to integrate API Platform](/docs/dg/dev/upgrade-and-migrate/integrate-api-platform.html)
- Configured router plugins in correct order (see below)
- Tested that API Platform is working with at least one test resource

## Migration strategy

The migration follows a **gradual replacement** approach:

1. **Coexistence**: Both Glue API and API Platform run side by side
2. **Router priority**: Existing Glue endpoints are matched first, API Platform endpoints second
3. **Resource-by-resource**: Migrate one resource at a time, verify, then move to the next
4. **No breaking changes**: Existing API consumers continue to work during migration
5. **Final cleanup**: Remove Glue router only after all resources are migrated

### Router configuration order

The key to gradual migration is router plugin order. The `SymfonyFrameworkRouterPlugin` must be placed **after** existing Glue router plugins:

`src/Pyz/Glue/Router/RouterDependencyProvider.php`

```php
&lt;?php

declare(strict_types = 1);

namespace Pyz\Glue\Router;

use Spryker\Glue\GlueApplication\Plugin\Rest\GlueRouterPlugin;
use Spryker\Glue\Router\Plugin\Router\SymfonyFrameworkRouterPlugin;
use Spryker\Glue\Router\RouterDependencyProvider as SprykerRouterDependencyProvider;

class RouterDependencyProvider extends SprykerRouterDependencyProvider
{
    /**
     * @return array&lt;\Spryker\Glue\RouterExtension\Dependency\Plugin\RouterPluginInterface&gt;
     */
    protected function getRouterPlugins(): array
    {
        return [
            new GlueRouterPlugin(),        // ← Existing Glue endpoints (checked first)
            new SymfonyFrameworkRouterPlugin(),     // ← API Platform endpoints (checked second)
        ];
    }
}
```

{% info_block warningBox &quot;Router order is critical&quot; %}

If `SymfonyFrameworkRouterPlugin` is placed before `GlueRouterPlugin`, API Platform routes may shadow existing Glue routes and break backward compatibility. Always place it **after** existing routers.

{% endinfo_block %}

With this configuration:
- Request comes in: `GET /customers`
- `GlueRouterPlugin` checks first: If Glue resource exists → use it
- `SymfonyFrameworkRouterPlugin` checks second: If no Glue match → try API Platform
- Result: Existing endpoints continue working, new API Platform endpoints are available

## Migration process

### Step 1: Identify resources to migrate

List all existing Glue resources in your application:

Backend API resources are typically registered in:

`\Pyz\Glue\GlueBackendApiApplication\GlueBackendApiApplicationDependencyProvider::getResourcePlugins()`

Storefront API resources are typically registered in:

`\Pyz\Glue\GlueApplication\GlueApplicationDependencyProvider::getResourceRoutePlugins()`

Create a migration checklist:

```bash
[ ] Customers resource
[ ] Products resource
[ ] Orders resource
[ ] Cart resource
[ ] Wishlist resource
...
```

{% info_block infoBox &quot;Migration order recommendation&quot; %}

Start with simpler, read-only resources (GET operations only) before migrating complex resources with write operations and business logic.

{% endinfo_block %}

### Step 2: Analyze existing Glue resource

Before migrating, understand the existing resource structure.

**Example: Existing Glue Customer Resource**

1. **Resource route plugin:**
   `src/Pyz/Glue/CustomersRestApi/Plugin/GlueApplication/CustomersResourceRoutePlugin.php`

2. **Resource class:**
   `src/Pyz/Glue/CustomersRestApi/Processor/Customer/CustomerReader.php`

3. **Attributes transfer:**
   `src/Generated/Shared/Transfer/RestCustomersAttributesTransfer.php`

4. **Operations supported:**
   - GET `/customers/{customerReference}` - Get single customer
   - GET `/customers` - Get customer collection
   - POST `/customers` - Create customer
   - PATCH `/customers/{customerReference}` - Update customer

### Step 3: Create API Platform schema

Create the equivalent API Platform schema for the resource.

**Map Glue concepts to API Platform:**

| Glue API | API Platform |
|---------------|--------------|
| Resource class | Provider class |
| Resource builder | Schema definition (YAML) |
| Attributes transfer | Resource class (auto-generated) |
| Reader | Provider |
| Writer | Processor |
| Resource route plugin | Operations in schema |
| Relationship plugins | Properties in schema |

**Create schema file:**

`src/Pyz/Zed/Customer/resources/api/backoffice/customers.yml`

```yaml
resource:
    name: Customers
    shortName: Customer
    description: &quot;Customer resource for backoffice API&quot;

    provider: &quot;Pyz\\Glue\\Customer\\Api\\Backoffice\\Provider\\CustomerBackofficeProvider&quot;
    processor: &quot;Pyz\\Glue\\Customer\\Api\\Backoffice\\Processor\\CustomerBackofficeProcessor&quot;

    paginationEnabled: true
    paginationItemsPerPage: 10

    operations:
        - type: Post
        - type: Get
        - type: GetCollection
        - type: Patch

    properties:
        customerReference:
            type: string
            description: &quot;A unique reference for a customer.&quot;
            writable: false
            identifier: true

        email:
            type: string
            description: &quot;The email address of the customer.&quot;
            openapiContext:
                example: &quot;john.doe@example.com&quot;

        firstName:
            type: string
            description: &quot;The first name of the customer.&quot;
            openapiContext:
                example: &quot;John&quot;

        lastName:
            type: string
            description: &quot;The last name of the customer.&quot;
            openapiContext:
                example: &quot;Doe&quot;

        # Map all properties from RestCustomersAttributesTransfer
```

**Create validation schema:**

`src/Pyz/Zed/Customer/resources/api/backoffice/customers.validation.yml`

```yaml
post:
    email:
        - NotBlank:
            message: &quot;Email is required&quot;
        - Email:
            message: &quot;Invalid email format&quot;

    firstName:
        - NotBlank:
            message: &quot;First name is required&quot;

    lastName:
        - NotBlank:
            message: &quot;Last name is required&quot;

patch:
    email:
        - Optional:
            constraints:
                - Email
```

### Step 4: Implement Provider

Create the Provider to handle read operations, reusing existing business logic.

{% info_block infoBox &quot;Reuse existing business logic&quot; %}

The Provider should primarily call existing Facade methods. This ensures consistency and reduces duplication of business logic.

{% endinfo_block %}

`src/Pyz/Zed/Customer/Api/Backoffice/Provider/CustomerBackofficeProvider.php`

```php
&lt;?php

namespace Pyz\Zed\Customer\Api\Backoffice\Provider;

use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\Pagination\TraversablePaginator;
use ApiPlatform\State\ProviderInterface;
use Generated\Api\Backoffice\CustomersBackofficeResource;
use Spryker\Zed\Customer\Business\CustomerFacadeInterface;

class CustomerBackofficeProvider implements ProviderInterface
{
    public function __construct(
        private CustomerFacadeInterface $customerFacade,
    ) {
    }

    public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
    {
        if (isset($uriVariables[&apos;customerReference&apos;])) {
            return $this-&gt;getCustomer($uriVariables[&apos;customerReference&apos;]);
        }

        return $this-&gt;getCustomers($context);
    }

    private function getCustomer(string $customerReference): ?CustomersBackofficeResource
    {
        // Reuse existing Glue logic
        $customerTransfer = $this-&gt;customerFacade-&gt;findCustomerByReference($customerReference);

        if ($customerTransfer === null) {
            return null;
        }

        // Map transfer to API Platform resource
        $resource = new CustomersBackofficeResource();
        $resource-&gt;fromArray($customerTransfer-&gt;toArray());

        return $resource;
    }

    private function getCustomers(array $context): TraversablePaginator
    {
        $filters = $context[&apos;filters&apos;] ?? [];
        $page = (int) ($filters[&apos;page&apos;] ?? 1);
        $itemsPerPage = (int) ($filters[&apos;itemsPerPage&apos;] ?? 10);

        // Reuse existing facade method
        $customerCollection = $this-&gt;customerFacade-&gt;getCustomerCollection($page, $itemsPerPage);

        $resources = [];
        foreach ($customerCollection-&gt;getCustomers() as $customerTransfer) {
            $resource = new CustomersBackofficeResource();
            $resource-&gt;fromArray($customerTransfer-&gt;toArray());
            $resources[] = $resource;
        }

        return new TraversablePaginator(
            new \ArrayObject($resources),
            $page,
            $itemsPerPage,
            $customerCollection-&gt;getTotalCount()
        );
    }
}
```

### Step 5: Implement Processor

Create the Processor to handle write operations.

{% info_block infoBox &quot;Reuse existing business logic&quot; %}

The Processor should primarily call existing Facade methods. This ensures consistency and reduces duplication of business logic.

{% endinfo_block %}

`src/Pyz/Zed/Customer/Api/Backoffice/Processor/CustomerBackofficeProcessor.php`

```php
&lt;?php

namespace Pyz\Zed\Customer\Api\Backoffice\Processor;

use ApiPlatform\Metadata\Operation;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use ApiPlatform\State\ProcessorInterface;
use Generated\Api\Backoffice\CustomersBackofficeResource;
use Generated\Shared\Transfer\CustomerTransfer;
use Spryker\Zed\Customer\Business\CustomerFacadeInterface;

class CustomerBackofficeProcessor implements ProcessorInterface
{
    public function __construct(
        private CustomerFacadeInterface $customerFacade,
    ) {
    }

    public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed
    {
        if ($operation instanceof Post) {
            return $this-&gt;createCustomer($data);
        }

        if ($operation instanceof Patch) {
            return $this-&gt;updateCustomer($data, $uriVariables[&apos;customerReference&apos;]);
        }

        return null;
    }

    private function createCustomer(CustomersBackofficeResource $resource): CustomersBackofficeResource
    {
        $customerTransfer = new CustomerTransfer();
        $customerTransfer-&gt;fromArray($resource-&gt;toArray(), true);

        // Reuse existing facade method
        $customerResponseTransfer = $this-&gt;customerFacade-&gt;addCustomer($customerTransfer);

        $result = new CustomersBackofficeResource();
        $result-&gt;fromArray($customerResponseTransfer-&gt;getCustomerTransfer()-&gt;toArray());

        return $result;
    }

    private function updateCustomer(CustomersBackofficeResource $resource, string $customerReference): CustomersBackofficeResource
    {
        $customerTransfer = new CustomerTransfer();
        $customerTransfer-&gt;fromArray($resource-&gt;toArray(), true);
        $customerTransfer-&gt;setCustomerReference($customerReference);

        // Reuse existing facade method
        $customerResponseTransfer = $this-&gt;customerFacade-&gt;updateCustomer($customerTransfer);

        $result = new CustomersBackofficeResource();
        $result-&gt;fromArray($customerResponseTransfer-&gt;getCustomerTransfer()-&gt;toArray());

        return $result;
    }
}
```

### Step 6: Generate API Platform resource

Generate the Back Office resource class from the schema:

```bash
console api:generate

# Verify generation
ls -la src/Generated/Api/Backoffice/CustomersBackofficeResource.php
```

Generate the storefront resource class from the schema:

```bash
glue api:generate storefront
```

### Step 7: Test the API Platform endpoint

Test that the new endpoint works correctly:

```bash
# Test single resource
curl -X GET http://backoffice.eu.spryker.local/customers/DE--1

# Test collection
curl -X GET http://backoffice.eu.spryker.local/customers?page=1&amp;itemsPerPage=10

# Test create
curl -X POST http://backoffice.eu.spryker.local/customers \
  -H &quot;Content-Type: application/json&quot; \
  -d &apos;{&quot;email&quot;:&quot;test@example.com&quot;,&quot;firstName&quot;:&quot;John&quot;,&quot;lastName&quot;:&quot;Doe&quot;}&apos;

# Test update
curl -X PATCH http://backoffice.eu.spryker.local/customers/DE--1 \
  -H &quot;Content-Type: application/json&quot; \
  -d &apos;{&quot;firstName&quot;:&quot;Jane&quot;}&apos;
```

Verify:
- ✅ Responses match expected format
- ✅ Validation rules work correctly
- ✅ Error handling is appropriate
- ✅ Pagination works for collections
- ✅ OpenAPI documentation is generated at root URL `/`

### Step 8: Run existing Glue API tests

Ensure backward compatibility by running existing tests:

```bash
# Run Glue API tests
vendor/bin/codecept run -c tests/PyzTest/Glue/CustomersRestApi

# Or specific test
vendor/bin/codecept run -c tests/PyzTest/Glue/CustomersRestApi/RestApi/CustomerRestApiCest
```

All existing tests should still pass because:
- `GlueRouterPlugin` is checked first
- Existing Glue endpoints still work
- No breaking changes to consumers

### Step 9: Remove Glue resource files

{% info_block warningBox &quot;Plugin removal is the migration switch&quot; %}

The actual switch from Glue REST to API Platform for this module is removing its `*ResourceRoutePlugin` from the project-level dependency provider (shown below). The optional `excludedPathFragments` setting in `spryker_api_platform.php` controls schema generation only — it does not flip routing. The `spryker/&lt;module&gt;-rest-api` composer package may stay installed; it simply no longer serves routes once the plugin is unregistered.

{% endinfo_block %}

Once the API Platform endpoint is working and tested, remove the old Glue files:

```bash
# Remove resource route plugin
rm src/Pyz/Glue/CustomersRestApi/Plugin/GlueApplication/CustomersResourceRoutePlugin.php

# Remove processor classes
rm -rf src/Pyz/Glue/CustomersRestApi/Processor/

# Update dependency provider to remove plugin registration
```

**Update GlueApplicationDependencyProvider:**

`src/Pyz/Glue/GlueApplication/GlueApplicationDependencyProvider.php`

```php
protected function getResourceRoutePlugins(): array
{
    return [
        // new CustomersResourceRoutePlugin(), // ← Remove this line
        new ProductsResourceRoutePlugin(),
        new OrdersResourceRoutePlugin(),
        // ... keep other plugins
    ];
}
```

### Step 10: Verify migration

After removing Glue resource files:

```bash
# Clear caches
console cache:clear

# Test that API Platform endpoint still works
curl -X GET http://backoffice.eu.spryker.local/customers/DE--1

# Verify OpenAPI docs include the resource
curl http://backoffice.eu.spryker.local/docs.json | jq &apos;.paths&apos;

# Check the interactive documentation at root URL
# Visit: http://backoffice.eu.spryker.local/
```

### Step 11: Repeat for remaining resources

Repeat steps 2-10 for each resource in your migration checklist:

```bash
[✓] Customers resource     ← Migrated
[ ] Products resource      ← Next
[ ] Orders resource
[ ] Cart resource
[ ] Wishlist resource
...
```

## Migration comparison

### Before: Glue API

```bash
Request: GET /customers/DE--1
    ↓
GlueRouterPlugin
    ↓
CustomersResourceRoutePlugin
    ↓
CustomerReaderInterface
    ↓
CustomerFacade
    ↓
RestResourceBuilder
    ↓
Response: RestCustomersAttributesTransfer
```

### After: API Platform

```bash
Request: GET /customers/DE--1
    ↓
SymfonyFrameworkRouterPlugin
    ↓
API Platform Router
    ↓
CustomerBackofficeProvider
    ↓
CustomerFacade (same!)
    ↓
CustomersBackofficeResource
    ↓
Response: JSON (auto-serialized)
```

## Key differences

| Aspect | Glue API | API Platform |
|--------|----------|--------------|
| **Definition** | PHP classes &amp; plugins | YAML schemas |
| **Routing** | ResourceRoutePlugin | Schema operations |
| **Reading data** | Reader classes | Provider classes |
| **Writing data** | Writer classes | Processor classes |
| **Validation** | Manual in reader/writer | Declarative in validation schema |
| **Documentation** | Separate OpenAPI schema | Auto-generated from schema |
| **Response building** | Manual RestResourceBuilder | Auto-serialization |
| **Relationships** | Relationship plugins | Schema properties |
| **File count** | ~10-15 files per resource | ~3-5 files per resource |

## Troubleshooting migration

### Both old and new endpoints respond

**Symptom:** Both Glue and API Platform endpoints return responses.

**Cause:** Different URLs are being used. Check if they&apos;re actually the same:

```bash
# Glue endpoint
GET /customers/DE--1

# API Platform endpoint
GET /customers/DE--1

# Check URL prefixes in configuration
```

**Solution:** Ensure URLs match exactly. API Platform resources use `shortName` for URL generation.

### API Platform endpoint returns 404 during migration

**Symptom:** After creating schema and generating resource, endpoint returns 404.

**Possible causes:**

1. Router order is wrong (SymfonyFrameworkRouterPlugin before GlueRouterPlugin)
2. Cache not cleared
3. Resource not generated

**Solution:**

```bash
# Check router order in RouterDependencyProvider
# Should be: GlueRouterPlugin, then SymfonyFrameworkRouterPlugin

# Clear caches
console cache:clear

# Regenerate resources
console|glue api:generate backoffice

# Verify generated file exists
ls -la src/Generated/Api/Backoffice/CustomersBackofficeResource.php
```

### Different response format between Glue and API Platform

**Symptom:** API Platform returns different JSON structure than Glue.

**Cause:** Glue uses JSON:API format, API Platform uses JSON-LD by default which is configurable and depending on your needs you can migrate to JSON-LD as well or stay with the JSON API format. API-Platform covers this possibility for you

**Solution:**

This is expected. You have three options:

1. **Accept the difference** (recommended): Update API consumers to handle both formats during migration
2. **Configure API Platform format**: Customize serialization to match the Glue format
3. **Use content negotiation**: Support both formats based on `Accept` header

### Business logic differs between implementations

**Symptom:** API Platform endpoint behaves differently than a Glue endpoint.

**Cause:** Provider/Processor uses different facade methods or has different logic.

**Solution:**

Review and ensure both use the same facade methods:

```php
// Glue Reader
$customerReader-&gt;readCustomer($customerReference);
    ↓ calls
$this-&gt;customerFacade-&gt;findCustomerByReference($customerReference);

// API Platform Provider
$this-&gt;customerFacade-&gt;findCustomerByReference($customerReference); // ← Same method!
```

## Best practices

### 1. Migrate in small batches

Don&apos;t try to migrate all resources at once. Migrate in small batches for example:

```bash
Sprint 1: Customers, Products (read-only)
Sprint 2: Orders, Cart
Sprint 3: Wishlist, Checkout
```

### 2. Keep business logic in facades

Don&apos;t duplicate business logic in Providers/Processors:

```php
// ❌ Bad: Logic in Provider
private function getCustomer(string $reference): ?CustomersBackofficeResource
{
    $customer = $this-&gt;repository-&gt;findByReference($reference);
    // ... business logic here
}

// ✅ Good: Delegate to Facade
private function getCustomer(string $reference): ?CustomersBackofficeResource
{
    $customerTransfer = $this-&gt;customerFacade-&gt;findCustomerByReference($reference);
    return $this-&gt;mapToResource($customerTransfer);
}
```

### 3. Use toArray/fromArray for mapping

Leverage generated `toArray()` and `fromArray()` methods:

```php
// Easy mapping between Transfer and Resource
$resource = new CustomersBackofficeResource();
$resource-&gt;fromArray($customerTransfer-&gt;toArray());
```

### 4. Test thoroughly before removing Glue code

- Run all existing tests
- Perform manual testing
- Check with API consumers
- Monitor production traffic

### 5. Document breaking changes

If response formats differ, document changes for API consumers:

```markdown
## Migration Notice: Customers API

The `/customers` endpoint is being migrated to API-Platform.

### Changes:
- Response format: JSON:API → JSON-LD
- Date format: unix timestamp → ISO 8601
- Error format: JSON:API errors → RFC 7807 Problem Details

### Timeline:
- Old endpoint: Supported until 2026-12-31
- New endpoint: Available now
- Deprecation: Old endpoint will return deprecation headers starting 2026-09-01
```

## Next steps

- [API Platform](/docs/dg/dev/architecture/api-platform.html) - Architecture overview
- [API Platform Enablement](/docs/dg/dev/architecture/api-platform/enablement.html) - Creating resources
- [Resource Schemas](/docs/dg/dev/architecture/api-platform/resource-schemas.html) - Resource Schemas
- [Validation Schemas](/docs/dg/dev/architecture/api-platform/validation-schemas.html) - Validation Schemas
- [Troubleshooting](/docs/dg/dev/architecture/api-platform/troubleshooting.html) - Common issues
</description>
            <pubDate>Fri, 29 May 2026 14:32:47 +0000</pubDate>
            <link>https://docs.spryker.com/docs/dg/dev/upgrade-and-migrate/migrate-to-api-platform.html</link>
            <guid isPermaLink="true">https://docs.spryker.com/docs/dg/dev/upgrade-and-migrate/migrate-to-api-platform.html</guid>
            
            
        </item>
        
    </channel>
</rss>
