[{"data":1,"prerenderedAt":1927},["ShallowReactive",2],{"docs-navigation":3,"docs-search":175,"page:\u002Fapi\u002Fdigital-and-subscriptions":1112},[4,11,17,23,30,36,42,48,54,60,66,72,78,84,90,96,102,108,114,120,126,132,139,145,151,157,163,169],{"path":5,"title":6,"description":7,"group":8,"order":9,"docId":10},"\u002Fgetting-started\u002Fintroduction","Introduction","Understand the platform and choose the right way to build with Sellforge.","getting-started",10,"getting-started.introduction",{"path":12,"title":13,"description":14,"group":8,"order":15,"docId":16},"\u002Fgetting-started\u002Fquickstart","Quickstart","Make your first authenticated request to the Sellforge product API.",20,"getting-started.quickstart",{"path":18,"title":19,"description":20,"group":8,"order":21,"docId":22},"\u002Fgetting-started\u002Fmigration-notes","Migration notes","Find the former reference topics and understand changes from the legacy documentation.",30,"getting-started.migration-notes",{"path":24,"title":25,"description":26,"group":27,"order":28,"docId":29},"\u002Fapi\u002Foverview","API overview","Find the right API surface for your application.","api",100,"api.overview",{"path":31,"title":32,"description":33,"group":27,"order":34,"docId":35},"\u002Fapi\u002Fauthentication","Authentication","Use scoped API tokens, user sessions, or publishable storefront keys.",110,"api.authentication",{"path":37,"title":38,"description":39,"group":27,"order":40,"docId":41},"\u002Fapi\u002Fconventions","API conventions","Work with money, resource IDs, pagination, and error responses.",120,"api.conventions",{"path":43,"title":44,"description":45,"group":27,"order":46,"docId":47},"\u002Fapi\u002Fproducts","Products","Search your catalog, inspect a product, and understand the product write endpoints.",130,"api.products",{"path":49,"title":50,"description":51,"group":27,"order":52,"docId":53},"\u002Fapi\u002Fcatalog-resources","Catalog resources","Organize products with categories, brands, tax classes, discounts, and vehicle fitment.",135,"api.catalog-resources",{"path":55,"title":56,"description":57,"group":27,"order":58,"docId":59},"\u002Fapi\u002Fchannels","Sales channels","Configure channel domains, product prices, countries, and payment methods.",138,"api.channels",{"path":61,"title":62,"description":63,"group":27,"order":64,"docId":65},"\u002Fapi\u002Forders","Orders","Read order lists and detail records for your shop.",140,"api.orders",{"path":67,"title":68,"description":69,"group":27,"order":70,"docId":71},"\u002Fapi\u002Fcustomers","Customers","Read customer accounts, merged guest\u002Fcustomer profiles, addresses, and order history.",145,"api.customers",{"path":73,"title":74,"description":75,"group":27,"order":76,"docId":77},"\u002Fapi\u002Fstorefront","Storefront","Identify your shop and retrieve the customer-facing product catalog.",150,"api.storefront",{"path":79,"title":80,"description":81,"group":27,"order":82,"docId":83},"\u002Fapi\u002Fcheckout-sessions","Checkout sessions","Send billing and delivery addresses from your storefront, open the hosted checkout, and confirm the final order.",160,"api.checkout-sessions",{"path":85,"title":86,"description":87,"group":27,"order":88,"docId":89},"\u002Fapi\u002Fdigital-and-subscriptions","Digital products and subscriptions","Sell digital goods, configure recurring plans, and synchronize subscription access.",165,"api.digital-and-subscriptions",{"path":91,"title":92,"description":93,"group":27,"order":94,"docId":95},"\u002Fapi\u002Fwebhooks","Webhooks","Receive and verify signed notifications from the hosted checkout.",170,"api.webhooks",{"path":97,"title":98,"description":99,"group":27,"order":100,"docId":101},"\u002Fapi\u002Finventory-and-fulfillment","Inventory and fulfillment","Work with warehouses, stock, picking, returns, transfers, and procurement.",175,"api.inventory-and-fulfillment",{"path":103,"title":104,"description":105,"group":27,"order":106,"docId":107},"\u002Fapi\u002Ffinance-and-partners","Finance and partners","Find invoice, credit-note, vendor, and affiliate operations.",180,"api.finance-and-partners",{"path":109,"title":110,"description":111,"group":27,"order":112,"docId":113},"\u002Fapi\u002Fdata-and-media","Data and media","Import and export CSV data and upload images for your catalog.",185,"api.data-and-media",{"path":115,"title":116,"description":117,"group":27,"order":118,"docId":119},"\u002Fapi\u002Fcontent-and-email","Content and email","Manage CMS pages, newsletter subscribers, campaigns, and custom email content.",190,"api.content-and-email",{"path":121,"title":122,"description":123,"group":27,"order":124,"docId":125},"\u002Fapi\u002Finsights","Insights and campaigns","Connect storefront tracking, preserve attribution, and read shop analytics.",192,"api.insights",{"path":127,"title":128,"description":129,"group":27,"order":130,"docId":131},"\u002Fapi\u002Fadministration","Administration","Inspect settings, credentials, jobs, audit events, and team access.",195,"api.administration",{"path":133,"title":134,"description":135,"group":136,"order":137,"docId":138},"\u002Fplugins\u002Fintroduction","Plugin overview","Understand how plugins extend the Sellforge kernel.","plugins",200,"plugins.introduction",{"path":140,"title":141,"description":142,"group":136,"order":143,"docId":144},"\u002Fplugins\u002Fquickstart","Your first plugin","Create a minimal plugin that listens for a new order.",210,"plugins.quickstart",{"path":146,"title":147,"description":148,"group":136,"order":149,"docId":150},"\u002Fplugins\u002Fmanifest","Package manifest","Describe your plugin and declare settings in package.json.",220,"plugins.manifest",{"path":152,"title":153,"description":154,"group":136,"order":155,"docId":156},"\u002Fplugins\u002Fhooks","Lifecycle hooks","Subscribe to order events inside the Sellforge kernel.",230,"plugins.hooks",{"path":158,"title":159,"description":160,"group":136,"order":161,"docId":162},"\u002Fplugins\u002Fcontext","Context and services","Use the services provided to your plugin's register function.",240,"plugins.context",{"path":164,"title":165,"description":166,"group":136,"order":167,"docId":168},"\u002Fplugins\u002Fmigrations","Database migrations","Create plugin-owned tables with versioned SQL migrations.",250,"plugins.migrations",{"path":170,"title":171,"description":172,"group":136,"order":173,"docId":174},"\u002Fplugins\u002Finstallation","Installation and lifecycle","Install, enable, inspect, and unload a plugin through the administrative API.",260,"plugins.installation",[176,180,186,191,196,201,206,209,214,219,225,230,235,240,245,248,253,258,263,268,273,278,283,286,291,296,301,306,311,316,319,324,329,334,339,344,349,354,359,364,369,374,379,384,387,392,397,402,407,412,417,422,424,429,434,439,444,449,451,456,461,466,471,476,481,483,488,493,498,501,506,511,516,521,526,531,534,539,544,549,554,559,564,569,574,579,584,589,594,599,602,607,612,617,622,627,630,635,640,645,650,655,660,665,670,675,677,682,687,692,697,702,705,710,715,720,725,730,732,737,742,747,752,757,762,767,769,774,779,784,789,792,797,802,807,812,817,822,825,830,835,840,845,850,853,858,863,868,873,878,883,888,893,896,901,906,911,916,921,923,928,933,938,943,948,950,955,960,965,970,975,977,982,987,992,997,1002,1007,1010,1015,1020,1025,1030,1035,1037,1042,1047,1052,1057,1059,1064,1069,1074,1079,1084,1087,1092,1097,1102,1107],{"id":127,"title":128,"titles":177,"content":178,"level":179},[],"Inspect settings, credentials, jobs, audit events, and team access. These routes are administrative application operations. Use a session with the necessary tenant and team permissions. Scoped API tokens do not provide a general substitute for this session.",1,{"id":181,"title":182,"titles":183,"content":184,"level":185},"\u002Fapi\u002Fadministration#settings-and-credentials","Settings and credentials",[128],"Method Path Purpose GET \u002Fapi\u002Fadmin\u002Fsettings Read visible settings GET \u002Fapi\u002Fadmin\u002Fsettings\u002F:key Read a setting PUT \u002Fapi\u002Fadmin\u002Fsettings\u002F:key Update an allowed setting GET \u002Fapi\u002Fadmin\u002Fcredentials Read credential metadata PUT \u002Fapi\u002Fadmin\u002Fcredentials\u002F:key Update a credential GET \u002Fapi\u002Fadmin\u002Fcredentials\u002F:key\u002Freveal Reveal an allowed credential The service applies additional key and tenant restrictions. Do not assume that an admin session can edit arbitrary platform-wide settings. All settings and credential routes are denied to API tokens.",2,{"id":187,"title":188,"titles":189,"content":190,"level":185},"\u002Fapi\u002Fadministration#jobs","Jobs",[128],"GET \u002Fapi\u002Fadmin\u002Fjobs accepts status, type, and limit and returns jobs. The default limit is 100 and the maximum is 500; this endpoint does not use the common offset\u002Fcount pagination envelope. Each record includes queue_name, job_type, status, attempts, max_attempts, last_error, and timestamps. POST \u002Fapi\u002Fadmin\u002Fjobs\u002F:id\u002Fretry resets the job to pending, clears locks\u002Ferror state, resets attempts, and schedules it immediately. DELETE \u002Fapi\u002Fadmin\u002Fjobs\u002F:id removes a job. Check the operation's external effects before repeating a completed action.",{"id":192,"title":193,"titles":194,"content":195,"level":185},"\u002Fapi\u002Fadministration#audit-log","Audit log",[128],"GET \u002Fapi\u002Fadmin\u002Faudit-log reads administrative activity; GET \u002Fapi\u002Fadmin\u002Faudit-log\u002Fcategories returns categories for filtering. Resource-specific order and customer audit routes are documented with those resources. The existence of a resource audit read scope does not grant a token access to the complete administrative log.",{"id":197,"title":198,"titles":199,"content":200,"level":185},"\u002Fapi\u002Fadministration#users-and-team-members","Users and team members",[128],"Team management uses \u002Fapi\u002Fadmin\u002Fmembers. GET lists members and POST adds one. Member-specific routes manage grants, channels, and membership. Account actions include POST \u002Fapi\u002Fadmin\u002Fusers\u002F:id\u002Freset-2fa, \u002F:id\u002Ftrigger-password-reset, and \u002F:id\u002Fforce-password. These require an authorized session and are excluded from token access. Partner self-service uses the vendor and affiliate APIs, rather than administrative team-management credentials.",{"id":202,"title":203,"titles":204,"content":205,"level":185},"\u002Fapi\u002Fadministration#checkout-and-authentication-settings","Checkout and authentication settings",[128],"Shop checkout configuration uses GET and PUT \u002Fapi\u002Fshop\u002Fcheckout-mode with mode (hosted, payment, or custom). The \u002Fapi\u002Fshop\u002Fcheckout-theme resource controls presentation; \u002Fapi\u002Fshop\u002Fcheckout-webhook controls the result URL. These require an admin session and the applicable settings permission. See Checkout sessions and Webhooks. Global second-factor policy is managed through UAM, using a platform-owner session: Method Path Purpose GET \u002Fapi\u002Fuam\u002Fauth-settings Read policy and the code-email template PUT \u002Fapi\u002Fuam\u002Fauth-settings Set primary_twofa to authenticator or email PUT \u002Fapi\u002Fuam\u002Fauth-settings\u002Fcode-template Save subject and html containing {{code}} DELETE \u002Fapi\u002Fuam\u002Fauth-settings\u002Fcode-template Reset the global code-email template Tenant owners configure their own identity-provider registrations in the App's SSO settings. A platform factor preference does not supply Google\u002FMicrosoft credentials for shops. Shop-specific email overrides use the transactional template API.",{"id":31,"title":32,"titles":207,"content":208,"level":179},[],"Use scoped API tokens, user sessions, or publishable storefront keys. Choose credentials based on who is making the request and which API they need.",{"id":210,"title":211,"titles":212,"content":213,"level":185},"\u002Fapi\u002Fauthentication#credential-types","Credential types",[32],"Type Intended use Header Scoped API token (sfk_…) Server-side integrations and machine access Authorization: Bearer sfk_… Browser cookie session Sellforge shop administration and partner portal Host-bound secure HttpOnly cookie Session JWT An authenticated user acting within their role Authorization: Bearer \u003CJWT> Publishable key (pk_sf_…) Public shop catalog and checkout creation Authorization: Bearer pk_sf_… Keep session JWTs and secret API tokens out of public storefront code. A publishable key identifies a shop and is designed for the storefront API.",{"id":215,"title":216,"titles":217,"content":218,"level":185},"\u002Fapi\u002Fauthentication#scoped-api-tokens","Scoped API tokens",[32],"An administrator creates tokens in the shop admin or through POST \u002Fapi\u002Fadmin\u002Fapi-tokens using an authenticated user session. Token management cannot be called using another API token. {\n  \"name\": \"Catalog integration\",\n  \"scopes\": [\"catalog:read\"],\n  \"expires_in_days\": 90,\n  \"environment\": \"test\"\n} The response includes token metadata in token and the one-time secret in plaintext. Expiry can be set from 1 to 365 days. The environment label defaults to live; choose the correct API host and credentials for the environment you actually want to access.",{"id":220,"title":221,"titles":222,"content":223,"level":224},"\u002Fapi\u002Fauthentication#read-scopes","Read scopes",[32,216],"Scope Resource area catalog:read Products, categories, brands, suppliers, and selected channel data orders:read Orders, selected order audit data, fulfillments, and returns inventory:read Warehouses, locations, stock, and purchase orders customers:read Customer records and selected order and audit data finance:read Selected vendor and affiliate data The policy also defines catalog:write and inventory:write. These have explicit method\u002Fpath allowlists and additional validation. Deletion uses the approval flow. There is no general-purpose orders:write, finance:write, or wildcard scope. Effective access depends on both the token scopes and the creator's current role. The scopes endpoint currently advertises the read scopes; consult the deployed policy before setting up write integrations. catalog:read also covers custom product-field definitions and values. New administration areas such as Digital E-Commerce, Insights, Newsletter Studio, and the merged customer overview currently require a session; existing resource scopes do not automatically include them.",3,{"id":226,"title":227,"titles":228,"content":229,"level":185},"\u002Fapi\u002Fauthentication#browser-sign-in","Browser sign-in",[32],"The Sellforge App redirects to the central Auth frontend. Authentication, provider credentials, membership checks, and session issuance belong to the API. Browser clients use opaque cookie sessions rather than storing bearer tokens in local storage. The browser flow is: Start a registered client request through POST \u002Fapi\u002Fauth\u002Fsso\u002Fstart with client_id and an optional return_path or tenant_id. Enter email and password together, or use an identity provider configured for the selected shop. Complete the second factor. The default is an authenticator app when configured, with email fallback for accounts without an authenticator. A platform owner can prioritize email under UAM → Anmeldung & Sicherheit. Select an eligible shop. Authorization checks membership and that shop's allowed login methods. Exchange the short-lived authorization code through the registered client callback. The exchange is bound to the initiating browser and server-side PKCE transaction. Use GET \u002Fapi\u002Fauth\u002Fsession to inspect the application session and POST \u002Fapi\u002Fauth\u002Fsso\u002Flogout to revoke the session family. Authenticated browser mutations need an allowed Origin, X-Sellforge-Browser: 1, and cookies; tenant-aware requests use X-Tenant-Id to detect a shop switch in another tab. A completed second factor remains valid when switching shops within the verified session. Shop owners configure Google, Microsoft, or another OIDC provider under App → Sicherheit → Single Sign-on using their own provider registration and credentials. First-time external-account linking requires mailbox confirmation. Platform-wide code-email templates are editable in UAM, with shop-specific overrides under the App's email settings. Theme and consent preferences use separate, non-sensitive first-party domain cookies. Authentication cookies remain host-bound, Secure, and HttpOnly.",{"id":231,"title":232,"titles":233,"content":234,"level":185},"\u002Fapi\u002Fauthentication#user-login","User login",[32],"The following bearer-token contract applies to non-browser clients. Requests marked with X-Sellforge-Browser: 1 use the cookie-based flow instead and do not receive browser-readable tokens. curl \"$SELLFORGE_ADMIN_URL\u002Fapi\u002Fauth\u002Flogin\" \\\n  -H 'Content-Type: application\u002Fjson' \\\n  -d '{\"email\":\"developer@example.com\",\"password\":\"your-password\"}' Without two-factor authentication, the response includes token and user. The user object uses id, email, role, display_name, must_change_password, and must_setup_2fa.",{"id":236,"title":237,"titles":238,"content":239,"level":224},"\u002Fapi\u002Fauthentication#two-factor-authentication","Two-factor authentication",[32,232],"When 2FA is enabled, login returns an intermediate response: {\n  \"twofa_required\": true,\n  \"twofa_token\": \"\u003Cshort-lived-stage-one-token>\"\n} Complete the second step through POST \u002Fapi\u002Fauth\u002F2fa\u002Fverify. The stage-one token is valid for five minutes and is not a full session JWT. {\n  \"twofa_token\": \"\u003Cshort-lived-stage-one-token>\",\n  \"code\": \"123456\"\n} To use a backup code, send that value in code and include is_backup_code: true. A successful verification returns the full token and user. Honor must_change_password and must_setup_2fa in login responses before attempting protected operations. Sensitive password and verification endpoints generally apply a limit of 10 requests per minute per IP; individual flows can be stricter, such as email discovery at five requests per ten minutes.",{"id":241,"title":242,"titles":243,"content":244,"level":185},"\u002Fapi\u002Fauthentication#storefront-authentication","Storefront authentication",[32],"curl \"$SELLFORGE_STORE_URL\u002Fapi\u002Fstore\u002Fproducts\" \\\n  -H \"Authorization: Bearer $SELLFORGE_PUBLIC_KEY\" \\\n  -H \"Origin: $STOREFRONT_ORIGIN\" The publishable key selects the tenant. Set STOREFRONT_ORIGIN to the storefront's origin for server-side requests; browsers send their own. Storefront origin restrictions and sales-channel resolution are applied separately. A publishable key does not grant administrative access. Read Storefront for more details. html pre.shiki code .sgsFI, html code.shiki .sgsFI{--shiki-default:#24292E}html pre.shiki code .sYu0t, html code.shiki .sYu0t{--shiki-default:#005CC5}html pre.shiki code .sYBdl, html code.shiki .sYBdl{--shiki-default:#032F62}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .s7eDp, html code.shiki .s7eDp{--shiki-default:#6F42C1}",{"id":49,"title":50,"titles":246,"content":247,"level":179},[],"Organize products with categories, brands, tax classes, discounts, and vehicle fitment. These resources complement the product API. Administrative writes require an authorized admin session or an explicitly supported token scope.",{"id":249,"title":250,"titles":251,"content":252,"level":185},"\u002Fapi\u002Fcatalog-resources#categories","Categories",[50],"Method Path Result GET \u002Fapi\u002Fadmin\u002Fcategories categories, including productCount POST \u002Fapi\u002Fadmin\u002Fcategories Created category PATCH \u002Fapi\u002Fadmin\u002Fcategories\u002F:id Updated category DELETE \u002Fapi\u002Fadmin\u002Fcategories\u002F:id id and deleted Creation requires name; optional fields are slug, parent_id, position, is_visible, icon, and image_url. Use an uploaded image URL for image_url, or null to clear it. Slugs are normalized and must be unique within the relevant shop scope. The list is not paginated. {\n  \"name\": \"Workshop clothing\",\n  \"slug\": \"workshop-clothing\",\n  \"is_visible\": true\n} Token reads use catalog:read. Creation and updates use catalog:write; token deletion goes through the write-approval flow.",{"id":254,"title":255,"titles":256,"content":257,"level":185},"\u002Fapi\u002Fcatalog-resources#brands","Brands",[50],"Use GET and POST \u002Fapi\u002Fadmin\u002Fbrands, and PATCH or DELETE \u002Fapi\u002Fadmin\u002Fbrands\u002F:id. The public read route is \u002Fapi\u002Fbrands. Token access follows the catalog read\u002Fwrite scopes and deletion rules.",{"id":259,"title":260,"titles":261,"content":262,"level":185},"\u002Fapi\u002Fcatalog-resources#suppliers","Suppliers",[50],"Use GET \u002Fapi\u002Fadmin\u002Fsuppliers to list suppliers and GET \u002Fapi\u002Fadmin\u002Fsuppliers\u002F:id to inspect one. Create with POST \u002Fapi\u002Fadmin\u002Fsuppliers and update with PUT \u002Fapi\u002Fadmin\u002Fsuppliers\u002F:id, not PATCH. Supplier IDs can be assigned to products and purchase orders.",{"id":264,"title":265,"titles":266,"content":267,"level":185},"\u002Fapi\u002Fcatalog-resources#tax-classes","Tax classes",[50],"GET \u002Fapi\u002Ftax-classes requires an admin or partner session and returns tax_classes. Creation, updates, and deletion use \u002Fapi\u002Fadmin\u002Ftax-classes and \u002Fapi\u002Fadmin\u002Ftax-classes\u002F:id with POST, PATCH, and DELETE respectively. The create body requires name and rate; is_default is optional. A tax rate is a fraction: 0.20 means 20%. {\n  \"name\": \"Standard VAT\",\n  \"rate\": 0.20,\n  \"is_default\": true\n} The current token allowlist mentions \u002Fapi\u002Fadmin\u002Ftax-classes for reads, but the implemented GET route is \u002Fapi\u002Ftax-classes. Use a session for this route; catalog:read does not currently match the implemented path.",{"id":269,"title":270,"titles":271,"content":272,"level":185},"\u002Fapi\u002Fcatalog-resources#discounts","Discounts",[50],"Use an admin session for discount management. The storefront validation endpoint uses the shop's publishable key: Method Path Purpose GET \u002Fapi\u002Fadmin\u002Fdiscounts Search codes with q, limit, and offset POST \u002Fapi\u002Fadmin\u002Fdiscounts Create a code PATCH \u002Fapi\u002Fadmin\u002Fdiscounts\u002F:id Update a code DELETE \u002Fapi\u002Fadmin\u002Fdiscounts\u002F:id Delete a code POST \u002Fapi\u002Fstore\u002Fdiscounts\u002Fvalidate Validate a code in the storefront flow The create body requires a code of 2–40 characters and a type. For percent, value is 1–100: 10 means 10%. For fixed, value is integer cents. For free_shipping, use 0. {\n  \"code\": \"WELCOME10\",\n  \"type\": \"percent\",\n  \"value\": 10,\n  \"active\": true,\n  \"max_uses_per_user\": 1\n} Optional controls include min_order_cents, usage limits, starts_at, expires_at, channel_ids, and account\u002Fnewsletter requirements. An empty channel list applies to all channels. Final eligibility and totals are evaluated by the checkout pricing flow.",{"id":274,"title":275,"titles":276,"content":277,"level":185},"\u002Fapi\u002Fcatalog-resources#vehicle-fitment","Vehicle fitment",[50],"Vehicle data forms a make → model → year-group hierarchy: Method Path Purpose GET \u002Fapi\u002Fvehicle-makes List makes GET \u002Fapi\u002Fvehicle-makes\u002F:makeId\u002Fmodels List models GET \u002Fapi\u002Fvehicle-models\u002F:modelId\u002Fyear-groups List year groups GET \u002Fapi\u002Fadmin\u002Fproducts\u002F:id\u002Ffitment Read a product's assignments POST \u002Fapi\u002Fadmin\u002Fproducts\u002F:id\u002Ffitment Add an assignment PUT \u002Fapi\u002Fadmin\u002Fproducts\u002F:id\u002Ffitment Replace assignments The lookup routes are allowed for catalog:read tokens; the write policy separately permits selected hierarchy and assignment operations. Product attributes use GET \u002Fapi\u002Fproducts\u002F:id\u002Fattributes, POST \u002Fapi\u002Fadmin\u002Fproducts\u002F:id\u002Fattributes, and DELETE \u002Fapi\u002Fadmin\u002Fattributes\u002F:attrId. The backend path allowlist matters: the split public shop backend does not expose \u002Fapi\u002Fvehicle*. Use a backend that serves these routes for your integration.",{"id":279,"title":280,"titles":281,"content":282,"level":185},"\u002Fapi\u002Fcatalog-resources#custom-product-fields","Custom product fields",[50],"These are tenant-owned field definitions and typed product values, separate from legacy product attributes: Method Path Result GET \u002Fapi\u002Fadmin\u002Fproduct-fields fields, product_count, and supported types POST \u002Fapi\u002Fadmin\u002Fproduct-fields Created field (201) PATCH \u002Fapi\u002Fadmin\u002Fproduct-fields\u002F:id Updated field DELETE \u002Fapi\u002Fadmin\u002Fproduct-fields\u002F:id Removes the definition and its product values GET \u002Fapi\u002Fadmin\u002Fproducts\u002F:id\u002Ffields All definitions with values, including empty ones PUT \u002Fapi\u002Fadmin\u002Fproducts\u002F:id\u002Ffields Sets only the supplied values GET \u002Fapi\u002Fadmin\u002Fproduct-fields\u002F:id\u002Fvalues A field and its populated product values PUT \u002Fapi\u002Fadmin\u002Fproduct-fields\u002F:id\u002Fvalues Bulk values for up to 1,000 products; session only Definition types are text, textarea, number, boolean, date, select, multiselect, and url. Create with a required label, optional stable key, type, and settings such as options, unit, help, required, show_in_shop, is_filterable, and position. Selection types need options; a shop can define at most 100 fields. Example definition: {\n  \"label\": \"Material\",\n  \"key\": \"material\",\n  \"type\": \"select\",\n  \"options\": [\"Cotton\", \"Wool\"],\n  \"show_in_shop\": true\n} Set the value with PUT \u002Fapi\u002Fadmin\u002Fproducts\u002F:id\u002Ffields: {\n  \"values\": {\n    \"material\": \"Cotton\"\n  }\n} Values can be addressed by definition ID or key; null or an empty string clears a value. Type validation runs before saving. Existing populated values must remain compatible when changing a definition's type or options. Public product responses expose populated fields marked show_in_shop as custom_fields, including value and value_text. The documented GET routes support catalog:read. Definition creation\u002Fupdate and per-product value updates support catalog:write; deletion uses the existing approval flow. Bulk value PUT and POST \u002Fapi\u002Fadmin\u002Fproduct-fields\u002Freorder are session-only in the current policy. Reordering accepts { \"ids\": [\"pfd_example\"] }. html pre.shiki code .sgsFI, html code.shiki .sgsFI{--shiki-default:#24292E}html pre.shiki code .sYu0t, html code.shiki .sYu0t{--shiki-default:#005CC5}html pre.shiki code .sYBdl, html code.shiki .sYBdl{--shiki-default:#032F62}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"id":55,"title":56,"titles":284,"content":285,"level":179},[],"Configure channel domains, product prices, countries, and payment methods. A sales channel belongs to one shop and controls its selling context. Use the shop-admin API for configuration and the public storefront\u002Fcheckout APIs for purchases.",{"id":287,"title":288,"titles":289,"content":290,"level":185},"\u002Fapi\u002Fchannels#permissions","Permissions",[56],"GET \u002Fapi\u002Fadmin\u002Fchannels supports catalog:read and returns channels. The channel detail, configuration, domain, country, and payment-method routes require an admin session with the applicable tenant permissions. A catalog:write token can update a product's channel listing, but cannot create or reconfigure a channel. Creating\u002Fdeleting channels and choosing the default additionally require the shop owner.",{"id":292,"title":293,"titles":294,"content":295,"level":185},"\u002Fapi\u002Fchannels#channel-configuration","Channel configuration",[56],"Method Path Purpose GET \u002Fapi\u002Fadmin\u002Fchannels List channels GET \u002Fapi\u002Fadmin\u002Fchannels\u002F:id Read channel POST \u002Fapi\u002Fadmin\u002Fchannels Create a channel PATCH \u002Fapi\u002Fadmin\u002Fchannels\u002F:id Update configuration POST \u002Fapi\u002Fadmin\u002Fchannels\u002F:id\u002Fset-default Choose the active default channel DELETE \u002Fapi\u002Fadmin\u002Fchannels\u002F:id Delete when business-state restrictions permit Creation accepts name, optional slug, default_country, and a supported currency. The returned channel contains id, name, slug, is_default, is_active, currency, default_country, allocation_strategy, auto_confirm_orders, allow_unpaid_orders, and pending_payment_ttl_hours. PATCH accepts name, activation, currency, default country, and order\u002Fallocation rules. Currency changes are restricted once commerce data depends on the channel; changing the currency is not a conversion of existing amounts. A new channel is not automatically the default.",{"id":297,"title":298,"titles":299,"content":300,"level":185},"\u002Fapi\u002Fchannels#storefront-domain-resolution","Storefront domain resolution",[56],"Channel resolution happens within the shop selected by the publishable key, in this order: A configured active channel matching the request's Origin hostname, falling back to Referer. An active channel matching a path prefix such as \u002Fen\u002Fapi\u002Fstore\u002Fproducts. The shop's active default channel. Passing an arbitrary channel_id in a checkout creation body does not select a channel. Use the same origin\u002Fpath context for catalog, cart, and checkout requests. Manage domain mappings with GET or POST \u002Fapi\u002Fadmin\u002Fchannels\u002F:id\u002Fdomains. POST accepts: {\n  \"domain\": \"console.cubora.gg\"\n} GET returns domains; POST returns domain with its mapping id. Remove a mapping with DELETE \u002Fapi\u002Fadmin\u002Fchannels\u002F:id\u002Fdomains\u002F:domainId. A domain is unique across channel mappings; conflicts return 409. This mapping chooses the channel. The publishable-key origin allowlist separately decides whether the request is allowed, and DNS\u002Fproxy routing separately decides which server receives it. Configure all three for a custom storefront.",{"id":302,"title":303,"titles":304,"content":305,"level":185},"\u002Fapi\u002Fchannels#product-prices-and-publication","Product prices and publication",[56],"GET \u002Fapi\u002Fproducts\u002F:id\u002Fchannels returns channel listings for a product and supports catalog:read. PATCH \u002Fapi\u002Fproducts\u002F:id\u002Fchannels\u002F:channelId supports catalog:write, subject to write validation, or an authorized session. {\n  \"is_published\": true,\n  \"price_cents\": 2490,\n  \"compare_at_price_cents\": 2990\n} An optional variant_id selects the variant. For a product without variants, a custom price is stored directly on its channel listing. Set price_cents to null to remove the custom channel price and fall back to the product\u002Fvariant price. compare_at_price_cents can also be null. The base product's publication state and its channel listing are separate. The storefront catalog currently returns base product prices; checkout pricing resolves applicable channel prices and currency. Use server-calculated cart\u002Fsession totals for payment.",{"id":307,"title":308,"titles":309,"content":310,"level":185},"\u002Fapi\u002Fchannels#countries-and-tax-overrides","Countries and tax overrides",[56],"GET \u002Fapi\u002Fadmin\u002Fchannels\u002F:id\u002Fcountries returns countries. Replace the channel's selection with PUT to the same path: {\n  \"country_codes\": [\"AT\", \"DE\"]\n} An empty selection falls back to the available active country list. PATCH \u002Fapi\u002Fadmin\u002Fchannels\u002F:id\u002Fcountries\u002F:code accepts channel-specific name, currency, tax_rate, food_tax_rate, and books_tax_rate; null removes an override. Prefer fractional tax values such as 0.20 for 20%. Add a missing shop-owned country using POST \u002Fapi\u002Fadmin\u002Fchannels\u002F:id\u002Fcountries\u002Fcustom with code, name, and optional currency\u002Ftax fields. Delete a shop-owned country with DELETE \u002Fapi\u002Fadmin\u002Fchannels\u002F:id\u002Fcountries\u002Fcustom\u002F:code; removal affects its assignments across the shop. Shared countries cannot be deleted through this route.",{"id":312,"title":313,"titles":314,"content":315,"level":185},"\u002Fapi\u002Fchannels#payment-methods","Payment methods",[56],"GET \u002Fapi\u002Fadmin\u002Fchannels\u002F:id\u002Fpayment-methods reads the channel selection. PUT replaces it using method_codes: {\n  \"method_codes\": [\"card\", \"apple_pay\", \"google_pay\"]\n} Use codes offered by the current payment-method configuration. An empty selection inherits the tenant-wide configuration. Wallet display also depends on provider\u002Fbrowser eligibility; selecting a code does not guarantee a wallet button on every device. See express checkout. html pre.shiki code .sgsFI, html code.shiki .sgsFI{--shiki-default:#24292E}html pre.shiki code .sYu0t, html code.shiki .sYu0t{--shiki-default:#005CC5}html pre.shiki code .sYBdl, html code.shiki .sYBdl{--shiki-default:#032F62}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"id":79,"title":80,"titles":317,"content":318,"level":179},[],"Send billing and delivery addresses from your storefront, open the hosted checkout, and confirm the final order. To send an address from Cubora or another storefront, include customer.shipping_address and customer.billing_address in POST \u002Fapi\u002Fstore\u002Fcheckout-sessions. Redirect to the returned url; Sellforge then handles the checkout and payment. The integration has four steps: Configure the checkout mode, publishable key, and allowed storefront origins. Create a session with product IDs, quantities, contact details, and addresses. Open its returned checkout URL. Confirm payment on your backend and use the order's final delivery address for fulfillment.",{"id":320,"title":321,"titles":322,"content":323,"level":185},"\u002Fapi\u002Fcheckout-sessions#choose-a-checkout-mode","Choose a checkout mode",[80],"The shop's checkout setting determines who collects customer details and handles payment: Mode Address and shipping Payment hosted Your storefront can prefill details; the customer can review and edit them in Sellforge Sellforge checkout payment Your storefront supplies complete details; Sellforge displays them read-only Sellforge checkout custom Your storefront Your storefront integration Sessions are available in hosted and payment mode. The selected mode is stored on each session so a later shop-setting change does not alter a purchase in progress. This page covers one-time purchases. Recurring plans use a separate subscription session contract, including a different creation body. For Cubora, choose hosted if the customer should be able to correct a prefilled address or use express checkout. Choose payment if Cubora already collects the final addresses, shipping method, and discount code. Set the mode in the shop's checkout settings; it is not a session-request field.",{"id":325,"title":326,"titles":327,"content":328,"level":185},"\u002Fapi\u002Fcheckout-sessions#configure-urls-and-credentials","Configure URLs and credentials",[80],"Use these configuration values in your integration: Variable Value SELLFORGE_CHECKOUT_URL Base URL serving the checkout API; production uses https:\u002F\u002Fcheckout.sellforge.ai SELLFORGE_PUBLIC_KEY Your shop's publishable key beginning with pk_sf_ STOREFRONT_ORIGIN Origin of the page starting checkout, such as https:\u002F\u002Fcubora.gg or https:\u002F\u002Fconsole.cubora.gg CHECKOUT_RETURN_URL Your implemented result page, such as https:\u002F\u002Fcubora.gg\u002Fcheckout\u002Fresult Add the actual storefront origin and the return URL's origin to the key's domain allowlist. https:\u002F\u002Fcubora.gg and https:\u002F\u002Fconsole.cubora.gg are different origins; allowing one does not allow the other. Include https:\u002F\u002Fwww.cubora.gg separately if customers use it. Allowlist entries are origins, not paths such as \u002Fcheckout\u002Fresult. Browser requests send their own Origin header. For server-side requests and curl, send the configured STOREFRONT_ORIGIN explicitly, as shown below. It is also used to resolve a domain-linked sales channel; use the same storefront origin for catalog and checkout requests. The publishable key identifies the shop; a customer does not need a Sellforge admin login. Secret admin API tokens and the checkout webhook secret belong on your backend. With split backends, session creation is available on the shop and checkout backends, while \u002Fapi\u002Fcheckout\u002F* is served by the checkout backend.",{"id":330,"title":331,"titles":332,"content":333,"level":185},"\u002Fapi\u002Fcheckout-sessions#create-a-hosted-session","Create a hosted session",[80],"POST \u002Fapi\u002Fstore\u002Fcheckout-sessions The following example assumes hosted mode. Save this complete request body as checkout-session.json, replacing the example product and return URL. It deliberately uses different billing and delivery addresses: {\n  \"items\": [\n    { \"product_id\": \"prod_example\", \"quantity\": 2 }\n  ],\n  \"return_url\": \"https:\u002F\u002Fcubora.gg\u002Fcheckout\u002Fresult\",\n  \"customer\": {\n    \"email\": \"alex@example.com\",\n    \"billing_address\": {\n      \"first_name\": \"Alex\",\n      \"last_name\": \"Example\",\n      \"company\": \"Example GmbH\",\n      \"street\": \"Musterstrasse 12\",\n      \"address1\": \"Floor 2\",\n      \"postal_code\": \"1010\",\n      \"city\": \"Wien\",\n      \"country\": \"AT\",\n      \"phone\": \"+436641234567\",\n      \"vat_id\": \"ATU12345678\"\n    },\n    \"shipping_address\": {\n      \"first_name\": \"Sam\",\n      \"last_name\": \"Example\",\n      \"company\": \"Example GmbH\",\n      \"street\": \"Lieferstrasse 8\",\n      \"postal_code\": \"4020\",\n      \"city\": \"Linz\",\n      \"country\": \"AT\",\n      \"phone\": \"+436641234567\"\n    }\n  }\n} Send it using the configured base URL, key, and storefront origin: curl --fail-with-body \"$SELLFORGE_CHECKOUT_URL\u002Fapi\u002Fstore\u002Fcheckout-sessions\" \\\n  -H \"Authorization: Bearer $SELLFORGE_PUBLIC_KEY\" \\\n  -H \"Origin: $STOREFRONT_ORIGIN\" \\\n  -H 'Content-Type: application\u002Fjson' \\\n  --data-binary @checkout-session.json Use real Sellforge product IDs from the shop's storefront catalog. An SKU or Cubora's own product identifier is not a substitute. Each item can include an optional variant_id. The request accepts 1–100 items with quantities from 1–999. The return_url must satisfy the shop's domain allowlist. Products must belong to the shop and be purchasable in the resolved sales channel. Sellforge calculates prices, tax, shipping, and discounts; do not send your own item prices or totals.",{"id":335,"title":336,"titles":337,"content":338,"level":224},"\u002Fapi\u002Fcheckout-sessions#customer-fields","Customer fields",[80,331],"Field inside customer Use email Customer's contact email; required before payment billing_address Billing address using the fields below shipping_address Delivery address for physical goods shipping_method Existing Sellforge shipping-method ID, not its display name or a shipping price discount_code Optional code, validated by Sellforge; maximum 64 characters customer_id Optional existing Sellforge customer ID; omit for a guest purchase where guest checkout is enabled The older top-level customer_email is also accepted. If both are present, customer.email takes precedence. Addresses must be nested inside customer; top-level shipping_address, billing_address, or address fields do not populate the session.",{"id":340,"title":341,"titles":342,"content":343,"level":224},"\u002Fapi\u002Fcheckout-sessions#address-field-names","Address field names",[80,331],"Use the same field names for both address objects: Field Meaning Example first_name Given name Alex last_name Family name Example street Street including house number Musterstrasse 12 postal_code Postal code as a string, preserving leading zeroes 01067 city City Dresden country Uppercase two-letter country code DE or AT company Optional company name Example GmbH address1 Optional additional address line, such as floor or apartment Floor 2 phone Contact telephone; required if enabled in shop checkout settings +436641234567 vat_id Optional VAT identification number ATU12345678 Map Cubora's fields explicitly: for example, zip → postal_code, countryCode → country, and combine street and house number into street. address1 is the additional line, not the main street. Use phone, not telephone, and omit unused optional fields instead of sending null. Supply name, street, postal code, city, and country for a complete address. When telephone collection is required, include phone in the shipping address for physical orders and the billing address for digital-only orders. The hosted form uses one contact phone number for both addresses. Its separate-delivery form currently exposes name, company, VAT ID, street, postal code, city, and country; it does not expose a separate additional-address-line input.",{"id":345,"title":346,"titles":347,"content":348,"level":224},"\u002Fapi\u002Fcheckout-sessions#response","Response",[80,331],"{\n  \"id\": \"cs_example\",\n  \"url\": \"https:\u002F\u002Fcheckout.sellforge.ai\u002Fc\u002Fcs_example\"\n} The actual host comes from the deployment's checkout configuration. Redirect the customer to the returned url instead of constructing it yourself. Keep the returned session id associated with your Cubora cart or purchase attempt. For backend fulfillment, persist that association on your backend before opening checkout. The session ID grants access to checkout details, so keep it out of public logs and analytics.",{"id":350,"title":351,"titles":352,"content":353,"level":185},"\u002Fapi\u002Fcheckout-sessions#javascript-integration","JavaScript integration",[80],"This browser helper creates the session and returns its ID and redirect URL. Pass configuration from your application and addresses mapped to the field names above: export async function createCheckoutSession({\n  checkoutBaseUrl,\n  publicKey,\n  returnUrl,\n  items,\n  email,\n  billingAddress,\n  shippingAddress,\n  shippingMethod,\n  discountCode,\n}) {\n  const response = await fetch(\n    `${checkoutBaseUrl.replace(\u002F\\\u002F+$\u002F, '')}\u002Fapi\u002Fstore\u002Fcheckout-sessions`,\n    {\n      method: 'POST',\n      credentials: 'omit',\n      headers: {\n        Authorization: `Bearer ${publicKey}`,\n        'Content-Type': 'application\u002Fjson',\n      },\n      body: JSON.stringify({\n        items,\n        return_url: returnUrl,\n        customer: {\n          email,\n          billing_address: billingAddress,\n          shipping_address: shippingAddress,\n          shipping_method: shippingMethod,\n          discount_code: discountCode,\n        },\n      }),\n    },\n  )\n\n  const data = await response.json()\n  if (!response.ok) {\n    throw new Error(data.message || `Checkout failed (${response.status})`)\n  }\n  return data \u002F\u002F { id, url }\n} Example call for a physical order with the same billing and delivery address. Here config is your application's public checkout configuration and cart.items contains Sellforge product_id, optional variant_id, and quantity values: const address = {\n  first_name: 'Alex',\n  last_name: 'Example',\n  street: 'Musterstrasse 12',\n  postal_code: '1010',\n  city: 'Wien',\n  country: 'AT',\n  phone: '+436641234567',\n}\n\nconst session = await createCheckoutSession({\n  checkoutBaseUrl: config.SELLFORGE_CHECKOUT_URL,\n  publicKey: config.SELLFORGE_PUBLIC_KEY,\n  returnUrl: config.CHECKOUT_RETURN_URL,\n  items: cart.items,\n  email: 'alex@example.com',\n  billingAddress: address,\n  shippingAddress: address,\n})\n\n\u002F\u002F Persist session.id with this purchase attempt in your backend here.\nwindow.location.assign(session.url) Replace the example contact details with the customer's form values. For a different delivery destination, pass a separate shippingAddress. For digital-only purchases, omit shippingAddress. Disable the checkout button while creating the session, and catch errors to show them on the Cubora page before re-enabling it. Each successful creation request creates a new session. If you move session creation to your backend, send Origin from your configured STOREFRONT_ORIGIN in addition to the headers above. In a browser, let the browser supply that header.",{"id":355,"title":356,"titles":357,"content":358,"level":185},"\u002Fapi\u002Fcheckout-sessions#payment-only-mode","Payment-only mode",[80],"Use the same creation endpoint and address structure. In payment mode, Cubora must collect all required details before creating the session: Supply customer.email and complete billing details. Physical orders need customer.shipping_address, even when it is identical to the billing address. Digital-only orders can use customer.billing_address alone. Supply the selected customer.shipping_method for physical delivery so the server can calculate the configured shipping charge. An address alone does not select a shipping method. Supply any customer.discount_code during creation; the customer cannot add one in the payment-only interface. Sellforge displays these details read-only. Correct them in Cubora and create a new session if they change. PATCH \u002Fapi\u002Fcheckout\u002F:id\u002Fcustomer is rejected in this mode with customer_data_owned_by_shop. The server remains responsible for totals and discount validation. Successfully creating a session does not mean all payment-time checks have passed; missing contact details, an invalid shipping method, or shop checkout rules can still prevent payment.",{"id":360,"title":361,"titles":362,"content":363,"level":185},"\u002Fapi\u002Fcheckout-sessions#prefill-billing-details","Prefill billing details",[80],"Both hosted and payment sessions accept billing and delivery addresses during creation. In the hosted form: billing_address fills the primary address form. If only shipping_address is supplied, it is used as the initial primary address. A different shipping_address opens the separate delivery-address form for physical goods. When billing and delivery are the same, send the same values in both objects. The normal hosted form also copies the primary address to delivery when the customer does not choose a separate address. For digital-only products, the form collects billing details and does not need delivery details. The hosted interface guides customers through contact details, billing address, and payment. Prefilling does not bypass these steps: the customer can review and edit the details before payment begins. The hosted checkout loads shipping options for the delivery country and saves the selected method.",{"id":365,"title":366,"titles":367,"content":368,"level":224},"\u002Fapi\u002Fcheckout-sessions#express-checkout","Express checkout",[80,361],"For eligible one-time hosted purchases, enabled Apple Pay, Google Pay, and PayPal methods can offer an express shortcut. Availability also depends on the Stripe account, registered payment domain, browser, and wallet. Express checkout is not offered for payment-only sessions, subscriptions, sessions with an order already created, or zero totals. The checkout requests billing and delivery details from the provider where supported. The wallet's selected details can replace the prefilled values; missing details must be completed in the regular form. Cubora does not need to add a wallet payload to session creation. Prices, tax, shipping, and discounts are still calculated by the API.",{"id":370,"title":371,"titles":372,"content":373,"level":185},"\u002Fapi\u002Fcheckout-sessions#session-lifecycle","Session lifecycle",[80],"An open session has a 30-minute lifetime. The hosted checkout reads it through GET \u002Fapi\u002Fcheckout\u002F:id, collects the required information, and initiates payment. You can use that GET endpoint to inspect customer.billing_address, customer.shipping_address, customer.email, and mode after creation. It uses the session ID itself for access, without a publishable-key header. Once payment has created an order, customer details are locked. POST \u002Fapi\u002Fcheckout\u002F:id\u002Fcomplete checks payment state. A customer returning from 3-D Secure or a redirect payment method must complete this step through the checkout flow. Reading an open session with an already paid order can also reconcile it to completed. When using the returned hosted URL, the Sellforge checkout handles payment initiation and completion for you.",{"id":375,"title":376,"titles":377,"content":378,"level":185},"\u002Fapi\u002Fcheckout-sessions#confirm-the-outcome","Confirm the outcome",[80],"A browser redirect alone is not an authoritative payment confirmation. The result URL includes status, session_id, an order_id when available, and sig. Treat it as navigation back to Cubora; show a pending state until your backend has confirmed payment. For a one-time order, your backend should: Receive and verify the signed checkout notification with the shop's webhook secret. Match data.session_id to the saved Cubora purchase attempt. Deduplicate by event ID and order ID so a repeated notification cannot fulfill twice. For checkout.completed with an order_id, retrieve the order and check its status, currency, and server-calculated total before fulfillment. Paid orders use paid or, after shipment, shipped. Use the final order's shipping_address, which can differ from Cubora's original prefill after edits or wallet checkout. The checkout webhook itself does not include addresses. The storefront order lookup is GET \u002Fapi\u002Fstore\u002Forders\u002F:id, available on the shop and checkout backends with the same publishable key. Set ORDER_ID from the verified notification and read it server-side: curl --fail-with-body \"$SELLFORGE_CHECKOUT_URL\u002Fapi\u002Fstore\u002Forders\u002F$ORDER_ID\" \\\n  -H \"Authorization: Bearer $SELLFORGE_PUBLIC_KEY\" \\\n  -H \"Origin: $STOREFRONT_ORIGIN\" The response has a top-level order object containing id, status, currency, total_cents, customer_email, shipping_address, and items, among other fields. Use the internal ord_… ID, not the display number. This storefront response does not include billing_address; use the server-side admin order lookup with an orders:read token if your integration needs the final billing address too.",{"id":380,"title":381,"titles":382,"content":383,"level":185},"\u002Fapi\u002Fcheckout-sessions#troubleshooting","Troubleshooting",[80],"Symptom Check 401 during creation Send the correct shop's pk_sf_ key as Authorization: Bearer … 403 \u002F Origin nicht erlaubt Allow the actual browser origin; include the configured Origin in server-side calls return_url-Origin error Add the result page's origin to the same allowlist; cubora.gg and console.cubora.gg differ 409 during creation Enable hosted or payment in the shop's checkout settings Missing product or out_of_stock Use Sellforge product\u002Fvariant IDs belonging to the shop and check availability in the resolved channel Address is blank Nest it under customer, and use street, postal_code, city, and country rather than Cubora's unmapped field names Payment-only checkout cannot continue Supply email, complete addresses, the applicable shipping method, and any required phone number before creating the session Address update is rejected Payment-only details belong to the storefront; hosted details lock once an order exists Wallet button is absent Check the enabled payment methods and wallet\u002Fbrowser eligibility; the regular checkout remains available Redirect says success but Cubora is still pending Check verified webhook delivery and the order state on your backend html pre.shiki code .sgsFI, html code.shiki .sgsFI{--shiki-default:#24292E}html pre.shiki code .sYu0t, html code.shiki .sYu0t{--shiki-default:#005CC5}html pre.shiki code .sYBdl, html code.shiki .sYBdl{--shiki-default:#032F62}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .s7eDp, html code.shiki .s7eDp{--shiki-default:#6F42C1}html pre.shiki code .sD7c4, html code.shiki .sD7c4{--shiki-default:#D73A49}html pre.shiki code .sqxcx, html code.shiki .sqxcx{--shiki-default:#E36209}html pre.shiki code .s691h, html code.shiki .s691h{--shiki-default:#22863A;--shiki-default-font-weight:bold}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}",{"id":115,"title":116,"titles":385,"content":386,"level":179},[],"Manage CMS pages, newsletter subscribers, campaigns, and custom email content. The administrative endpoints on this page use an admin session. The reviewed token policy does not include scopes for CMS editing or newsletter management.",{"id":388,"title":389,"titles":390,"content":391,"level":185},"\u002Fapi\u002Fcontent-and-email#cms-pages","CMS pages",[116],"Method Path Purpose GET \u002Fapi\u002Fadmin\u002Fcms-pages List pages, including drafts GET \u002Fapi\u002Fadmin\u002Fcms-pages\u002F:id Read a page POST \u002Fapi\u002Fadmin\u002Fcms-pages Create a page PUT \u002Fapi\u002Fadmin\u002Fcms-pages\u002F:id Update a page DELETE \u002Fapi\u002Fadmin\u002Fcms-pages\u002F:id Delete a non-legal page The body uses slug, title, bodyMd, published, seoTitle, and seoDescription. These field names are intentionally camelCase. {\n  \"slug\": \"about-us\",\n  \"title\": \"About our workshop\",\n  \"bodyMd\": \"We build products for people who love their workshop.\",\n  \"published\": false\n} Public reads use GET \u002Fapi\u002Fpages and GET \u002Fapi\u002Fpages\u002F:slug, resolving the shop from its publishable key. Only published pages appear. A detail response includes slug, title, bodyMd, and updatedAt.",{"id":393,"title":394,"titles":395,"content":396,"level":185},"\u002Fapi\u002Fcontent-and-email#newsletter-subscribers","Newsletter subscribers",[116],"Administrative routes under \u002Fapi\u002Fadmin\u002Fnewsletter\u002F include: GET stats: subscriber overview. GET subscribers: subscriber list. GET subscribers\u002Fexport: CSV export. PATCH subscribers\u002F:id: update a subscriber. Public subscription and tracking handlers use \u002Fstore\u002Fnewsletter..., without \u002Fapi. This prefix is exposed by the current combined API and shop\u002Fshop-admin split backends. POST \u002Fstore\u002Fnewsletter accepts email, optional name, source, bike_model, and consent. Supply the shop's publishable key so the signup belongs to the intended tenant. The response includes subscriber and already_subscribed, with confirmation_required when a confirmation flow is started. Double opt-in is controlled by the shop's newsletter settings; clients should honor the returned flag. Confirmation uses the emailed \u002Fstore\u002Fnewsletter\u002Fconfirm?token=… link. Follow the returned email links for unsubscribe and web-view actions rather than constructing subscriber tokens yourself.",{"id":398,"title":399,"titles":400,"content":401,"level":185},"\u002Fapi\u002Fcontent-and-email#campaigns","Campaigns",[116],"Method Path Purpose GET \u002Fapi\u002Fadmin\u002Fnewsletter\u002Ftemplates Available templates POST \u002Fapi\u002Fadmin\u002Fnewsletter\u002Frender Render a template preview POST \u002Fapi\u002Fadmin\u002Fnewsletter\u002Ftest Send a test email GET \u002Fapi\u002Fadmin\u002Fnewsletter\u002Fcampaigns List campaigns POST \u002Fapi\u002Fadmin\u002Fnewsletter\u002Fcampaigns Create a campaign GET \u002Fapi\u002Fadmin\u002Fnewsletter\u002Fcampaigns\u002F:id Read a campaign PUT \u002Fapi\u002Fadmin\u002Fnewsletter\u002Fcampaigns\u002F:id Update a campaign DELETE \u002Fapi\u002Fadmin\u002Fnewsletter\u002Fcampaigns\u002F:id Delete a campaign POST \u002Fapi\u002Fadmin\u002Fnewsletter\u002Fcampaigns\u002F:id\u002Fsend Queue campaign delivery GET \u002Fapi\u002Fadmin\u002Fnewsletter\u002Fcampaigns\u002F:id\u002Fstats Delivery\u002Fengagement statistics Creation requires name, template_slug, and subject; optional fields include preheader, content, and audience_filter. Use a template returned by the templates endpoint and preview its content before sending.",{"id":403,"title":404,"titles":405,"content":406,"level":185},"\u002Fapi\u002Fcontent-and-email#custom-emails-and-attachments","Custom emails and attachments",[116],"The campaign body can include body_html for custom HTML and attachments descriptors. When HTML is provided, the normal template lookup is skipped. Upload attachments through POST \u002Fapi\u002Fadmin\u002Fnewsletter\u002Fattachments and reuse the returned descriptors; attachment ownership is checked against the shop. Audience selection belongs to audience_filter. An empty legacy filter targets subscribers. A successful send response describes queued recipients; it is not a guarantee that every destination has received the email.",{"id":408,"title":409,"titles":410,"content":411,"level":185},"\u002Fapi\u002Fcontent-and-email#newsletter-studio","Newsletter Studio",[116],"The newer visual editor uses \u002Fapi\u002Fadmin\u002Fnewsletter\u002Fstudio. These admin-session routes supplement the legacy campaign endpoints above and use design plus audience rather than only template_slug, content, and audience_filter. Method Path Purpose GET \u002Fapi\u002Fadmin\u002Fnewsletter\u002Fstudio\u002Fsettings Read settings and shop marke PUT \u002Fapi\u002Fadmin\u002Fnewsletter\u002Fstudio\u002Fsettings Save studio settings GET \u002Fapi\u002Fadmin\u002Fnewsletter\u002Fstudio\u002Ftemplates Read built-in fertig and shop-owned eigene templates POST \u002Fapi\u002Fadmin\u002Fnewsletter\u002Fstudio\u002Ftemplates Save a named design template POST \u002Fapi\u002Fadmin\u002Fnewsletter\u002Fstudio\u002Frender Preview a design; returns html POST \u002Fapi\u002Fadmin\u002Fnewsletter\u002Fstudio\u002Ftest Send a design to 1–5 addresses in to POST \u002Fapi\u002Fadmin\u002Fnewsletter\u002Fstudio\u002Faudience\u002Fcount Preview recipient count and sample for an audience GET \u002Fapi\u002Fadmin\u002Fnewsletter\u002Fstudio\u002Fcampaigns List campaigns POST \u002Fapi\u002Fadmin\u002Fnewsletter\u002Fstudio\u002Fcampaigns Create a draft GET \u002Fapi\u002Fadmin\u002Fnewsletter\u002Fstudio\u002Fcampaigns\u002F:id Read the complete campaign, including design PUT \u002Fapi\u002Fadmin\u002Fnewsletter\u002Fstudio\u002Fcampaigns\u002F:id Update draft\u002Fscheduled content and audience POST \u002Fapi\u002Fadmin\u002Fnewsletter\u002Fstudio\u002Fcampaigns\u002F:id\u002Fduplicate Copy a campaign into a new draft POST \u002Fapi\u002Fadmin\u002Fnewsletter\u002Fstudio\u002Fcampaigns\u002F:id\u002Fsend Send now or schedule using at POST \u002Fapi\u002Fadmin\u002Fnewsletter\u002Fstudio\u002Fcampaigns\u002F:id\u002Funschedule Return a scheduled campaign to draft GET \u002Fapi\u002Fadmin\u002Fnewsletter\u002Fstudio\u002Fcampaigns\u002F:id\u002Freport Delivery, failure, click, and open statistics Minimal draft body: {\n  \"name\": \"Autumn update\",\n  \"subject\": \"News from our shop\",\n  \"template\": \"fertig:leer\"\n} Reuse the design returned by a template or campaign when rendering\u002Fupdating. Sending accepts an optional ISO timestamp at with timezone; more than one minute in the future schedules delivery. The all_customers audience requires confirm_all_customers: true in the send request. Preview\u002Frender only returns HTML; the test and send endpoints actually deliver email. Subscriber management is also available under \u002Fapi\u002Fadmin\u002Fnewsletter\u002Fstudio\u002Fsubscribers, including create, detail, patch, delete, import, and bulk operations. Use the returned subscriber IDs and the studio contract rather than mixing it with the older subscriber endpoint's limited update body.",{"id":413,"title":414,"titles":415,"content":416,"level":185},"\u002Fapi\u002Fcontent-and-email#legal-pages-and-external-urls","Legal pages and external URLs",[116],"Legal checkout content is configured separately from CMS pages. Use an admin session for GET \u002Fapi\u002Fshop\u002Flegal, GET \u002Fapi\u002Fshop\u002Flegal\u002F:key, and PUT \u002Fapi\u002Fshop\u002Flegal\u002F:key. Keys are imprint, terms, privacy, and withdrawal; writes require the CMS-pages permission. The PUT body accepts content (Markdown) and\u002For url. For example, save this to \u002Fapi\u002Fshop\u002Flegal\u002Fprivacy: {\n  \"url\": \"https:\u002F\u002Fcubora.gg\u002Flegal\u002Fprivacy\"\n} A configured HTTPS URL takes precedence in the hosted checkout and counts as configured legal information. Relative paths are resolved against the shop's configured custom domain, or its Sellforge subdomain when none is set. Use a complete external URL for a storefront such as Cubora. Setting url to null or an empty string removes the link, provided a valid saved legal text remains. Administrative GET responses can show default editing templates with isDefault: true; those placeholders do not count as published legal content. Checkout details expose legal flags and legal_urls, and GET \u002Fapi\u002Fcheckout\u002F:id\u002Flegal\u002F:key returns the saved content and optional URL.",{"id":418,"title":419,"titles":420,"content":421,"level":185},"\u002Fapi\u002Fcontent-and-email#transactional-email-templates","Transactional email templates",[116],"With an admin session, GET \u002Fapi\u002Fshop\u002Femail-templates lists templates, and GET \u002Fapi\u002Fshop\u002Femail-templates\u002F:key returns subject, html, supported placeholders, and isDefault. Save both subject and html using PUT; DELETE removes the shop override. Writes require settings permission. Keys are auth_code, order_confirmation, shipping_notification, refund_confirmation, account_welcome, and password_reset. Preserve the returned placeholders. The auth_code HTML must contain {{code}}; its fallback is the platform owner's centrally managed template. Order confirmation supports invoice_url for the customer's signed invoice link. html pre.shiki code .sgsFI, html code.shiki .sgsFI{--shiki-default:#24292E}html pre.shiki code .sYu0t, html code.shiki .sYu0t{--shiki-default:#005CC5}html pre.shiki code .sYBdl, html code.shiki .sYBdl{--shiki-default:#032F62}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"id":37,"title":38,"titles":423,"content":39,"level":179},[],{"id":425,"title":426,"titles":427,"content":428,"level":185},"\u002Fapi\u002Fconventions#requests-and-responses","Requests and responses",[38],"Send JSON request bodies with Content-Type: application\u002Fjson. Most commerce payloads use snake_case field names. Some subsystems, including plugin management, expose fields such as packageName; follow each endpoint's contract. Treat IDs as opaque strings. Use the returned id to address a resource; a human-readable order number is not the resource ID.",{"id":430,"title":431,"titles":432,"content":433,"level":185},"\u002Fapi\u002Fconventions#money","Money",[38],"Monetary values use integer cents. For example, 1299 represents 12.99 in a currency with two decimal places. Product selling prices such as base_price_cents are gross. Purchase-cost fields can be net. Use the field's documented meaning instead of assuming every amount includes tax. Read the returned currency rather than hardcoding EUR. Channel-specific prices can override catalog base prices. Tax rates normally use fractions (0.20 means 20%); discount fields are endpoint-specific: a discount code's value: 10 means 10%, while a product affiliate discount_percent: 0.10 means 10%. Order line items preserve pricing and tax snapshots. Compute displayed totals from server-provided values, especially during checkout.",{"id":435,"title":436,"titles":437,"content":438,"level":185},"\u002Fapi\u002Fconventions#pagination","Pagination",[38],"The administrative product and order lists use these parameters: Parameter Default Behavior limit 50 Clamped to 1–500 offset 0 Non-negative starting offset q Empty Trimmed search string, limited to 120 characters search Empty Alias used when q is absent {\n  \"products\": [],\n  \"count\": 0,\n  \"limit\": 50,\n  \"offset\": 0\n} count is the matching record count, not the current page length. Resource-specific visibility rules can further restrict returned items. In the product list, member channel restrictions are applied after the page query, so a short page does not necessarily mean the end of the catalog. Advance by limit and use the pagination metadata. Not all endpoints use this envelope. For example, the storefront product list returns a products array without the same pagination contract.",{"id":440,"title":441,"titles":442,"content":443,"level":185},"\u002Fapi\u002Fconventions#errors","Errors",[38],"Errors raised through the kernel's HTTP error helpers use error and message, with optional details: {\n  \"error\": \"not_found\",\n  \"message\": \"Nicht gefunden\"\n} Messages may be in German. Prefer status codes and machine-readable error values for application logic. Some older endpoints return only an error field. Status Meaning 400 Invalid input or a failed business rule 401 Missing or invalid authentication 403 Insufficient permissions or rejected origin 404 Missing resource, tenant-hidden resource, or route unavailable on this backend 409 A conflict with the current state 429 Request rate exceeded 500 Unexpected server error 503 A required external service is not configured",{"id":445,"title":446,"titles":447,"content":448,"level":185},"\u002Fapi\u002Fconventions#retrying-requests","Retrying requests",[38],"Retry transient read failures with bounded backoff. For writes, inspect the endpoint's behavior and check the resulting resource before retrying. The API does not promise a universal idempotency header across all routes. For checkout event delivery, use the event ID to deduplicate notifications as described in Webhooks. html pre.shiki code .sgsFI, html code.shiki .sgsFI{--shiki-default:#24292E}html pre.shiki code .sYu0t, html code.shiki .sYu0t{--shiki-default:#005CC5}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sYBdl, html code.shiki .sYBdl{--shiki-default:#032F62}",{"id":67,"title":68,"titles":450,"content":69,"level":179},[],{"id":452,"title":453,"titles":454,"content":455,"level":185},"\u002Fapi\u002Fcustomers#list-customers","List customers",[68],"GET \u002Fapi\u002Fadmin\u002Fcustomers requires an admin identity or a token with customers:read. curl --get \"$SELLFORGE_ADMIN_URL\u002Fapi\u002Fadmin\u002Fcustomers\" \\\n  -H \"Authorization: Bearer $SELLFORGE_API_TOKEN\" \\\n  --data-urlencode 'q=example.com' \\\n  --data-urlencode 'limit=25' q searches email and display name. The response uses customers, count, limit, and offset; accounts are sorted newest first. This list includes user records with the customer role, rather than every guest email address ever used in checkout.",{"id":457,"title":458,"titles":459,"content":460,"level":185},"\u002Fapi\u002Fcustomers#retrieve-a-customer","Retrieve a customer",[68],"GET \u002Fapi\u002Fadmin\u002Fcustomers\u002F:id returns customer with id, email, first_name, last_name, phone, has_account, timestamps, and addresses. In the current serializer, phone is null and addresses is an empty array. Do not interpret those placeholders as a complete address book. Order addresses remain part of order data.",{"id":462,"title":463,"titles":464,"content":465,"level":185},"\u002Fapi\u002Fcustomers#order-history","Order history",[68],"GET \u002Fapi\u002Fadmin\u002Fcustomers\u002F:id\u002Forders returns an orders array. It matches both the customer ID and their email within the relevant shop scope. This can include purchases made before an order was linked to the account. GET \u002Fapi\u002Fadmin\u002Fcustomers\u002F:id\u002Faudit-log returns entries for account-related activity.",{"id":467,"title":468,"titles":469,"content":470,"level":185},"\u002Fapi\u002Fcustomers#imports-and-exports","Imports and exports",[68],"Customer CSV templates, exports, and background imports are covered in Data and media. There is no customers:write token scope in the reviewed policy.",{"id":472,"title":473,"titles":474,"content":475,"level":185},"\u002Fapi\u002Fcustomers#merged-customer-overview","Merged customer overview",[68],"The App's consolidated customer list uses GET \u002Fapi\u002Fadmin\u002Fcustomer-overview, not the older account-only \u002Fapi\u002Fadmin\u002Fcustomers endpoint. It returns customers and count without the common pagination envelope and requires an admin session; customers:read does not include this path. The overview combines customer accounts, saved profiles, and qualifying guest orders within the shop, normalizing email case and surrounding whitespace. Account-linked orders are grouped with that account even if the checkout email differs. Test purchases and never-paid abandoned checkouts do not create guest customer entries. Entries include name, phone, company, group, tags, note, orders_count, total_spent_cents, newsletter, has_account, user_id, and profile_id. IDs can be a profile ID, an account ID, or email:\u003Caddress>; URL-encode the returned ID when using it in a path. GET \u002Fapi\u002Fadmin\u002Fcustomer-overview\u002F:id returns: Field Contents customer Consolidated overview record addresses The linked account's stored address book, or an empty array last_order_address Latest matching order's shipping and billing addresses, or null Address-book entries have separate street, house_number, and address2 fields. Map them into the checkout address format, combining street and house number and using address1 for the additional line. Create saved metadata with POST \u002Fapi\u002Fadmin\u002Fcustomer-profiles (email required), and update with PATCH \u002Fapi\u002Fadmin\u002Fcustomer-profiles\u002F:id. Supported metadata includes name, phone, company, group (Privat, Werkstatt, or Händler), tags, note, and newsletter. Creating a profile does not create a login account. Both operations return customer and require an admin session.",{"id":477,"title":478,"titles":479,"content":480,"level":185},"\u002Fapi\u002Fcustomers#customer-self-service","Customer self-service",[68],"A signed-in customer uses GET \u002Fapi\u002Fcustomer\u002Forders and GET \u002Fapi\u002Fcustomer\u002Forders\u002F:id. These include account-linked orders and qualifying guest orders matched to the customer's verified email within the same shop. Responses expose invoice_available and invoice_url; see customer invoice downloads. html pre.shiki code .s7eDp, html code.shiki .s7eDp{--shiki-default:#6F42C1}html pre.shiki code .sYu0t, html code.shiki .sYu0t{--shiki-default:#005CC5}html pre.shiki code .sYBdl, html code.shiki .sYBdl{--shiki-default:#032F62}html pre.shiki code .sgsFI, html code.shiki .sgsFI{--shiki-default:#24292E}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"id":109,"title":110,"titles":482,"content":111,"level":179},[],{"id":484,"title":485,"titles":486,"content":487,"level":185},"\u002Fapi\u002Fdata-and-media#csv","CSV",[110],"Use an admin session for the documented import\u002Fexport workflow. Download the current template before preparing an import; its column definitions are tied to the deployed kernel. Method Path Purpose GET \u002Fapi\u002Fadmin\u002Fproducts\u002Ftemplate.csv Product template GET \u002Fapi\u002Fadmin\u002Fcustomers\u002Ftemplate.csv Customer template GET \u002Fapi\u002Fadmin\u002Fproducts\u002Fexport.csv Product export GET \u002Fapi\u002Fadmin\u002Fcustomers\u002Fexport.csv Customer export GET \u002Fapi\u002Fadmin\u002Forders\u002Fexport.csv Order export POST \u002Fapi\u002Fadmin\u002Fproducts\u002Fimport Queue a product import POST \u002Fapi\u002Fadmin\u002Fcustomers\u002Fimport Queue a customer import Send an import as multipart form data with a file field: curl \"$SELLFORGE_ADMIN_URL\u002Fapi\u002Fadmin\u002Fproducts\u002Fimport\" \\\n  -H \"Authorization: Bearer $SELLFORGE_SESSION\" \\\n  -F 'file=@products.csv;type=text\u002Fcsv' The response contains job_id and status. Poll GET \u002Fapi\u002Fadmin\u002Fimports\u002F:jobId\u002Fstatus to read progress, summary, last_error, and attempt information. Accepting the upload does not mean every row has been imported successfully. For customer imports, ?invite=1 enables the default invitation behavior for that import.",{"id":489,"title":490,"titles":491,"content":492,"level":185},"\u002Fapi\u002Fdata-and-media#media-uploads","Media uploads",[110],"POST \u002Fapi\u002Fuploads\u002Fimage accepts a multipart image for an admin or partner session. Supported MIME types are JPEG, PNG, WebP, and GIF. curl \"$SELLFORGE_ADMIN_URL\u002Fapi\u002Fuploads\u002Fimage\" \\\n  -H \"Authorization: Bearer $SELLFORGE_SESSION\" \\\n  -F 'file=@product.jpg;type=image\u002Fjpeg' The response includes url, variants, width, height, and size_bytes. The pipeline produces WebP variants and stores them using configured media storage, with a local-disk fallback. POST \u002Fapi\u002Fproducts\u002F:id\u002Fimages uses the same upload pipeline with a product-specific storage path. Preserve the returned URLs when associating images with product data.",{"id":494,"title":495,"titles":496,"content":497,"level":185},"\u002Fapi\u002Fdata-and-media#content-types","Content types",[110],"Let your HTTP client set the multipart boundary. Do not combine a multipart upload with a manually forced Content-Type: application\u002Fjson header. CSV exports are downloadable CSV responses rather than JSON resource envelopes. html pre.shiki code .s7eDp, html code.shiki .s7eDp{--shiki-default:#6F42C1}html pre.shiki code .sYBdl, html code.shiki .sYBdl{--shiki-default:#032F62}html pre.shiki code .sgsFI, html code.shiki .sgsFI{--shiki-default:#24292E}html pre.shiki code .sYu0t, html code.shiki .sYu0t{--shiki-default:#005CC5}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"id":85,"title":86,"titles":499,"content":500,"level":179},[],"Sell digital goods, configure recurring plans, and synchronize subscription access. Digital E-Commerce must be available to the shop and activated. Administrative \u002Fapi\u002Fadmin\u002Fdigital\u002F* routes require an admin session; existing catalog:read\u002Fcatalog:write scopes do not grant access to these add-on routes. Product creation itself still uses the product API. The current API deliberately uses German route and response names such as plaene, abos, and kuendigen. Keep those identifiers unchanged in requests.",{"id":502,"title":503,"titles":504,"content":505,"level":185},"\u002Fapi\u002Fdigital-and-subscriptions#check-activation","Check activation",[86],"GET \u002Fapi\u002Fadmin\u002Fdigital\u002Fstatus returns erlaubt (available), aktiv (enabled), abo_provision, and abo_provision_monate. PUT to the same path updates aktiv and subscription commission settings when the add-on is available. The commission modes are erste, monate, and immer.",{"id":507,"title":508,"titles":509,"content":510,"level":185},"\u002Fapi\u002Fdigital-and-subscriptions#one-time-digital-purchases","One-time digital purchases",[86],"Create a normal product with is_digital: true. Digital products default to track_stock: false on creation; specify track_stock: true if supply is limited. Use GET \u002Fapi\u002Fadmin\u002Fproducts?digital=ja to list them, or digital=alle for the entire catalog. Sell a one-time digital item through POST \u002Fapi\u002Fstore\u002Fcheckout-sessions, using its product ID and customer.billing_address. Digital-only orders need no shipping address or shipping method and return requires_shipping: false. Mixed carts still need delivery details for physical goods. The hosted payment flow collects the digital-content acknowledgement; direct checkout calls use widerruf_verzicht where required. Deliver the digital entitlement from verified payment\u002Forder information in your own application. Marking a product as digital does not upload a file or implement your storefront's access control.",{"id":512,"title":513,"titles":514,"content":515,"level":185},"\u002Fapi\u002Fdigital-and-subscriptions#create-a-recurring-plan","Create a recurring plan",[86],"Use an existing digital product belonging to the shop: Method Path Result GET \u002Fapi\u002Fadmin\u002Fdigital\u002Fplaene plaene, including product names and active counts POST \u002Fapi\u002Fadmin\u002Fdigital\u002Fplaene Created plan PATCH \u002Fapi\u002Fadmin\u002Fdigital\u002Fplaene\u002F:id Updated plan and optional hinweis Example create body: {\n  \"product_id\": \"prod_digital_example\",\n  \"name\": \"Monthly access\",\n  \"price_cents\": 1490,\n  \"interval_unit\": \"month\",\n  \"interval_count\": 1,\n  \"trial_days\": 7,\n  \"is_active\": true\n} price_cents must be 50–10,000,000. Intervals use day, week, month, or year; interval_count defaults to 1 and must satisfy the provider's maximum interval. trial_days defaults to 0 and can be up to 730. Optional intro_discount_percent and intro_discount_months must be supplied together (or both cleared). Responses include currency, interval_text, first_charge_cents, and intro_price_cents. The create schema does not accept a caller-selected currency. Read the returned values for display. Updating a plan's price\u002Finterval affects new subscriptions; existing provider subscriptions keep their price. A plan's product cannot be changed; disable it with is_active: false when it should no longer be sold.",{"id":517,"title":518,"titles":519,"content":520,"level":185},"\u002Fapi\u002Fdigital-and-subscriptions#create-a-subscription-checkout","Create a subscription checkout",[86],"Use POST \u002Fapi\u002Fstore\u002Fabo-sessions, rather than adding a plan to the one-time session's items array. This endpoint is served by the shop and checkout backends and uses the shop's pk_sf_ key with the same origin\u002Freturn-URL rules as checkout sessions. curl --fail-with-body \"$SELLFORGE_CHECKOUT_URL\u002Fapi\u002Fstore\u002Fabo-sessions\" \\\n  -H \"Authorization: Bearer $SELLFORGE_PUBLIC_KEY\" \\\n  -H \"Origin: $STOREFRONT_ORIGIN\" \\\n  -H 'Content-Type: application\u002Fjson' \\\n  -d '{\n    \"plan_id\": \"spl_example\",\n    \"return_url\": \"https:\u002F\u002Fshop.example.com\u002Fcheckout\u002Fresult\",\n    \"customer_email\": \"alex@example.com\"\n  }' The plan must be active and its digital product published. Obtain the real plan ID through the administrative plan configuration; there is currently no public plan-list endpoint in this module. The response is { \"id\": \"cs_example\", \"url\": \"https:\u002F\u002Fcheckout.sellforge.ai\u002Fc\u002Fcs_example\" }. Save the session association and redirect to the returned URL. A subscription session always uses the hosted flow. Its creation schema accepts plan_id, return_url, customer_email, and optional affiliate_code; it does not accept the one-time checkout's nested customer address prefill. Billing details are collected in the hosted checkout. affiliate_code attributes the subscription to a partner; introductory discounts belong to the plan. Subscription checkout does not expose a separate discount-code input or the one-time express shortcut. During a free trial, the amount charged today can be zero while a payment method is collected for later billing.",{"id":522,"title":523,"titles":524,"content":525,"level":185},"\u002Fapi\u002Fdigital-and-subscriptions#synchronize-access","Synchronize access",[86],"Use the signed subscription events sent to the shop's checkout webhook destination. checkout.completed can carry a subscription_id without an order_id during a trial. Ongoing lifecycle events use abo.aktiv, abo.bezahlt, abo.gekuendigt, abo.fortgesetzt, abo.beendet, and abo.zahlung_fehlgeschlagen. Persist the subscription ID and current period in your application. A paid subscription invoice creates an order with subscription_id; the abo.bezahlt event identifies that order. Keep is_test subscriptions separate from real customer access and make repeated event deliveries idempotent.",{"id":527,"title":528,"titles":529,"content":530,"level":185},"\u002Fapi\u002Fdigital-and-subscriptions#administration-and-cancellation","Administration and cancellation",[86],"Method Path Purpose GET \u002Fapi\u002Fadmin\u002Fdigital\u002Fabos List up to 500 subscriptions in abos GET \u002Fapi\u002Fadmin\u002Fdigital\u002Fabos\u002Fstatistik Subscription statistics GET \u002Fapi\u002Fadmin\u002Fdigital\u002Fabos\u002F:id abo plus related bestellungen POST \u002Fapi\u002Fadmin\u002Fdigital\u002Fabos\u002F:id\u002Fkuendigen Cancel; accepts sofort (default false) and optional grund POST \u002Fapi\u002Fadmin\u002Fdigital\u002Fabos\u002F:id\u002Ffortsetzen Withdraw a scheduled cancellation List filters include plan_id, q, test (ja, nein, alle), and status (laufend, testzeit, zahlung_offen, gekuendigt, beendet, alle). Response subscription states use provider-style values such as active, trialing, and past_due, rather than those list-filter names. Customer-account routes are GET \u002Fapi\u002Fcustomer\u002Fabos and POST \u002Fapi\u002Fcustomer\u002Fabos\u002F:id\u002Fkuendigen, served by the shop backend with a customer session. Email management links use the token-protected \u002Fapi\u002Fcheckout\u002Fabo\u002F:token routes on the checkout backend, including POST \u002F:token\u002Fkuendigen and \u002F:token\u002Ffortsetzen. Customer cancellations take effect at the end of the paid period. Read cancel_at_period_end, current_period_end, and endet_am; a cancellation request is not the same as immediate loss of access. html pre.shiki code .sgsFI, html code.shiki .sgsFI{--shiki-default:#24292E}html pre.shiki code .sYu0t, html code.shiki .sYu0t{--shiki-default:#005CC5}html pre.shiki code .sYBdl, html code.shiki .sYBdl{--shiki-default:#032F62}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .s7eDp, html code.shiki .s7eDp{--shiki-default:#6F42C1}",{"id":103,"title":104,"titles":532,"content":533,"level":179},[],"Find invoice, credit-note, vendor, and affiliate operations. Financial operations use an authenticated admin session. A finance:read token permits only selected vendor and affiliate reads; it does not grant access to invoices, credit notes, settlements, refunds, or credential management.",{"id":535,"title":536,"titles":537,"content":538,"level":185},"\u002Fapi\u002Ffinance-and-partners#invoices","Invoices",[104],"Method Path Purpose GET \u002Fapi\u002Fadmin\u002Finvoices List invoices GET \u002Fapi\u002Fadmin\u002Finvoices\u002F:id Retrieve an invoice POST \u002Fapi\u002Fadmin\u002Forders\u002F:orderId\u002Finvoice Create an invoice for an order POST \u002Fapi\u002Fadmin\u002Finvoices\u002F:id\u002Ffinalize Finalize the invoice POST \u002Fapi\u002Fadmin\u002Finvoices\u002F:id\u002Fsend-email Send the invoice email GET \u002Fapi\u002Fadmin\u002Finvoices\u002F:id\u002Fpdf Retrieve its PDF Use the returned invoice ID for later operations. Finalization is a business operation with numbering and accounting effects, rather than a cosmetic status edit. The invoice list uses invoices, count, limit, and offset. It also exposes order_display_id, order_status, and order_paid_at; invoice states such as DRAFT\u002FFINALIZED\u002FSENT are distinct from the order's payment state.",{"id":540,"title":541,"titles":542,"content":543,"level":185},"\u002Fapi\u002Ffinance-and-partners#customer-invoice-downloads","Customer invoice downloads",[104],"GET \u002Fapi\u002Fcustomer\u002Forders\u002F:id\u002Finvoice returns a PDF for the authenticated customer. Verified-email matching can include that customer's guest orders in the same shop. GET \u002Fapi\u002Fcustomer\u002Finvoices lists finalized customer invoices; returned pdf_url values point to GET \u002Fapi\u002Fcustomer\u002Finvoices\u002F:id\u002Fpdf. Guest downloads use the signed invoice_url provided in checkout notifications and confirmation emails. It points to \u002Fstore\u002Finvoices\u002F:token, without \u002Fapi and without a publishable-key header. Follow the returned URL directly. A paid order can generate its invoice on demand if none has been finalized. With SevDesk configured, a download can return 409 \u002F rechnung_in_arbeit while the accounting invoice is being prepared. Unpaid orders return rechnung_nach_zahlung. For browser navigation these failures may be rendered as HTML rather than JSON. The signed download route is exposed by the combined API and the shop\u002Fshop-admin split backends. The checkout-only split backend does not expose \u002Fstore\u002Finvoices; this is another reason to use the returned invoice URL rather than replacing its host with the checkout host.",{"id":545,"title":546,"titles":547,"content":548,"level":185},"\u002Fapi\u002Ffinance-and-partners#credit-notes","Credit notes",[104],"Create a credit note with POST \u002Fapi\u002Fadmin\u002Finvoices\u002F:id\u002Fcredit-note. List with GET \u002Fapi\u002Fadmin\u002Fcredit-notes, finalize with POST \u002Fapi\u002Fadmin\u002Fcredit-notes\u002F:id\u002Ffinalize, and retrieve the PDF at GET \u002Fapi\u002Fadmin\u002Fcredit-notes\u002F:id\u002Fpdf.",{"id":550,"title":551,"titles":552,"content":553,"level":185},"\u002Fapi\u002Ffinance-and-partners#sevdesk","SevDesk",[104],"Credential setup uses POST \u002Fapi\u002Fadmin\u002Fcredentials\u002Fsevdesk\u002Fconnect; connection checks use POST \u002Fapi\u002Fadmin\u002Fcredentials\u002Ftest\u002Fsevdesk. Per-order actions include POST \u002Fapi\u002Fadmin\u002Forders\u002F:id\u002Fsevdesk-retry and POST \u002Fapi\u002Fadmin\u002Forders\u002F:id\u002Fsevdesk-cancel. These endpoints depend on the configured accounting integration and order state. They are blocked for API tokens, including tokens with finance:read.",{"id":555,"title":556,"titles":557,"content":558,"level":185},"\u002Fapi\u002Ffinance-and-partners#vendors","Vendors",[104],"Administrative reads include \u002Fapi\u002Fadmin\u002Fvendors, \u002Fapi\u002Fadmin\u002Fvendors\u002F:id, \u002Fapi\u002Fadmin\u002Fvendor-earnings, and \u002Fapi\u002Fadmin\u002Fvendor-analytics. These are among the finance:read routes. Invitations use POST \u002Fapi\u002Fadmin\u002Fvendors\u002Finvite. Commission-rule changes use POST \u002Fapi\u002Fadmin\u002Fvendors\u002F:id\u002Fcommission-rule. Both require an authorized admin session.",{"id":560,"title":561,"titles":562,"content":563,"level":224},"\u002Fapi\u002Ffinance-and-partners#vendor-earnings","Vendor earnings",[104,556],"GET \u002Fapi\u002Fadmin\u002Fvendor-earnings reads earnings. POST \u002Fapi\u002Fadmin\u002Fvendor-earnings\u002Frelease releases them through the administrative workflow; a read token cannot perform that action.",{"id":565,"title":566,"titles":567,"content":568,"level":224},"\u002Fapi\u002Ffinance-and-partners#vendor-payouts","Vendor payouts",[104,556],"GET and POST \u002Fapi\u002Fadmin\u002Fvendor-payouts list and create payouts. Processing and marking paid use POST \u002Fapi\u002Fadmin\u002Fvendor-payouts\u002F:id\u002Fprocess and \u002F:id\u002Fmark-paid. The current hard-deny policy blocks the collection path even for GET with a token. Use a session to list payouts despite its presence in the read allowlist.",{"id":570,"title":571,"titles":572,"content":573,"level":224},"\u002Fapi\u002Ffinance-and-partners#vendor-statements","Vendor statements",[104,556],"Use GET and POST \u002Fapi\u002Fadmin\u002Fvendor-statements, GET \u002Fapi\u002Fadmin\u002Fvendor-statements\u002F:id, and POST \u002Fapi\u002Fadmin\u002Fvendor-statements\u002F:id\u002Ffinalize. These accounting routes require a session.",{"id":575,"title":576,"titles":577,"content":578,"level":224},"\u002Fapi\u002Ffinance-and-partners#vendor-portal","Vendor portal",[104,556],"Partner sessions use \u002Fapi\u002Fvendor\u002Fme, \u002Fapi\u002Fvendor\u002Fdashboard, \u002Fapi\u002Fvendor\u002Fproducts, \u002Fapi\u002Fvendor\u002Forders, \u002Fapi\u002Fvendor\u002Fearnings, \u002Fapi\u002Fvendor\u002Fledger, \u002Fapi\u002Fvendor\u002Fpayouts, and \u002Fapi\u002Fvendor\u002Fstatements for self-service reads.",{"id":580,"title":581,"titles":582,"content":583,"level":185},"\u002Fapi\u002Ffinance-and-partners#affiliates","Affiliates",[104],"Admin reads include \u002Fapi\u002Fadmin\u002Faffiliates, \u002Fapi\u002Fadmin\u002Faffiliates\u002F:id, \u002Fapi\u002Fadmin\u002Faffiliate-earnings, and \u002Fapi\u002Fadmin\u002Faffiliate-payouts. The token policy allows these with finance:read. Create affiliates with POST \u002Fapi\u002Fadmin\u002Faffiliates, invite with POST \u002Fapi\u002Fadmin\u002Faffiliates\u002Finvite, and update with PATCH \u002Fapi\u002Fadmin\u002Faffiliates\u002F:id using an admin session.",{"id":585,"title":586,"titles":587,"content":588,"level":224},"\u002Fapi\u002Ffinance-and-partners#affiliate-earnings-and-payouts","Affiliate earnings and payouts",[104,581],"Release earnings through POST \u002Fapi\u002Fadmin\u002Faffiliate-earnings\u002Frelease. Administrative payout actions are POST \u002Fapi\u002Fadmin\u002Faffiliate-payouts\u002F:id\u002Fmark-paid and \u002F:id\u002Fcancel. Financial mutations are unavailable to API tokens.",{"id":590,"title":591,"titles":592,"content":593,"level":224},"\u002Fapi\u002Ffinance-and-partners#affiliate-portal","Affiliate portal",[104,581],"Affiliate sessions use \u002Fapi\u002Faffiliate\u002Fme, \u002Fapi\u002Faffiliate\u002Fearnings, \u002Fapi\u002Faffiliate\u002Fpayouts, and \u002Fapi\u002Faffiliate\u002Fcontent. A payout request is POST \u002Fapi\u002Faffiliate\u002Fpayouts\u002Frequest.",{"id":595,"title":596,"titles":597,"content":598,"level":224},"\u002Fapi\u002Ffinance-and-partners#partnerships-attribution-and-payouts","Partnerships, attribution, and payouts",[104,581],"Affiliate partners use central browser sign-in and select the eligible shop. GET \u002Fapi\u002Faffiliate\u002Fpartnerschaften lists their shop partnerships. GET \u002Fapi\u002Faffiliate\u002Fledger returns entries and balances; GET \u002Fapi\u002Faffiliate\u002Fpayouts\u002F:id\u002Fgutschrift downloads a payout credit-note PDF. Administrative session routes include: Method Path Purpose GET \u002Fapi\u002Fadmin\u002Faffiliate-invites List invitations POST \u002Fapi\u002Fadmin\u002Faffiliate-invites\u002F:id\u002Fresend Resend an invitation POST \u002Fapi\u002Fadmin\u002Faffiliate-invites\u002F:id\u002Frevoke Revoke an invitation PUT \u002Fapi\u002Fadmin\u002Faffiliates\u002F:id\u002Fchannels Set nonempty channel_ids GET \u002Fapi\u002Fadmin\u002Faffiliates\u002F:id\u002Fledger Read entries and balances GET \u002Fapi\u002Fadmin\u002Faffiliate-settings Read payout configuration PUT \u002Fapi\u002Fadmin\u002Faffiliate-settings Set auszahlung to manuell or stripe POST \u002Fapi\u002Fadmin\u002Faffiliate-settings\u002Fauszahlen Run the due-payout workflow GET \u002Fapi\u002Fadmin\u002Faffiliate-payouts\u002F:id\u002Fgutschrift Download a payout credit-note PDF These additional paths are not covered by finance:read. Stripe payout setup also needs the shop's connected account. Affiliates connect their own payout account with POST \u002Fapi\u002Faffiliate\u002Fstripe\u002Fconnect and a return_url; it returns url. Read connection state with GET \u002Fapi\u002Faffiliate\u002Fstripe\u002Fstatus. Per-product commission eligibility uses GET and PUT \u002Fapi\u002Fadmin\u002Fproducts\u002F:id\u002Faffiliate. PUT requires eligible, nullable discount_percent, on_sale_mode (hoechster or kein), and nullable channel_ids. Here discount_percent is a fraction from 0 to 1, unlike the percentage value in a discount code. Use the ledger and server-calculated order values for reporting, including refund adjustments.",{"id":121,"title":122,"titles":600,"content":601,"level":179},[],"Connect storefront tracking, preserve attribution, and read shop analytics. Insights combines storefront events with server-side cart, checkout, and purchase events. Administrative routes require an admin session and a selected tenant; no current scoped API token grants general Insights access.",{"id":603,"title":604,"titles":605,"content":606,"level":185},"\u002Fapi\u002Finsights#install-the-storefront-script","Install the storefront script",[122],"Read GET \u002Fapi\u002Fadmin\u002Finsights\u002Fsettings. It returns einstellungen, shop_url, hosts, tenant_id, snippet, and api. Insert the returned script snippet into the storefront once. Its URL has the form \u002Fstore\u002Finsights\u002Fsf.js?shop=\u003Ctenant-id> on the configured public API host. The script and collection routes use \u002Fstore\u002Finsights, without \u002Fapi. They are exposed by the combined API and the shop\u002Fshop-admin split backends, not by the checkout-only split backend. Script loading uses the public tenant identifier, not an admin token. The script tracks page navigation and supported catalog interactions, including SPA history\u002Fhash changes. Collection uses POST \u002Fstore\u002Finsights\u002Fcollect. A 204 response means the request was handled, not that every event was accepted; invalid or disallowed-origin events can also receive 204.",{"id":608,"title":609,"titles":610,"content":611,"level":185},"\u002Fapi\u002Finsights#settings-and-consent","Settings and consent",[122],"Update configuration with PUT \u002Fapi\u002Fadmin\u002Finsights\u002Fsettings. Options include: Field Purpose shop_url Storefront base URL url_product, url_category, url_brand, url_vehicle, url_search Actual storefront URL patterns attribution_days Campaign attribution window, 1–90 days; default 7 consent_required Wait for the storefront's consent signal; default false ignored_visitors Visitor IDs to exclude, such as your own test visits URL patterns support {shop}, {slug}, {id}, {q}, {make}, {model}, and {year} where relevant. Set these to your storefront's routes before generating campaign destinations. When consent_required is enabled, connect the consent banner to the script. After initialization: window.sellforge.consent(true)  \u002F\u002F Customer enabled tracking.\nwindow.sellforge.consent(false) \u002F\u002F Customer withdrew tracking consent. Calling consent(false) clears the script's visitor, session, and campaign cookies and pending event buffers. An external storefront must explicitly integrate this signal; a cookie with a similar name does not automatically configure Insights.",{"id":613,"title":614,"titles":615,"content":616,"level":185},"\u002Fapi\u002Finsights#additional-storefront-events","Additional storefront events",[122],"After the script is ready, supported helpers include: window.sellforge.search('workshop shirt', 12)\nwindow.sellforge.track('wishlist_add', { product: 'prod_example' })\nwindow.sellforge.vehicle('make-slug', 'model-slug', 2024) window.sellforge.visitor() returns the current visitor ID; window.sellforge.campaign() returns the campaign code when set. The script attaches X-SF-Visitor, X-SF-Session, and X-SF-Campaign to fetch\u002FXHR requests targeting its configured API origin. If your storefront calls another host or proxies commerce requests through its own backend, preserve this attribution context when forwarding the request. The visitor\u002Fsession values come from the script's sf_vid and sf_sid cookies; the campaign code comes from sf_cmp. These values are attribution data, not authentication. Preserve UTM parameters and sfc\u002Fsfn through storefront redirects so the script can attribute the landing. Server-generated purchases and recurring billing remain the source for revenue events.",{"id":618,"title":619,"titles":620,"content":621,"level":185},"\u002Fapi\u002Finsights#read-reports","Read reports",[122],"The following GET endpoints use \u002Fapi\u002Fadmin\u002Finsights: Path suffix Contents \u002Foverview zeitraum, kennzahlen, previous-period metrics, chart\u002Ffunnel, and tracking status \u002Ftraffic Sources and device\u002Fbrowser information \u002Fcountries Country breakdown \u002Fproducts Product metrics \u002Fcatalog Catalog metrics \u002Fvehicles Vehicle-selection metrics \u002Fsearch Search metrics \u002Fonline Current online summary \u002Flive Live activity and tracking status \u002Fvisitors\u002F:id One visitor's sessions\u002Fevents Historical reports generally accept from and to as YYYY-MM-DD and optional channel. The default range is 30 days and the maximum requested range is approximately three years; date boundaries use the Europe\u002FVienna reporting context. Online\u002Flive views have their own current-activity scope. Follow each response's field names rather than assuming a paginated items envelope.",{"id":623,"title":624,"titles":625,"content":626,"level":185},"\u002Fapi\u002Finsights#advertising-campaigns","Advertising campaigns",[122],"Campaign administration uses \u002Fapi\u002Fadmin\u002Fad-campaigns: GET \u002Fmeta supplies platform IDs and target types. POST \u002Fpreview resolves a proposed target. GET\u002FPOST on the collection list\u002Fcreate campaigns. GET\u002FPATCH\u002FDELETE \u002F:id read, update, or remove a campaign. POST \u002F:id\u002Fduplicate copies it with a new tracking code. GET \u002F:id\u002Freport reads attributed results. Creation requires name, platform, and target_type, plus the appropriate target_id, target_query, or target_url. Optional fields include UTM overrides, budget_cents, start\u002Fend dates, status, and notes. List responses use kampagnen, and individual responses use kampagne. Use the returned tracking URLs. Public short links use \u002Fstore\u002Fr\u002F:code and redirect to the configured destination with campaign\u002FUTM parameters. Deleting a campaign with clicks archives it to preserve reporting; archived short links still redirect but no longer attach attribution parameters. html pre.shiki code .sgsFI, html code.shiki .sgsFI{--shiki-default:#24292E}html pre.shiki code .s7eDp, html code.shiki .s7eDp{--shiki-default:#6F42C1}html pre.shiki code .sYu0t, html code.shiki .sYu0t{--shiki-default:#005CC5}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sYBdl, html code.shiki .sYBdl{--shiki-default:#032F62}",{"id":97,"title":98,"titles":628,"content":629,"level":179},[],"Work with warehouses, stock, picking, returns, transfers, and procurement. These operational routes are served by the shop-admin backend. Use an admin session for workflow mutations. Token access is limited to the reads and stock adjustment explicitly allowed by the policy.",{"id":631,"title":632,"titles":633,"content":634,"level":185},"\u002Fapi\u002Finventory-and-fulfillment#warehouses","Warehouses",[98],"GET \u002Fapi\u002Fadmin\u002Fwarehouses returns warehouses; the same path accepts POST to create a warehouse. Update or delete with PATCH or DELETE \u002Fapi\u002Fadmin\u002Fwarehouses\u002F:id. Warehouse locations use GET and POST \u002Fapi\u002Fadmin\u002Fwarehouses\u002F:id\u002Flocations, with deletion at \u002Fapi\u002Fadmin\u002Fwarehouse-locations\u002F:id. The separate \u002Fapi\u002Fadmin\u002Fstorage-locations resource manages storage-location assignments and barcodes.",{"id":636,"title":637,"titles":638,"content":639,"level":185},"\u002Fapi\u002Finventory-and-fulfillment#inventory","Inventory",[98],"GET \u002Fapi\u002Fadmin\u002Finventory\u002Fby-warehouse\u002F:warehouseId returns items. It is supported by inventory:read. curl \"$SELLFORGE_ADMIN_URL\u002Fapi\u002Fadmin\u002Finventory\u002Fby-warehouse\u002F$WAREHOUSE_ID\" \\\n  -H \"Authorization: Bearer $SELLFORGE_API_TOKEN\" POST \u002Fapi\u002Fadmin\u002Finventory\u002Fadjust records a stock movement. Its body uses product_id, optional variant_id and warehouse_id, a signed quantity, type, and optional notes. {\n  \"product_id\": \"prod_example\",\n  \"warehouse_id\": \"wh_example\",\n  \"quantity\": 5,\n  \"type\": \"MANUAL_CORRECTION\",\n  \"notes\": \"Stock count found five additional units\"\n} Supported types include PURCHASE_RECEIVED, MANUAL_CORRECTION, STOCKTAKE_ADJUSTMENT, DAMAGED, and RETURN_RECEIVED. Machine writes require inventory:write and additional write validation, including a reason\u002Fnote and a maximum adjustment size. This is a movement, not an absolute replacement of the current stock count. Admin-session routes also include \u002Fapi\u002Fadmin\u002Finventory, \u002Fapi\u002Fadmin\u002Finventory\u002Flow-stock, and \u002Fapi\u002Fadmin\u002Finventory\u002F:productId\u002Ftransactions. The current token read allowlist does not cover all of these paths.",{"id":641,"title":642,"titles":643,"content":644,"level":185},"\u002Fapi\u002Finventory-and-fulfillment#fulfillments","Fulfillments",[98],"GET \u002Fapi\u002Fadmin\u002Ffulfillments lists fulfillments; GET \u002Fapi\u002Fadmin\u002Ffulfillments\u002F:id retrieves details. These reads support orders:read. Create with POST \u002Fapi\u002Fadmin\u002Ffulfillments, providing order_id, optional warehouse_id, and optionally items containing order_item_id and quantity. The workflow uses these POST operations: \u002Fapi\u002Fadmin\u002Ffulfillments\u002F:id\u002Fstart-picking \u002Fapi\u002Fadmin\u002Fpick-list-items\u002F:id\u002Fpick with quantity \u002Fapi\u002Fadmin\u002Ffulfillments\u002F:id\u002Fcomplete-picking \u002Fapi\u002Fadmin\u002Ffulfillments\u002F:id\u002Fpackages \u002Fapi\u002Fadmin\u002Ffulfillments\u002F:id\u002Fship Cancellation uses \u002Fapi\u002Fadmin\u002Ffulfillments\u002F:id\u002Fcancel. Each action validates its business state; a fulfillment is not shipped by simply changing a display field.",{"id":646,"title":647,"titles":648,"content":649,"level":185},"\u002Fapi\u002Finventory-and-fulfillment#returns","Returns",[98],"GET \u002Fapi\u002Fadmin\u002Freturns and \u002Fapi\u002Fadmin\u002Freturns\u002F:id support orders:read. Operational POST actions under \u002Fapi\u002Fadmin\u002Freturns\u002F:id\u002F are approve, receive, inspect, refund, and reject. Refunding uses amount_cents and requires an authorized session. The public request endpoint is POST \u002Fapi\u002Freturns; it validates the order\u002Fcustomer context and applies its own rate limit.",{"id":651,"title":652,"titles":653,"content":654,"level":185},"\u002Fapi\u002Finventory-and-fulfillment#transfers","Transfers",[98],"Use GET \u002Fapi\u002Fadmin\u002Ftransfers or \u002Fapi\u002Fadmin\u002Ftransfers\u002F:id to inspect warehouse transfers. Create with POST to the collection, then POST to \u002F:id\u002Fsend, \u002F:id\u002Freceive, or \u002F:id\u002Fcancel to progress the workflow. These routes require an admin session; inventory:read does not grant transfer access.",{"id":656,"title":657,"titles":658,"content":659,"level":185},"\u002Fapi\u002Finventory-and-fulfillment#production-orders","Production orders",[98],"Use GET or POST \u002Fapi\u002Fadmin\u002Fproduction-orders, and GET \u002Fapi\u002Fadmin\u002Fproduction-orders\u002F:id for details. The \u002F:id\u002Fmaterial-plan GET route shows materials. POST actions include start, complete, produced, and cancel. Bills of materials and production steps are separate resources under \u002Fapi\u002Fadmin\u002Fproducts\u002F:id\u002Fbom and \u002Fapi\u002Fadmin\u002Fproducts\u002F:id\u002Fproduction-steps.",{"id":661,"title":662,"titles":663,"content":664,"level":185},"\u002Fapi\u002Finventory-and-fulfillment#purchase-orders","Purchase orders",[98],"GET \u002Fapi\u002Fadmin\u002Fpurchase-orders is supported by inventory:read. Creation uses POST on the collection; receiving uses POST \u002Fapi\u002Fadmin\u002Fpurchase-orders\u002F:id\u002Freceive. Mutations require an admin session.",{"id":666,"title":667,"titles":668,"content":669,"level":185},"\u002Fapi\u002Finventory-and-fulfillment#shipping-and-tracking","Shipping and tracking",[98],"Administrative shipping methods use \u002Fapi\u002Fadmin\u002Fshipping-methods. Customer-facing method selection uses GET \u002Fapi\u002Fshipping-methods?country=AT and returns methods. The checkout also exposes session-specific shipping selection. The kernel registers GET \u002Fapi\u002Ftracking\u002F:trackingNumber, but the split backend definitions do not currently expose \u002Fapi\u002Ftracking. Check your deployment's routing before offering this legacy endpoint to customers.",{"id":671,"title":672,"titles":673,"content":674,"level":224},"\u002Fapi\u002Finventory-and-fulfillment#shipping-labels-and-delivery","Shipping labels and delivery",[98,667],"With an admin session, GET \u002Fapi\u002Fadmin\u002Forders\u002F:id\u002Fshipping-label reports available providers\u002Fcarriers, calculated weight, address completeness, and any existing label. POST to the same path creates a label using provider: \"senddrop\", carrier_code, weight_grams, and optional mark_shipped (default false). Read the returned label_url to download the PDF through \u002Fapi\u002Fadmin\u002Ffulfillments\u002F:id\u002Flabel.pdf. Label creation requires the shop's SendDrop connection, a complete delivery address, and a shippable, non-test order. These label routes are not included in orders:read. Record delivery with the order delivery action. html pre.shiki code .s7eDp, html code.shiki .s7eDp{--shiki-default:#6F42C1}html pre.shiki code .sYBdl, html code.shiki .sYBdl{--shiki-default:#032F62}html pre.shiki code .sgsFI, html code.shiki .sgsFI{--shiki-default:#24292E}html pre.shiki code .sYu0t, html code.shiki .sYu0t{--shiki-default:#005CC5}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"id":61,"title":62,"titles":676,"content":63,"level":179},[],{"id":678,"title":679,"titles":680,"content":681,"level":185},"\u002Fapi\u002Forders#list-orders","List orders",[62],"GET \u002Fapi\u002Fadmin\u002Forders Requires an admin identity. Machine callers need the orders:read scope. curl --get \"$SELLFORGE_ADMIN_URL\u002Fapi\u002Fadmin\u002Forders\" \\\n  -H \"Authorization: Bearer $SELLFORGE_API_TOKEN\" \\\n  --data-urlencode 'status=paid' \\\n  --data-urlencode 'limit=25'",{"id":683,"title":684,"titles":685,"content":686,"level":224},"\u002Fapi\u002Forders#query-parameters","Query parameters",[62,679],"Parameter Behavior q Searches email, customer name, internal ID, and numeric display number status Filters by a valid order status; unknown values are rejected channel_id Filters to a channel the caller is allowed to access limit, offset Pagination parameters Results are ordered newest first. The response contains orders, count, limit, and offset. Each list entry also includes items_count, the sum of ordered quantities. An unknown or inaccessible channel_id returns 404. Channel restrictions on a team member apply in addition to the tenant boundary.",{"id":688,"title":689,"titles":690,"content":691,"level":185},"\u002Fapi\u002Forders#retrieve-an-order","Retrieve an order",[62],"GET \u002Fapi\u002Fadmin\u002Forders\u002F:id curl \"$SELLFORGE_ADMIN_URL\u002Fapi\u002Fadmin\u002Forders\u002F$ORDER_ID\" \\\n  -H \"Authorization: Bearer $SELLFORGE_API_TOKEN\" The response contains two top-level fields: Field Contents order The serialized order, including customer and total information line_items The order's serialized line items Use the internal order id in the URL. The human-readable display number is for presentation and search. The administrative serializer also includes is_test, requires_shipping, subscription_id, billing_address, shipping_address, and delivered_at. A digital-only purchase has requires_shipping: false; an order generated from a subscription can carry subscription_id. Delivery is tracked with delivered_at while the order's status remains shipped; delivered is not an order-status filter. POST \u002Fapi\u002Fadmin\u002Forders\u002F:id\u002Fdelivered is an admin-session operation that records delivery and returns order with id, status, and delivered_at.",{"id":693,"title":694,"titles":695,"content":696,"level":185},"\u002Fapi\u002Forders#historical-pricing","Historical pricing",[62],"Order line items preserve the product, price, tax, and commission values relevant to the order. A later catalog change does not mean an existing order should be recalculated from today's product price. Read all amounts as integer cents and respect the fields returned by the server.",{"id":698,"title":699,"titles":700,"content":701,"level":185},"\u002Fapi\u002Forders#write-operations","Write operations",[62],"The kernel includes manual order and operational mutation routes. The scoped API-token policy does not offer an orders:write scope. Do not assume that orders:read can create an order, issue a refund, or change its state. For a storefront purchase, start with Checkout sessions. For a server-side extension that reacts to an order, use Plugin hooks. html pre.shiki code .s7eDp, html code.shiki .s7eDp{--shiki-default:#6F42C1}html pre.shiki code .sYu0t, html code.shiki .sYu0t{--shiki-default:#005CC5}html pre.shiki code .sYBdl, html code.shiki .sYBdl{--shiki-default:#032F62}html pre.shiki code .sgsFI, html code.shiki .sgsFI{--shiki-default:#24292E}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"id":24,"title":25,"titles":703,"content":704,"level":179},[],"Find the right API surface for your application. Sellforge exposes HTTP endpoints grouped by domain. JSON endpoints accept Content-Type: application\u002Fjson. Machine integrations use an Authorization header; the administrative browser applications use cookie sessions with the required browser\u002Forigin checks.",{"id":706,"title":707,"titles":708,"content":709,"level":185},"\u002Fapi\u002Foverview#api-surfaces","API surfaces",[25],"Surface Typical routes Credentials Merchant operations \u002Fapi\u002Fadmin\u002Fproducts, \u002Fapi\u002Fadmin\u002Forders Admin session or scoped API token Product writes \u002Fapi\u002Fproducts Authorized session or supported write-scoped token Public storefront \u002Fapi\u002Fstore\u002Fproducts Shop publishable key Checkout creation \u002Fapi\u002Fstore\u002Fcheckout-sessions Shop publishable key Subscription creation \u002Fapi\u002Fstore\u002Fabo-sessions Shop publishable key; Digital E-Commerce enabled Hosted checkout \u002Fapi\u002Fcheckout\u002F:id Checkout session flow Plugin management \u002Fapi\u002Fadmin\u002Fplugins Admin session; mutations require platform context",{"id":711,"title":712,"titles":713,"content":714,"level":185},"\u002Fapi\u002Foverview#base-urls","Base URLs",[25],"Your deployment can serve these APIs on different hosts. Examples use these variables: Variable Backend SELLFORGE_ADMIN_URL Shop-admin API SELLFORGE_STORE_URL Public shop API SELLFORGE_CHECKOUT_URL Checkout API The combined kernel defaults to http:\u002F\u002Flocalhost:9001. The optional split-backend wrappers use local ports 9201, 9203, and 9204 respectively. Current production runs a shared Core per environment; frontend hosts proxy to it. Use the public host that actually serves your route, rather than assuming the split ports exist in production.",{"id":716,"title":717,"titles":718,"content":719,"level":185},"\u002Fapi\u002Foverview#authentication-and-permissions","Authentication and permissions",[25],"API tokens are bound to their creator and tenant. A scope permits only the routes and methods explicitly allowed by the token policy. A valid token does not make every admin endpoint available. Start with Authentication, then read API conventions.",{"id":721,"title":722,"titles":723,"content":724,"level":185},"\u002Fapi\u002Foverview#resources-covered-here","Resources covered here",[25],"Products: catalog queries and product writes. Sales channels: domains, channel prices, countries, and payment methods. Orders: order lists, details, and status filtering. Storefront: shop identification and channel-aware product visibility. Checkout sessions: create a checkout and return the customer to your shop. Webhooks: signed checkout notifications. Digital products and subscriptions: recurring plans, checkout, and access events. Content and email: CMS, legal links, transactional templates, and Newsletter Studio. Insights: storefront tracking and campaign attribution.",{"id":726,"title":727,"titles":728,"content":729,"level":185},"\u002Fapi\u002Foverview#versioning","Versioning",[25],"The current routes use \u002Fapi\u002F..., without a \u002Fv1 path prefix. The source-review date on each guide identifies the implementation used for the reference. Check changes against your deployed kernel before depending on new fields or endpoints.",{"id":43,"title":44,"titles":731,"content":45,"level":179},[],{"id":733,"title":734,"titles":735,"content":736,"level":185},"\u002Fapi\u002Fproducts#list-products","List products",[44],"GET \u002Fapi\u002Fadmin\u002Fproducts Requires an admin identity. API token callers need catalog:read. Results are scoped to the shop and exclude soft-deleted products. curl --get \"$SELLFORGE_ADMIN_URL\u002Fapi\u002Fadmin\u002Fproducts\" \\\n  -H \"Authorization: Bearer $SELLFORGE_API_TOKEN\" \\\n  --data-urlencode 'q=brake' \\\n  --data-urlencode 'status=published' \\\n  --data-urlencode 'limit=25'",{"id":738,"title":739,"titles":740,"content":741,"level":224},"\u002Fapi\u002Fproducts#filters","Filters",[44,734],"Parameter Values or behavior q Searches name, SKU, slug, and variant SKU status draft, published, or archived category_id Category ID, or ohne for no category brand_id Brand ID, or ohne for no brand supplier_id Supplier ID, or ohne for no supplier digital ja for digital, nein for physical, or alle for both limit, offset Pagination parameters Products are ordered by creation date, newest first. The response contains products, count, limit, and offset. Employee channel restrictions can reduce the returned page after pagination. When Digital E-Commerce is active, omitting digital defaults to nein; otherwise it defaults to alle. Set digital=alle explicitly when exporting the entire catalog.",{"id":743,"title":744,"titles":745,"content":746,"level":185},"\u002Fapi\u002Fproducts#retrieve-a-product","Retrieve a product",[44],"GET \u002Fapi\u002Fadmin\u002Fproducts\u002F:id curl \"$SELLFORGE_ADMIN_URL\u002Fapi\u002Fadmin\u002Fproducts\u002F$PRODUCT_ID\" \\\n  -H \"Authorization: Bearer $SELLFORGE_API_TOKEN\" The response wraps the record in product.",{"id":748,"title":749,"titles":750,"content":751,"level":224},"\u002Fapi\u002Fproducts#common-fields","Common fields",[44,744],"Field Meaning id Product ID name, slug, sku Product identity status Publication state type STANDARD, ASSEMBLY, or INTERNAL base_price_cents Gross base selling price in cents cost_price_cents Purchase-cost field, nullable category_id, brand_id, supplier_id Related record IDs variants Product variants images Product image data is_digital Whether the product is delivered without physical shipping track_stock Whether inventory availability is tracked custom_fields Populated custom fields on the detail response; each has a key, type, value, and display text",{"id":753,"title":754,"titles":755,"content":756,"level":185},"\u002Fapi\u002Fproducts#create-a-product","Create a product",[44],"POST \u002Fapi\u002Fproducts Product writes use \u002Fapi\u002Fproducts, rather than the administrative read-list prefix. A session must have an allowed role; API token writes need catalog:write and must pass the additional write-policy checks. curl \"$SELLFORGE_ADMIN_URL\u002Fapi\u002Fproducts\" \\\n  -H \"Authorization: Bearer $SELLFORGE_API_TOKEN\" \\\n  -H 'Content-Type: application\u002Fjson' \\\n  -d '{\n    \"name\": \"Workshop T-shirt\",\n    \"sku\": \"TSHIRT-001\",\n    \"status\": \"draft\",\n    \"type\": \"STANDARD\",\n    \"base_price_cents\": 2490\n  }' name is required. status defaults to draft, type to STANDARD, and base_price_cents to 0. The schema also accepts descriptions, related IDs, variants, images, and options. Creating a digital product or changing a physical product to is_digital: true requires the Digital E-Commerce add-on. On creation, track_stock defaults to false for a digital product and true otherwise. Changing is_digital later does not automatically change track_stock; send the intended value explicitly. See Digital products and subscriptions.",{"id":758,"title":759,"titles":760,"content":761,"level":185},"\u002Fapi\u002Fproducts#update-a-product","Update a product",[44],"PATCH \u002Fapi\u002Fproducts\u002F:id accepts a partial product body and returns the updated product. {\n  \"name\": \"Workshop T-shirt — blue\",\n  \"base_price_cents\": 2690\n} Price and other sensitive changes are subject to the token policy when using machine credentials. An update to the base product is not a replacement for configuring channel-specific listings. Use the dedicated custom-field endpoints to write field values and the product-channel endpoints to set channel prices.",{"id":763,"title":764,"titles":765,"content":766,"level":185},"\u002Fapi\u002Fproducts#storefront-visibility","Storefront visibility",[44],"The public listing is GET \u002Fapi\u002Fstore\u002Fproducts. It filters by tenant and publication, and requires a published listing when a channel is resolved. Checkout applies additional sellability, stock, and pricing checks. See Storefront. html pre.shiki code .s7eDp, html code.shiki .s7eDp{--shiki-default:#6F42C1}html pre.shiki code .sYu0t, html code.shiki .sYu0t{--shiki-default:#005CC5}html pre.shiki code .sYBdl, html code.shiki .sYBdl{--shiki-default:#032F62}html pre.shiki code .sgsFI, html code.shiki .sgsFI{--shiki-default:#24292E}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"id":73,"title":74,"titles":768,"content":75,"level":179},[],{"id":770,"title":771,"titles":772,"content":773,"level":185},"\u002Fapi\u002Fstorefront#identify-the-shop","Identify the shop",[74],"Requests under \u002Fapi\u002Fstore\u002F* use a publishable key beginning with pk_sf_. Create and rotate this key through the shop's public API settings. curl \"$SELLFORGE_STORE_URL\u002Fapi\u002Fstore\u002Fproducts\" \\\n  -H \"Authorization: Bearer $SELLFORGE_PUBLIC_KEY\" \\\n  -H \"Origin: $STOREFRONT_ORIGIN\" The key identifies the tenant. Origin restrictions, when configured, are checked in addition to the key. Configure the domains your actual storefront uses. Set STOREFRONT_ORIGIN to the origin of your storefront, such as https:\u002F\u002Fcubora.gg. Server-side requests and curl must send an allowed storefront Origin when the allowlist is configured; browsers supply their own origin. Key management uses an administrative session: GET \u002Fapi\u002Fshop\u002Fpublic-key returns the prefix, origin allowlist, and usage timestamps. POST \u002Fapi\u002Fshop\u002Fpublic-key\u002Frotate requires the shop owner and returns the new key once; the previous key stops working immediately. PUT \u002Fapi\u002Fshop\u002Fpublic-key\u002Fallowlist accepts domains, an array of allowed origin URLs, and requires settings write permission. For example, the allowlist body can be { \"domains\": [\"https:\u002F\u002Fcubora.gg\", \"https:\u002F\u002Fconsole.cubora.gg\"] }. These exact origins are separate from channel domain mappings.",{"id":775,"title":776,"titles":777,"content":778,"level":185},"\u002Fapi\u002Fstorefront#read-the-public-catalog","Read the public catalog",[74],"GET \u002Fapi\u002Fstore\u002Fproducts returns a products array. The listing returns the shop's published products. When a sales channel is resolved, it also requires a published product listing in that channel. With no resolved channel it falls back to the shop's published products. This route does not separately filter is_sellable or is_visible_in_listings; presence in this response is not a guarantee that checkout will accept the product. If a product appears in the admin but not in the storefront, check: You are using the publishable key for the intended shop. The product has status: published. The storefront resolves to the intended channel. The product has a published listing in that channel. The response includes is_digital, track_stock, and populated custom_fields with show_in_shop enabled. Catalog base_price_cents is the product's base price: this listing does not replace it with a channel-specific price. Use the cart\u002Fsession pricing response for authoritative purchase totals. Sales channels explains domain resolution and price configuration.",{"id":780,"title":781,"titles":782,"content":783,"level":185},"\u002Fapi\u002Fstorefront#use-the-correct-backend","Use the correct backend",[74],"The public shop backend exposes the \u002Fapi\u002Fstore route family. The shop-admin backend exposes only selected storefront routes, so its host is not a general substitute for the shop API URL. The storefront listing does not use the same pagination envelope as the administrative product list. Use Products when building an authenticated catalog administration or export integration.",{"id":785,"title":786,"titles":787,"content":788,"level":185},"\u002Fapi\u002Fstorefront#continue-to-checkout","Continue to checkout",[74],"Use product and variant IDs from your shop's catalog to create a checkout session. The guide includes complete billing\u002Fshipping payloads, JavaScript examples, and final-order verification. Prices and final totals are calculated by the server. html pre.shiki code .s7eDp, html code.shiki .s7eDp{--shiki-default:#6F42C1}html pre.shiki code .sYBdl, html code.shiki .sYBdl{--shiki-default:#032F62}html pre.shiki code .sgsFI, html code.shiki .sgsFI{--shiki-default:#24292E}html pre.shiki code .sYu0t, html code.shiki .sYu0t{--shiki-default:#005CC5}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"id":91,"title":92,"titles":790,"content":791,"level":179},[],"Receive and verify signed notifications from the hosted checkout. Checkout notifications tell your shop when a session completes, is cancelled, or fails. They are delivered by the background job queue to the tenant's configured checkout webhook URL.",{"id":793,"title":794,"titles":795,"content":796,"level":185},"\u002Fapi\u002Fwebhooks#configure-delivery","Configure delivery",[92],"Configure the destination with PUT \u002Fapi\u002Fshop\u002Fcheckout-webhook using an admin session with settings write permission: {\n  \"webhook_url\": \"https:\u002F\u002Fshop.example.com\u002Fwebhooks\u002Fsellforge\"\n} Set webhook_url to null to disable delivery. GET \u002Fapi\u002Fshop\u002Fcheckout-webhook returns webhook_url and webhook_secret_set; it does not reveal the secret or offer secret rotation. Arrange backend access to the tenant's existing webhook secret through your platform operator's configuration process before verifying events. The underlying settings are tenant_\u003Cid>.checkout.webhook_url and tenant_\u003Cid>.checkout.webhook_secret; session creation generates a secret when one is absent. Delivery requires a public HTTPS URL reachable by the server. The delivery worker does not follow redirects.",{"id":798,"title":799,"titles":800,"content":801,"level":185},"\u002Fapi\u002Fwebhooks#events","Events",[92],"Event Meaning checkout.completed Checkout completed successfully checkout.cancelled The checkout was cancelled checkout.failed The checkout failed, including expiry of an open session Example delivery: {\n  \"id\": \"cwh_example\",\n  \"type\": \"checkout.completed\",\n  \"created_at\": \"2026-10-17T12:00:00.000Z\",\n  \"data\": {\n    \"session_id\": \"cs_example\",\n    \"order_id\": \"ord_example\",\n    \"subscription_id\": null,\n    \"status\": \"paid\",\n    \"amount_cents\": 2490,\n    \"currency\": \"EUR\",\n    \"invoice_url\": null,\n    \"reason\": null\n  }\n} The request includes X-Sellforge-Event-Id and X-Sellforge-Signature headers. Address objects are not included in the event. After verifying it, associate data.session_id with your storefront purchase attempt and use data.order_id to retrieve the final order. For physical delivery, use its final shipping_address: the customer may have changed a prefilled address or selected another destination in a wallet. Make fulfillment idempotent by order ID as well as deduplicating delivery event IDs. For a subscription checkout, subscription_id identifies the subscription; order_id and amount_cents can be null during a free trial. A successful subscription checkout therefore does not always represent a charged order. A non-null invoice_url is a signed customer download link; see invoice downloads.",{"id":803,"title":804,"titles":805,"content":806,"level":185},"\u002Fapi\u002Fwebhooks#subscription-events","Subscription events",[92],"Digital subscriptions use the same destination, signing secret, and headers: Event Meaning abo.aktiv Subscription became active or entered its trial abo.bezahlt A subscription invoice was paid; includes its order_id abo.gekuendigt Cancellation was scheduled for the end of the period abo.fortgesetzt Scheduled cancellation was withdrawn abo.beendet Subscription ended abo.zahlung_fehlgeschlagen A subscription invoice payment failed These events use data.subscription_id and data.checkout_session_id, rather than checkout events' data.session_id. Other fields include plan_id, product_id, customer_email, customer_id, status, is_test, current_period_end, trial_end, cancel_at_period_end, ended_at, and optional\u002Fnull order_id. Grant or extend access according to the verified subscription state and period. abo.gekuendigt alone does not mean access has already ended. Keep test subscriptions out of real fulfillment. Event data reflects the stored subscription when the delivery job runs; delayed deliveries can describe a newer state than the original transition.",{"id":808,"title":809,"titles":810,"content":811,"level":185},"\u002Fapi\u002Fwebhooks#verify-the-signature","Verify the signature",[92],"The signature header has this format: t=\u003Cunix-timestamp>,v1=\u003Chex-hmac> The HMAC uses SHA-256, the checkout webhook secret, and the exact string timestamp.rawBody. Preserve the original request bytes before parsing JSON. The following Node.js example also applies a five-minute replay window. That window is a receiver-side choice: import { createHmac, timingSafeEqual } from 'node:crypto'\n\nexport function verifyCheckoutWebhook(rawBody, signature, secret) {\n  if (typeof signature !== 'string' || !secret) return false\n  const match = \u002F^t=(\\d+),v1=([a-f0-9]{64})$\u002F.exec(signature)\n  if (!match) return false\n\n  const [, timestamp, digest] = match\n  const age = Math.abs(Date.now() \u002F 1000 - Number(timestamp))\n  if (!Number.isFinite(age) || age > 300) return false\n\n  const expected = createHmac('sha256', secret)\n    .update(`${timestamp}.`)\n    .update(rawBody)\n    .digest()\n  const received = Buffer.from(digest, 'hex')\n  return received.length === expected.length\n    && timingSafeEqual(received, expected)\n}",{"id":813,"title":814,"titles":815,"content":816,"level":185},"\u002Fapi\u002Fwebhooks#handle-retries","Handle retries",[92],"New checkout and subscription delivery jobs allow up to five attempts. The event ID remains the same between retries; the delivery timestamp, signature, and state fields can change. Persist the event ID alongside your business action and make repeated deliveries safe to acknowledge. Return a successful HTTP response after accepting the event. A non-success response causes the job handler to fail and enter the queue's retry handling.",{"id":818,"title":819,"titles":820,"content":821,"level":185},"\u002Fapi\u002Fwebhooks#other-event-mechanisms","Other event mechanisms",[92],"These HTTP notifications are different from the in-process plugin hook bus. Incoming Stripe and shipping-provider callbacks are handled by the webhooks backend and follow those providers' verification rules. html pre.shiki code .sgsFI, html code.shiki .sgsFI{--shiki-default:#24292E}html pre.shiki code .sYu0t, html code.shiki .sYu0t{--shiki-default:#005CC5}html pre.shiki code .sYBdl, html code.shiki .sYBdl{--shiki-default:#032F62}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sD7c4, html code.shiki .sD7c4{--shiki-default:#D73A49}html pre.shiki code .s7eDp, html code.shiki .s7eDp{--shiki-default:#6F42C1}html pre.shiki code .sqxcx, html code.shiki .sqxcx{--shiki-default:#E36209}",{"id":5,"title":6,"titles":823,"content":824,"level":179},[],"Understand the platform and choose the right way to build with Sellforge. Sellforge brings product catalogs, orders, inventory, checkout, and invoicing into a shared commerce platform. These guides cover two ways to integrate: calling the HTTP API and extending the kernel with a plugin.",{"id":826,"title":827,"titles":828,"content":829,"level":185},"\u002Fgetting-started\u002Fintroduction#choose-your-integration","Choose your integration",[6],"Your goal Start here Read products or orders from another application API quickstart Build a customer-facing storefront Storefront API Send customers to a hosted checkout Checkout sessions Sell digital products or recurring access Digital products and subscriptions Configure storefront domains and channel prices Sales channels Measure storefront traffic and campaign attribution Insights React to order events inside Sellforge Plugin quickstart",{"id":831,"title":832,"titles":833,"content":834,"level":185},"\u002Fgetting-started\u002Fintroduction#shops-and-sales-channels","Shops and sales channels",[6],"A tenant represents a shop and defines its data boundary. A sales channel is a selling context inside that shop, with its own product listings and configuration. A channel does not grant access to another tenant. Administrative requests inherit the permissions of their authenticated user. Storefront requests identify the shop with a publishable key. See Authentication before choosing credentials.",{"id":836,"title":837,"titles":838,"content":839,"level":185},"\u002Fgetting-started\u002Fintroduction#how-the-backends-fit-together","How the backends fit together",[6],"The kernel can run as one combined API or as several route-restricted backend processes. Sellforge's current production setup uses a shared Core API per environment, with frontend proxies. The optional split configuration defines these surfaces: Backend Purpose Shop admin Merchant operations, products, orders, and settings Shop Public catalog, cart, and customer-facing features Checkout Checkout sessions and the payment flow Webhooks Incoming provider callbacks Platform admin Platform-level tenant administration Use the base URL of the backend serving your integration. A route that exists in the kernel can return 404 on a backend that does not expose it.",{"id":841,"title":842,"titles":843,"content":844,"level":185},"\u002Fgetting-started\u002Fintroduction#about-this-reference","About this reference",[6],"These guides describe the checked-in kernel at commit c2e229b, reviewed on October 19, 2026. Examples use environment variables for deployment-specific URLs and credentials. Sample IDs are illustrative: replace them with IDs returned by your own shop. The reference documents the integration paths listed in the navigation. Endpoint behavior is described per resource; do not assume that every legacy route follows one universal response format.",{"id":846,"title":847,"titles":848,"content":849,"level":185},"\u002Fgetting-started\u002Fintroduction#next-step","Next step",[6],"Follow the quickstart to make a read-only request and inspect a real response from your shop.",{"id":18,"title":19,"titles":851,"content":852,"level":179},[],"Find the former reference topics and understand changes from the legacy documentation. The old single-page API and Plugin SDK reference has been reorganized into Markdown guides. The \u002Fapi and \u002Fplugins entry routes translate the former section fragments into their new destinations.",{"id":854,"title":855,"titles":856,"content":857,"level":185},"\u002Fgetting-started\u002Fmigration-notes#find-an-existing-topic","Find an existing topic",[19],"Former topics New guide Categories, tax, discounts, suppliers, vehicle fitment Catalog resources Warehouses, inventory, fulfillment, returns, transfers, production, procurement, shipping Inventory and fulfillment Invoices, credit notes, SevDesk, vendors, affiliates Finance and partners CSV and image uploads Data and media CMS pages, subscribers, campaigns, custom emails Content and email Settings, credentials, jobs, audit, users Administration",{"id":859,"title":860,"titles":861,"content":862,"level":185},"\u002Fgetting-started\u002Fmigration-notes#api-changes-and-corrections","API changes and corrections",[19],"Administrative list pagination now defaults to 50 with a maximum of 500 on endpoints using the shared helper. Individual endpoints can have different response envelopes and limits. Authentication distinguishes browser cookie sessions, session JWTs, scoped sfk_ tokens, and public pk_sf_ keys. Browser administration uses central sign-in, tenant selection, and a second factor. A token scope is an explicit allowlist, not access to every route in that resource area. Deployments can split the shared kernel across several API hosts. Use the backend overview to select the correct host. Some legacy registered routes are not exposed by the split backend definitions; the relevant guides identify those cases.",{"id":864,"title":865,"titles":866,"content":867,"level":185},"\u002Fgetting-started\u002Fmigration-notes#october-19-source-review","October 19 source review",[19],"The reference now covers digital products and subscriptions, channel configuration and prices, custom product fields, Newsletter Studio, and Insights. Existing integrations should also check the digital-product list filter, the separate merged customer overview, signed customer invoice links, subscription webhook events, and the distinction between catalog base prices and checkout totals. The public newsletter routes are exposed by the current shop\u002Fshop-admin split backends.",{"id":869,"title":870,"titles":871,"content":872,"level":185},"\u002Fgetting-started\u002Fmigration-notes#sdks","SDKs",[19],"The HTTP examples use curl and standard request semantics. The legacy page's client-package and scaffolding claims have not been verified against a published SDK release and are not prerequisites for using the API. For plugins, start from the working JavaScript package shape. TypeScript consumers should use the plugin SDK version supplied with their kernel environment and check its types against the running implementation.",{"id":874,"title":875,"titles":876,"content":877,"level":185},"\u002Fgetting-started\u002Fmigration-notes#plugin-distribution-and-compatibility","Plugin distribution and compatibility",[19],"The current installer registers packages already resolvable by Node.js. It does not download packages, enforce a coreVersion range, or automatically provision admin pages and HTTP routes from metadata. Before upgrading a package, check its registration, settings, migrations, and hook usage against the target kernel. Keep applied migration files unchanged and test lifecycle behavior in a development runtime.",{"id":879,"title":880,"titles":881,"content":882,"level":185},"\u002Fgetting-started\u002Fmigration-notes#billing-and-partner-programs","Billing and partner programs",[19],"Marketplace distribution, plugin billing, certification, and commercial partner terms are separate from the runtime contract documented here. Contact the Sellforge team for the current publishing and commercial process.",{"id":884,"title":885,"titles":886,"content":887,"level":185},"\u002Fgetting-started\u002Fmigration-notes#changelog","Changelog",[19],"The historical version labels on the old HTML reference are not a versioned API compatibility guarantee. This documentation records a source-review date and kernel baseline instead. API versioning explains the current URL convention.",{"id":889,"title":890,"titles":891,"content":892,"level":185},"\u002Fgetting-started\u002Fmigration-notes#support","Support",[19],"For a documentation correction, include the page URL, your kernel version, and the relevant request\u002Fresponse with credentials removed. Email office@sellforge.ai.",{"id":12,"title":13,"titles":894,"content":895,"level":179},[],"Make your first authenticated request to the Sellforge product API. This walkthrough reads your product catalog without changing shop data.",{"id":897,"title":898,"titles":899,"content":900,"level":185},"\u002Fgetting-started\u002Fquickstart#before-you-begin","Before you begin",[13],"You need access to a Sellforge shop, its shop-admin API URL, and an API token with the catalog:read scope. Create the token from the shop admin's API token settings. Its plaintext value is shown once. For a local combined kernel, the usual base URL is http:\u002F\u002Flocalhost:9001. The optional split shop-admin wrapper uses port 9201. Use your deployment's configured public API URL when connecting to a hosted environment.",{"id":902,"title":903,"titles":904,"content":905,"level":185},"\u002Fgetting-started\u002Fquickstart#configure-your-environment","Configure your environment",[13],"Set the base URL in your terminal and read your token without including it in shell history: export SELLFORGE_ADMIN_URL='http:\u002F\u002Flocalhost:9001'\nread -r -s -p 'API token: ' SELLFORGE_API_TOKEN\nexport SELLFORGE_API_TOKEN The secret token starts with sfk_. This is different from the pk_sf_ publishable key used by storefronts.",{"id":907,"title":908,"titles":909,"content":910,"level":185},"\u002Fgetting-started\u002Fquickstart#make-a-request","Make a request",[13],"curl --fail-with-body \\\n  \"$SELLFORGE_ADMIN_URL\u002Fapi\u002Fadmin\u002Fproducts?limit=10\" \\\n  -H \"Authorization: Bearer $SELLFORGE_API_TOKEN\" The response contains a products array and pagination metadata. For an empty catalog, a valid response is: {\n  \"products\": [],\n  \"count\": 0,\n  \"limit\": 10,\n  \"offset\": 0\n}",{"id":912,"title":913,"titles":914,"content":915,"level":185},"\u002Fgetting-started\u002Fquickstart#fetch-the-next-page","Fetch the next page",[13],"Increase offset by the page size to retrieve the next page: curl --fail-with-body \\\n  \"$SELLFORGE_ADMIN_URL\u002Fapi\u002Fadmin\u002Fproducts?limit=10&offset=10\" \\\n  -H \"Authorization: Bearer $SELLFORGE_API_TOKEN\" Read the resource's pagination notes before implementing a complete export. Channel-restricted users can receive fewer product entries than the requested limit. For an export including digital products, add digital=alle to the product-list query. When Digital E-Commerce is active, the unfiltered administrative list otherwise defaults to physical products.",{"id":917,"title":918,"titles":919,"content":920,"level":185},"\u002Fgetting-started\u002Fquickstart#troubleshoot-a-request","Troubleshoot a request",[13],"Response Check 401 The token is present, valid, and not expired or revoked 403 The token includes catalog:read and its creator still has the required permissions 404 You are calling the shop-admin backend and the resource belongs to your shop Empty products Your shop has visible products matching the supplied filters Continue with Products for filters and product fields, or Authentication for the different credential types. html pre.shiki code .sD7c4, html code.shiki .sD7c4{--shiki-default:#D73A49}html pre.shiki code .sgsFI, html code.shiki .sgsFI{--shiki-default:#24292E}html pre.shiki code .sYBdl, html code.shiki .sYBdl{--shiki-default:#032F62}html pre.shiki code .sYu0t, html code.shiki .sYu0t{--shiki-default:#005CC5}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .s7eDp, html code.shiki .s7eDp{--shiki-default:#6F42C1}",{"id":158,"title":159,"titles":922,"content":160,"level":179},[],{"id":924,"title":925,"titles":926,"content":927,"level":185},"\u002Fplugins\u002Fcontext#context-overview","Context overview",[159],"The loader creates a context for each installed plugin and passes it to register(ctx). Property Purpose name, version Installed package identity db Shared Drizzle database instance coreSchema Selected core table definitions id(prefix) Generate an ID using the kernel helper hooks.on() Subscribe to lifecycle events settings Read and write settings audit.log() Record audit data jobs Enqueue jobs and inspect queued work storage Invoice\u002Fmedia upload and download helpers scheduler.every() Register an in-process periodic task logger Plugin-prefixed debug, info, warning, and error logging errors HTTP error helpers auth Password hashing and verification helpers inventory Inventory service helpers csv CSV parsing and serialization",{"id":929,"title":930,"titles":931,"content":932,"level":185},"\u002Fplugins\u002Fcontext#read-settings-and-log-an-event","Read settings and log an event",[159],"const label = (await ctx.settings.get('hello.label')) ?? 'Hello'\nctx.logger.info('Plugin registered', { label, version: ctx.version }) Namespace your keys explicitly. The shared settings service does not automatically prepend your package name.",{"id":934,"title":935,"titles":936,"content":937,"level":185},"\u002Fplugins\u002Fcontext#schedule-periodic-work","Schedule periodic work",[159],"ctx.scheduler.every(60_000, async () => {\n  ctx.logger.debug('Periodic task running')\n}, { label: 'heartbeat' }) The scheduler manages intervals and clears them when the plugin is unloaded. It logs exceptions from individual ticks. Work can overlap if a callback takes longer than the interval; coordinate long-running or non-repeatable operations accordingly. For durable tasks, ctx.jobs.enqueue(type, payload, options) uses the core job queue. A corresponding job handler must be registered by the runtime integration. The context does not expose a method for registering arbitrary handler types.",{"id":939,"title":940,"titles":941,"content":942,"level":185},"\u002Fplugins\u002Fcontext#database-and-storage","Database and storage",[159],"ctx.db is the shared database instance, not an automatically tenant-filtered or plugin-schema-scoped client. Use explicit table\u002Fschema definitions and preserve tenant ownership in queries. Storage helpers derive their tenant context from the installed plugin record. The context provides uploadInvoicePdf, uploadMedia, and getPresignedDownloadUrl.",{"id":944,"title":945,"titles":946,"content":947,"level":185},"\u002Fplugins\u002Fcontext#current-limitations","Current limitations",[159],"ctx.inventory.getStock() is currently a stub that returns 0; it must not be used as an actual stock reading. The factory provides inventory.move and inventory.defaultWarehouseId separately. There is no Fastify instance, HTTP route registrar, or UI extension registrar on the current context. Plan those integrations against explicit runtime support rather than adding undeclared context methods. html pre.shiki code .sD7c4, html code.shiki .sD7c4{--shiki-default:#D73A49}html pre.shiki code .sYu0t, html code.shiki .sYu0t{--shiki-default:#005CC5}html pre.shiki code .sgsFI, html code.shiki .sgsFI{--shiki-default:#24292E}html pre.shiki code .s7eDp, html code.shiki .s7eDp{--shiki-default:#6F42C1}html pre.shiki code .sYBdl, html code.shiki .sYBdl{--shiki-default:#032F62}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"id":152,"title":153,"titles":949,"content":154,"level":179},[],{"id":951,"title":952,"titles":953,"content":954,"level":185},"\u002Fplugins\u002Fhooks#register-a-handler","Register a handler",[153],"Subscribe during your plugin's register(ctx) call: ctx.hooks.on('order.paid', async (event) => {\n  ctx.logger.info('Order payment received', {\n    orderId: event.orderId,\n    amountCents: event.amountCents,\n  })\n})",{"id":956,"title":957,"titles":958,"content":959,"level":185},"\u002Fplugins\u002Fhooks#order-events","Order events",[153],"Event Payload fields order.placed orderId, channelId, channel order.paid orderId, amountCents, channelId, channel order.cancelled orderId, channelId, channel order.refunded orderId, amountCents, channelId, channel channelId and channel can be null for legacy or channel-less orders. Monetary amounts are integer cents. The hooks above are emitted at specific lifecycle points in the kernel. They are not a change feed for every direct database update or every administrative mutation. In particular, order.placed is emitted by POST \u002Fapi\u002Fstore\u002Fcart\u002F:id\u002Fplace-order. Hosted session order creation does not emit that hook. order.paid is emitted by the shared paid-order finalizer used by hosted checkout for non-test orders; its test-payment path suppresses this hook. Subscription lifecycle delivery uses the separate subscription webhooks. Choose the event path that actually covers your purchase flow.",{"id":961,"title":962,"titles":963,"content":964,"level":185},"\u002Fplugins\u002Fhooks#execution-behavior","Execution behavior",[153],"Handlers for an event run concurrently. The bus waits for them, catches individual failures, and logs the failing plugin. One failing handler does not prevent the other handlers from running. The hook bus is in-process and does not itself provide durable delivery or automatic retries. For slow external work, use the available job infrastructure and design the operation to tolerate repeated execution.",{"id":966,"title":967,"titles":968,"content":969,"level":185},"\u002Fplugins\u002Fhooks#plugin-isolation-and-cleanup","Plugin isolation and cleanup",[153],"Each subscription is associated with the package name. Unloading a plugin detaches that package's event handlers. Do not register handlers repeatedly from a timer or request callback. Plugins execute in the kernel process. When reading or writing shop data, preserve the relevant tenant and channel context; the fact that an event contains an order ID does not make unrelated shop data part of that event.",{"id":971,"title":972,"titles":973,"content":974,"level":185},"\u002Fplugins\u002Fhooks#http-delivery","HTTP delivery",[153],"If your integration runs outside the kernel, checkout webhooks provide signed HTTP delivery. They are separate from this hook bus and have different payloads and delivery semantics. html pre.shiki code .sgsFI, html code.shiki .sgsFI{--shiki-default:#24292E}html pre.shiki code .s7eDp, html code.shiki .s7eDp{--shiki-default:#6F42C1}html pre.shiki code .sYBdl, html code.shiki .sYBdl{--shiki-default:#032F62}html pre.shiki code .sD7c4, html code.shiki .sD7c4{--shiki-default:#D73A49}html pre.shiki code .sqxcx, html code.shiki .sqxcx{--shiki-default:#E36209}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"id":170,"title":171,"titles":976,"content":172,"level":179},[],{"id":978,"title":979,"titles":980,"content":981,"level":185},"\u002Fplugins\u002Finstallation#prepare-the-runtime","Prepare the runtime",[171],"The package must already be available to Node.js from the kernel environment. The install endpoint reads its manifest and creates the database records; it does not run npm install. The management endpoints are registered on the platform or combined service path when plugin runtime registration is enabled. Use a backend exposing \u002Fapi\u002Fadmin\u002Fplugins and a session with platform-management context for mutations. An ordinary shop administrator cannot install or enable platform-wide code.",{"id":983,"title":984,"titles":985,"content":986,"level":185},"\u002Fplugins\u002Finstallation#install-a-package","Install a package",[171],"POST \u002Fapi\u002Fadmin\u002Fplugins\u002Finstall curl \"$SELLFORGE_ADMIN_URL\u002Fapi\u002Fadmin\u002Fplugins\u002Finstall\" \\\n  -H \"Authorization: Bearer $SELLFORGE_PLATFORM_SESSION\" \\\n  -H 'Content-Type: application\u002Fjson' \\\n  -d '{\"packageName\":\"@sellforge-plugin\u002Fhello\"}' Installation reads the manifest, creates the installed record and plugin schema, and runs migrations. The plugin initially remains disabled. The body can also contain tenantId for installed-record context. This does not turn plugin activation into a merchant-level operation.",{"id":988,"title":989,"titles":990,"content":991,"level":185},"\u002Fplugins\u002Finstallation#enable-and-disable","Enable and disable",[171],"Encode a scoped package name when using it as a route parameter: curl -X POST \\\n  \"$SELLFORGE_ADMIN_URL\u002Fapi\u002Fadmin\u002Fplugins\u002F%40sellforge-plugin%2Fhello\u002Fenable\" \\\n  -H \"Authorization: Bearer $SELLFORGE_PLATFORM_SESSION\" Enabling loads the plugin into the running process. The equivalent \u002Fdisable endpoint updates the installed state, detaches hooks, disposes managed intervals, and removes registered services in that process.",{"id":993,"title":994,"titles":995,"content":996,"level":185},"\u002Fplugins\u002Finstallation#inspect-status","Inspect status",[171],"GET \u002Fapi\u002Fadmin\u002Fplugins returns a plugins array. Records include installed metadata, loaded, and last_error. loaded reflects the current process's runtime registry. GET \u002Fapi\u002Fadmin\u002Fplugins\u002F:name returns one record in plugin. The \u002Fsettings subresource exposes declared settings to platform context, masking secret values.",{"id":998,"title":999,"titles":1000,"content":1001,"level":185},"\u002Fplugins\u002Finstallation#startup-and-failures","Startup and failures",[171],"At boot, enabled plugins are loaded from the database. If startup registration throws, the loader records the error and disables that plugin while allowing the application to continue booting. Hot-enabling uses a separate code path and returns an error when loading fails. Inspect both the installed record and the process logs after a failed enable request.",{"id":1003,"title":1004,"titles":1005,"content":1006,"level":185},"\u002Fplugins\u002Finstallation#uninstall","Uninstall",[171],"DELETE \u002Fapi\u002Fadmin\u002Fplugins\u002F:name unloads the plugin and removes its installed record. Data is preserved by default. Use ?drop_data=true only when the plugin schema and migration history should also be removed. In deployments with multiple processes, coordinate lifecycle changes across all processes that load the plugin. html pre.shiki code .s7eDp, html code.shiki .s7eDp{--shiki-default:#6F42C1}html pre.shiki code .sYBdl, html code.shiki .sYBdl{--shiki-default:#032F62}html pre.shiki code .sgsFI, html code.shiki .sgsFI{--shiki-default:#24292E}html pre.shiki code .sYu0t, html code.shiki .sYu0t{--shiki-default:#005CC5}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"id":133,"title":134,"titles":1008,"content":1009,"level":179},[],"Understand how plugins extend the Sellforge kernel. A Sellforge plugin is a Node.js package loaded into the kernel process. It exports a registration function and can use the supplied context to subscribe to events, read settings, interact with core services, and register periodic work.",{"id":1011,"title":1012,"titles":1013,"content":1014,"level":185},"\u002Fplugins\u002Fintroduction#when-to-use-a-plugin","When to use a plugin",[134],"Use a plugin for behavior that needs to run inside the kernel, such as reacting to an order lifecycle event. Use the HTTP API for an external application that needs commerce data.",{"id":1016,"title":1017,"titles":1018,"content":1019,"level":185},"\u002Fplugins\u002Fintroduction#runtime-model","Runtime model",[134],"The loader reads enabled records from installed_plugins, runs their migrations, imports their packages, and invokes register(ctx). Returned services are stored in the runtime registry. Plugins currently run platform-wide in a process. Installation, enable\u002Fdisable actions, and settings changes are restricted to platform context. A merchant administrator can inspect installed plugin status but cannot independently activate platform code.",{"id":1021,"title":1022,"titles":1023,"content":1024,"level":185},"\u002Fplugins\u002Fintroduction#available-building-blocks","Available building blocks",[134],"Registration and lifecycle Package manifest Order lifecycle hooks Context services SQL migrations Installation and operations",{"id":1026,"title":1027,"titles":1028,"content":1029,"level":185},"\u002Fplugins\u002Fintroduction#implementation-boundary","Implementation boundary",[134],"The current context does not expose a Fastify application or an automatic admin-page registration API. A manifest field alone does not add an HTTP route or a page to the new shop admin. This guide documents the runtime available in the kernel. Package publishing, marketplace distribution, and storefront UI extension points need their own supported integration before they can be used.",{"id":1031,"title":1032,"titles":1033,"content":1034,"level":185},"\u002Fplugins\u002Fintroduction#start-building","Start building",[134],"The quickstart creates a minimal package using the same default-export registration shape as the kernel's own test fixture.",{"id":146,"title":147,"titles":1036,"content":148,"level":179},[],{"id":1038,"title":1039,"titles":1040,"content":1041,"level":185},"\u002Fplugins\u002Fmanifest#required-package-structure","Required package structure",[147],"The installer resolves \u003CpackageName>\u002Fpackage.json and reads the sellforge block. That block must contain a displayName. The package must also provide a usable name, version, and import entry point. If you add a package exports map, keep .\u002Fpackage.json resolvable as well as the plugin's main entry point. {\n  \"name\": \"@sellforge-plugin\u002Fhello\",\n  \"version\": \"0.1.0\",\n  \"type\": \"module\",\n  \"main\": \".\u002Findex.js\",\n  \"files\": [\"index.js\", \"migrations\"],\n  \"sellforge\": {\n    \"displayName\": \"Hello Sellforge\",\n    \"description\": \"A minimal order-event extension.\",\n    \"settings\": {\n      \"namespace\": \"hello.\",\n      \"keys\": [\n        {\n          \"key\": \"hello.label\",\n          \"description\": \"Label attached to log messages\",\n          \"default\": \"Hello\",\n          \"required\": false,\n          \"secret\": false\n        }\n      ]\n    }\n  }\n}",{"id":1043,"title":1044,"titles":1045,"content":1046,"level":185},"\u002Fplugins\u002Fmanifest#settings-metadata","Settings metadata",[147],"The settings endpoint reads sellforge.settings.namespace and sellforge.settings.keys. Field Purpose namespace Prefix used to fetch the plugin's settings key Full settings key, including your prefix description Human-readable explanation default Default metadata returned by the settings endpoint required Required-field metadata secret Controls masking in the plugin settings response The context's settings.get() reads stored values. Apply a default explicitly in your code when a key has not been saved: const label = (await ctx.settings.get('hello.label')) ?? 'Hello'",{"id":1048,"title":1049,"titles":1050,"content":1051,"level":185},"\u002Fplugins\u002Fmanifest#what-metadata-does-not-do","What metadata does not do",[147],"The manifest is stored with the installed record. Additional fields such as hooks, category, or coreVersion may describe a package, but the current loader registers behavior through executable code. Declare event handlers with ctx.hooks.on(). The installer does not enforce a coreVersion compatibility range, and the runtime does not automatically install an admin page or HTTP route based on manifest metadata.",{"id":1053,"title":1054,"titles":1055,"content":1056,"level":185},"\u002Fplugins\u002Fmanifest#migration-location","Migration location",[147],"SQL migrations are discovered in the package's root migrations\u002F directory. Include that directory in the published package. See Migrations. html pre.shiki code .sgsFI, html code.shiki .sgsFI{--shiki-default:#24292E}html pre.shiki code .sYu0t, html code.shiki .sYu0t{--shiki-default:#005CC5}html pre.shiki code .sYBdl, html code.shiki .sYBdl{--shiki-default:#032F62}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sD7c4, html code.shiki .sD7c4{--shiki-default:#D73A49}html pre.shiki code .s7eDp, html code.shiki .s7eDp{--shiki-default:#6F42C1}",{"id":164,"title":165,"titles":1058,"content":166,"level":179},[],{"id":1060,"title":1061,"titles":1062,"content":1063,"level":185},"\u002Fplugins\u002Fmigrations#migration-files","Migration files",[165],"Place SQL files in the root migrations\u002F directory of your package: hello-plugin\u002F\n  package.json\n  index.js\n  migrations\u002F\n    0001_create_events.sql\n    0002_add_processed_at.sql Files are applied in alphabetical order. Use zero-padded numeric prefixes and ensure the directory is included in the package's published files.",{"id":1065,"title":1066,"titles":1067,"content":1068,"level":185},"\u002Fplugins\u002Fmigrations#plugin-schema","Plugin schema",[165],"The installer derives a schema from the package's final name segment. For example, @sellforge-plugin\u002Fhello maps to plugin_hello. Different scopes with the same final package name produce the same schema name, so choose a unique slug within the deployment. Each migration runs in its own transaction with a local search path set to the plugin schema followed by public. CREATE TABLE IF NOT EXISTS processed_orders (\n  order_id text PRIMARY KEY,\n  processed_at timestamptz NOT NULL DEFAULT now()\n); The table above is created in the plugin schema. This local migration search path does not automatically apply to later ctx.db queries.",{"id":1070,"title":1071,"titles":1072,"content":1073,"level":185},"\u002Fplugins\u002Fmigrations#migration-history","Migration history",[165],"The runner records each file's name and SHA-256 checksum in plugin_migrations. It skips previously applied files with the same checksum and rejects a changed file that has already run. Create a new migration when changing an installed schema. Preserve earlier migration files unchanged.",{"id":1075,"title":1076,"titles":1077,"content":1078,"level":185},"\u002Fplugins\u002Fmigrations#sql-restrictions","SQL restrictions",[165],"The validator rejects statements including DROP SCHEMA, changing search_path, grants\u002Frevokes, and user, role, or database creation\u002Falteration. It also rejects recognized writes against public or core tables. Keep migration changes within the plugin's own schema. The validator is a set of checks, not a general-purpose SQL sandbox.",{"id":1080,"title":1081,"titles":1082,"content":1083,"level":185},"\u002Fplugins\u002Fmigrations#uninstall-behavior","Uninstall behavior",[165],"Uninstalling normally removes the installed record while preserving plugin data. The explicit drop_data=true option drops the plugin schema and its migration history. See Installation. html pre.shiki code .sD7c4, html code.shiki .sD7c4{--shiki-default:#D73A49}html pre.shiki code .s7eDp, html code.shiki .s7eDp{--shiki-default:#6F42C1}html pre.shiki code .sgsFI, html code.shiki .sgsFI{--shiki-default:#24292E}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"id":140,"title":141,"titles":1085,"content":1086,"level":179},[],"Create a minimal plugin that listens for a new order. This example uses plain JavaScript modules and the runtime context supplied by the kernel. It does not require a scaffolding CLI.",{"id":1088,"title":1089,"titles":1090,"content":1091,"level":185},"\u002Fplugins\u002Fquickstart#create-the-package","Create the package",[141],"Create a directory with these files: hello-plugin\u002F\n  package.json\n  index.js Use this package.json: {\n  \"name\": \"@sellforge-plugin\u002Fhello\",\n  \"version\": \"0.1.0\",\n  \"type\": \"module\",\n  \"main\": \".\u002Findex.js\",\n  \"files\": [\"index.js\", \"migrations\"],\n  \"sellforge\": {\n    \"displayName\": \"Hello Sellforge\",\n    \"description\": \"Logs newly placed orders.\"\n  }\n}",{"id":1093,"title":1094,"titles":1095,"content":1096,"level":185},"\u002Fplugins\u002Fquickstart#register-an-event-handler","Register an event handler",[141],"Create index.js: export default {\n  name: '@sellforge-plugin\u002Fhello',\n  async register(ctx) {\n    ctx.hooks.on('order.placed', async ({ orderId }) => {\n      ctx.logger.info('A new order was placed', { orderId })\n    })\n\n    return {\n      ping() {\n        return 'pong'\n      },\n    }\n  },\n} The loader supplies ctx. The returned ping service is available in the internal registry; it is not automatically exposed as an HTTP endpoint. This example demonstrates registration, not a universal order feed: the current order.placed emitter belongs to the direct cart place-order flow. For hosted purchases, use the paid lifecycle or signed checkout notifications as described under hook coverage.",{"id":1098,"title":1099,"titles":1100,"content":1101,"level":185},"\u002Fplugins\u002Fquickstart#test-the-registration-locally","Test the registration locally",[141],"Create test.mjs alongside index.js and run it with node test.mjs: import assert from 'node:assert\u002Fstrict'\nimport plugin from '.\u002Findex.js'\n\nconst handlers = new Map()\nconst messages = []\nconst services = await plugin.register({\n  hooks: { on: (name, handler) => handlers.set(name, handler) },\n  logger: { info: (message, meta) => messages.push({ message, meta }) },\n})\n\nassert.equal(services.ping(), 'pong')\nassert.ok(handlers.has('order.placed'))\nawait handlers.get('order.placed')({ orderId: 'ord_test' })\nassert.equal(messages[0].meta.orderId, 'ord_test')\nconsole.log('Plugin registration and handler passed.')",{"id":1103,"title":1104,"titles":1105,"content":1106,"level":185},"\u002Fplugins\u002Fquickstart#load-it-in-a-development-kernel","Load it in a development kernel",[141],"Make the package resolvable from the kernel's Node.js environment, for example by adding a local package dependency to a development kernel. The install endpoint registers an already available package; it does not download one from npm. Then use the platform-authorized installation flow to install and enable it. Test against a development shop before enabling the package in another environment.",{"id":1108,"title":1109,"titles":1110,"content":1111,"level":185},"\u002Fplugins\u002Fquickstart#next-steps","Next steps",[141],"Add configuration through the manifest, learn the hook payloads, or create plugin-owned tables with migrations. html pre.shiki code .sgsFI, html code.shiki .sgsFI{--shiki-default:#24292E}html pre.shiki code .sYu0t, html code.shiki .sYu0t{--shiki-default:#005CC5}html pre.shiki code .sYBdl, html code.shiki .sYBdl{--shiki-default:#032F62}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sD7c4, html code.shiki .sD7c4{--shiki-default:#D73A49}html pre.shiki code .s7eDp, html code.shiki .s7eDp{--shiki-default:#6F42C1}html pre.shiki code .sqxcx, html code.shiki .sqxcx{--shiki-default:#E36209}",{"id":1113,"title":86,"body":1114,"description":87,"docId":89,"extension":1910,"group":27,"meta":1911,"navigation":1912,"order":88,"path":85,"reviewed":1913,"seo":1914,"sources":1915,"stem":1925,"__hash__":1926},"docs\u002Fapi\u002Fdigital-and-subscriptions.md",{"type":1115,"value":1116,"toc":1902},"minimark",[1117,1139,1154,1158,1191,1194,1217,1232,1239,1242,1245,1323,1326,1440,1476,1496,1499,1520,1615,1618,1647,1652,1655,1692,1705,1708,1812,1865,1885,1898],[1118,1119,1120,1121,1125,1126,1129,1130,1133,1134,1138],"p",{},"Digital E-Commerce must be available to the shop and activated. Administrative ",[1122,1123,1124],"code",{},"\u002Fapi\u002Fadmin\u002Fdigital\u002F*"," routes require an admin session; existing ",[1122,1127,1128],{},"catalog:read","\u002F",[1122,1131,1132],{},"catalog:write"," scopes do not grant access to these add-on routes. Product creation itself still uses the ",[1135,1136,1137],"a",{"href":43},"product API",".",[1118,1140,1141,1142,1145,1146,1149,1150,1153],{},"The current API deliberately uses German route and response names such as ",[1122,1143,1144],{},"plaene",", ",[1122,1147,1148],{},"abos",", and ",[1122,1151,1152],{},"kuendigen",". Keep those identifiers unchanged in requests.",[1155,1156,503],"h2",{"id":1157},"check-activation",[1118,1159,1160,1163,1164,1167,1168,1171,1172,1149,1175,1178,1179,1181,1182,1145,1185,1149,1188,1138],{},[1122,1161,1162],{},"GET \u002Fapi\u002Fadmin\u002Fdigital\u002Fstatus"," returns ",[1122,1165,1166],{},"erlaubt"," (available), ",[1122,1169,1170],{},"aktiv"," (enabled), ",[1122,1173,1174],{},"abo_provision",[1122,1176,1177],{},"abo_provision_monate",". PUT to the same path updates ",[1122,1180,1170],{}," and subscription commission settings when the add-on is available. The commission modes are ",[1122,1183,1184],{},"erste",[1122,1186,1187],{},"monate",[1122,1189,1190],{},"immer",[1155,1192,508],{"id":1193},"one-time-digital-purchases",[1118,1195,1196,1197,1200,1201,1204,1205,1208,1209,1212,1213,1216],{},"Create a normal product with ",[1122,1198,1199],{},"is_digital: true",". Digital products default to ",[1122,1202,1203],{},"track_stock: false"," on creation; specify ",[1122,1206,1207],{},"track_stock: true"," if supply is limited. Use ",[1122,1210,1211],{},"GET \u002Fapi\u002Fadmin\u002Fproducts?digital=ja"," to list them, or ",[1122,1214,1215],{},"digital=alle"," for the entire catalog.",[1118,1218,1219,1220,1223,1224,1227,1228,1231],{},"Sell a one-time digital item through ",[1122,1221,1222],{},"POST \u002Fapi\u002Fstore\u002Fcheckout-sessions",", using its product ID and ",[1122,1225,1226],{},"customer.billing_address",". Digital-only orders need no shipping address or shipping method and return ",[1122,1229,1230],{},"requires_shipping: false",". Mixed carts still need delivery details for physical goods.",[1118,1233,1234,1235,1238],{},"The hosted payment flow collects the digital-content acknowledgement; direct checkout calls use ",[1122,1236,1237],{},"widerruf_verzicht"," where required. Deliver the digital entitlement from verified payment\u002Forder information in your own application. Marking a product as digital does not upload a file or implement your storefront's access control.",[1155,1240,513],{"id":1241},"create-a-recurring-plan",[1118,1243,1244],{},"Use an existing digital product belonging to the shop:",[1246,1247,1248,1264],"table",{},[1249,1250,1251],"thead",{},[1252,1253,1254,1258,1261],"tr",{},[1255,1256,1257],"th",{},"Method",[1255,1259,1260],{},"Path",[1255,1262,1263],{},"Result",[1265,1266,1267,1285,1302],"tbody",{},[1252,1268,1269,1275,1280],{},[1270,1271,1272],"td",{},[1122,1273,1274],{},"GET",[1270,1276,1277],{},[1122,1278,1279],{},"\u002Fapi\u002Fadmin\u002Fdigital\u002Fplaene",[1270,1281,1282,1284],{},[1122,1283,1144],{},", including product names and active counts",[1252,1286,1287,1292,1296],{},[1270,1288,1289],{},[1122,1290,1291],{},"POST",[1270,1293,1294],{},[1122,1295,1279],{},[1270,1297,1298,1299],{},"Created ",[1122,1300,1301],{},"plan",[1252,1303,1304,1309,1314],{},[1270,1305,1306],{},[1122,1307,1308],{},"PATCH",[1270,1310,1311],{},[1122,1312,1313],{},"\u002Fapi\u002Fadmin\u002Fdigital\u002Fplaene\u002F:id",[1270,1315,1316,1317,1319,1320],{},"Updated ",[1122,1318,1301],{}," and optional ",[1122,1321,1322],{},"hinweis",[1118,1324,1325],{},"Example create body:",[1327,1328,1333],"pre",{"className":1329,"code":1330,"language":1331,"meta":1332,"style":1332},"language-json shiki shiki-themes github-light","{\n  \"product_id\": \"prod_digital_example\",\n  \"name\": \"Monthly access\",\n  \"price_cents\": 1490,\n  \"interval_unit\": \"month\",\n  \"interval_count\": 1,\n  \"trial_days\": 7,\n  \"is_active\": true\n}\n","json","",[1122,1334,1335,1343,1359,1371,1384,1397,1410,1423,1434],{"__ignoreMap":1332},[1336,1337,1339],"span",{"class":1338,"line":179},"line",[1336,1340,1342],{"class":1341},"sgsFI","{\n",[1336,1344,1345,1349,1352,1356],{"class":1338,"line":185},[1336,1346,1348],{"class":1347},"sYu0t","  \"product_id\"",[1336,1350,1351],{"class":1341},": ",[1336,1353,1355],{"class":1354},"sYBdl","\"prod_digital_example\"",[1336,1357,1358],{"class":1341},",\n",[1336,1360,1361,1364,1366,1369],{"class":1338,"line":224},[1336,1362,1363],{"class":1347},"  \"name\"",[1336,1365,1351],{"class":1341},[1336,1367,1368],{"class":1354},"\"Monthly access\"",[1336,1370,1358],{"class":1341},[1336,1372,1374,1377,1379,1382],{"class":1338,"line":1373},4,[1336,1375,1376],{"class":1347},"  \"price_cents\"",[1336,1378,1351],{"class":1341},[1336,1380,1381],{"class":1347},"1490",[1336,1383,1358],{"class":1341},[1336,1385,1387,1390,1392,1395],{"class":1338,"line":1386},5,[1336,1388,1389],{"class":1347},"  \"interval_unit\"",[1336,1391,1351],{"class":1341},[1336,1393,1394],{"class":1354},"\"month\"",[1336,1396,1358],{"class":1341},[1336,1398,1400,1403,1405,1408],{"class":1338,"line":1399},6,[1336,1401,1402],{"class":1347},"  \"interval_count\"",[1336,1404,1351],{"class":1341},[1336,1406,1407],{"class":1347},"1",[1336,1409,1358],{"class":1341},[1336,1411,1413,1416,1418,1421],{"class":1338,"line":1412},7,[1336,1414,1415],{"class":1347},"  \"trial_days\"",[1336,1417,1351],{"class":1341},[1336,1419,1420],{"class":1347},"7",[1336,1422,1358],{"class":1341},[1336,1424,1426,1429,1431],{"class":1338,"line":1425},8,[1336,1427,1428],{"class":1347},"  \"is_active\"",[1336,1430,1351],{"class":1341},[1336,1432,1433],{"class":1347},"true\n",[1336,1435,1437],{"class":1338,"line":1436},9,[1336,1438,1439],{"class":1341},"}\n",[1118,1441,1442,1445,1446,1145,1449,1145,1452,1455,1456,1459,1460,1463,1464,1467,1468,1471,1472,1475],{},[1122,1443,1444],{},"price_cents"," must be 50–10,000,000. Intervals use ",[1122,1447,1448],{},"day",[1122,1450,1451],{},"week",[1122,1453,1454],{},"month",", or ",[1122,1457,1458],{},"year","; ",[1122,1461,1462],{},"interval_count"," defaults to 1 and must satisfy the provider's maximum interval. ",[1122,1465,1466],{},"trial_days"," defaults to 0 and can be up to 730. Optional ",[1122,1469,1470],{},"intro_discount_percent"," and ",[1122,1473,1474],{},"intro_discount_months"," must be supplied together (or both cleared).",[1118,1477,1478,1479,1145,1482,1145,1485,1149,1488,1491,1492,1495],{},"Responses include ",[1122,1480,1481],{},"currency",[1122,1483,1484],{},"interval_text",[1122,1486,1487],{},"first_charge_cents",[1122,1489,1490],{},"intro_price_cents",". The create schema does not accept a caller-selected currency. Read the returned values for display. Updating a plan's price\u002Finterval affects new subscriptions; existing provider subscriptions keep their price. A plan's product cannot be changed; disable it with ",[1122,1493,1494],{},"is_active: false"," when it should no longer be sold.",[1155,1497,518],{"id":1498},"create-a-subscription-checkout",[1118,1500,1501,1502,1508,1509,1512,1513,1516,1517,1138],{},"Use ",[1503,1504,1505],"strong",{},[1122,1506,1507],{},"POST \u002Fapi\u002Fstore\u002Fabo-sessions",", rather than adding a plan to the one-time session's ",[1122,1510,1511],{},"items"," array. This endpoint is served by the shop and checkout backends and uses the shop's ",[1122,1514,1515],{},"pk_sf_"," key with the same origin\u002Freturn-URL rules as ",[1135,1518,1519],{"href":325},"checkout sessions",[1327,1521,1525],{"className":1522,"code":1523,"language":1524,"meta":1332,"style":1332},"language-bash shiki shiki-themes github-light","curl --fail-with-body \"$SELLFORGE_CHECKOUT_URL\u002Fapi\u002Fstore\u002Fabo-sessions\" \\\n  -H \"Authorization: Bearer $SELLFORGE_PUBLIC_KEY\" \\\n  -H \"Origin: $STOREFRONT_ORIGIN\" \\\n  -H 'Content-Type: application\u002Fjson' \\\n  -d '{\n    \"plan_id\": \"spl_example\",\n    \"return_url\": \"https:\u002F\u002Fshop.example.com\u002Fcheckout\u002Fresult\",\n    \"customer_email\": \"alex@example.com\"\n  }'\n","bash",[1122,1526,1527,1548,1564,1578,1587,1595,1600,1605,1610],{"__ignoreMap":1332},[1336,1528,1529,1533,1536,1539,1542,1545],{"class":1338,"line":179},[1336,1530,1532],{"class":1531},"s7eDp","curl",[1336,1534,1535],{"class":1347}," --fail-with-body",[1336,1537,1538],{"class":1354}," \"",[1336,1540,1541],{"class":1341},"$SELLFORGE_CHECKOUT_URL",[1336,1543,1544],{"class":1354},"\u002Fapi\u002Fstore\u002Fabo-sessions\"",[1336,1546,1547],{"class":1347}," \\\n",[1336,1549,1550,1553,1556,1559,1562],{"class":1338,"line":185},[1336,1551,1552],{"class":1347},"  -H",[1336,1554,1555],{"class":1354}," \"Authorization: Bearer ",[1336,1557,1558],{"class":1341},"$SELLFORGE_PUBLIC_KEY",[1336,1560,1561],{"class":1354},"\"",[1336,1563,1547],{"class":1347},[1336,1565,1566,1568,1571,1574,1576],{"class":1338,"line":224},[1336,1567,1552],{"class":1347},[1336,1569,1570],{"class":1354}," \"Origin: ",[1336,1572,1573],{"class":1341},"$STOREFRONT_ORIGIN",[1336,1575,1561],{"class":1354},[1336,1577,1547],{"class":1347},[1336,1579,1580,1582,1585],{"class":1338,"line":1373},[1336,1581,1552],{"class":1347},[1336,1583,1584],{"class":1354}," 'Content-Type: application\u002Fjson'",[1336,1586,1547],{"class":1347},[1336,1588,1589,1592],{"class":1338,"line":1386},[1336,1590,1591],{"class":1347},"  -d",[1336,1593,1594],{"class":1354}," '{\n",[1336,1596,1597],{"class":1338,"line":1399},[1336,1598,1599],{"class":1354},"    \"plan_id\": \"spl_example\",\n",[1336,1601,1602],{"class":1338,"line":1412},[1336,1603,1604],{"class":1354},"    \"return_url\": \"https:\u002F\u002Fshop.example.com\u002Fcheckout\u002Fresult\",\n",[1336,1606,1607],{"class":1338,"line":1425},[1336,1608,1609],{"class":1354},"    \"customer_email\": \"alex@example.com\"\n",[1336,1611,1612],{"class":1338,"line":1436},[1336,1613,1614],{"class":1354},"  }'\n",[1118,1616,1617],{},"The plan must be active and its digital product published. Obtain the real plan ID through the administrative plan configuration; there is currently no public plan-list endpoint in this module.",[1118,1619,1620,1621,1624,1625,1145,1628,1145,1631,1634,1635,1638,1639,1642,1643,1646],{},"The response is ",[1122,1622,1623],{},"{ \"id\": \"cs_example\", \"url\": \"https:\u002F\u002Fcheckout.sellforge.ai\u002Fc\u002Fcs_example\" }",". Save the session association and redirect to the returned URL. A subscription session always uses the hosted flow. Its creation schema accepts ",[1122,1626,1627],{},"plan_id",[1122,1629,1630],{},"return_url",[1122,1632,1633],{},"customer_email",", and optional ",[1122,1636,1637],{},"affiliate_code","; it does ",[1503,1640,1641],{},"not"," accept the one-time checkout's nested ",[1122,1644,1645],{},"customer"," address prefill. Billing details are collected in the hosted checkout.",[1118,1648,1649,1651],{},[1122,1650,1637],{}," attributes the subscription to a partner; introductory discounts belong to the plan. Subscription checkout does not expose a separate discount-code input or the one-time express shortcut. During a free trial, the amount charged today can be zero while a payment method is collected for later billing.",[1155,1653,523],{"id":1654},"synchronize-access",[1118,1656,1657,1658,1661,1662,1665,1666,1669,1670,1673,1674,1145,1677,1145,1680,1145,1683,1145,1686,1149,1689,1138],{},"Use the ",[1135,1659,1660],{"href":803},"signed subscription events"," sent to the shop's checkout webhook destination. ",[1122,1663,1664],{},"checkout.completed"," can carry a ",[1122,1667,1668],{},"subscription_id"," without an ",[1122,1671,1672],{},"order_id"," during a trial. Ongoing lifecycle events use ",[1122,1675,1676],{},"abo.aktiv",[1122,1678,1679],{},"abo.bezahlt",[1122,1681,1682],{},"abo.gekuendigt",[1122,1684,1685],{},"abo.fortgesetzt",[1122,1687,1688],{},"abo.beendet",[1122,1690,1691],{},"abo.zahlung_fehlgeschlagen",[1118,1693,1694,1695,1697,1698,1700,1701,1704],{},"Persist the subscription ID and current period in your application. A paid subscription invoice creates an order with ",[1122,1696,1668],{},"; the ",[1122,1699,1679],{}," event identifies that order. Keep ",[1122,1702,1703],{},"is_test"," subscriptions separate from real customer access and make repeated event deliveries idempotent.",[1155,1706,528],{"id":1707},"administration-and-cancellation",[1246,1709,1710,1721],{},[1249,1711,1712],{},[1252,1713,1714,1716,1718],{},[1255,1715,1257],{},[1255,1717,1260],{},[1255,1719,1720],{},"Purpose",[1265,1722,1723,1739,1753,1773,1798],{},[1252,1724,1725,1729,1734],{},[1270,1726,1727],{},[1122,1728,1274],{},[1270,1730,1731],{},[1122,1732,1733],{},"\u002Fapi\u002Fadmin\u002Fdigital\u002Fabos",[1270,1735,1736,1737],{},"List up to 500 subscriptions in ",[1122,1738,1148],{},[1252,1740,1741,1745,1750],{},[1270,1742,1743],{},[1122,1744,1274],{},[1270,1746,1747],{},[1122,1748,1749],{},"\u002Fapi\u002Fadmin\u002Fdigital\u002Fabos\u002Fstatistik",[1270,1751,1752],{},"Subscription statistics",[1252,1754,1755,1759,1764],{},[1270,1756,1757],{},[1122,1758,1274],{},[1270,1760,1761],{},[1122,1762,1763],{},"\u002Fapi\u002Fadmin\u002Fdigital\u002Fabos\u002F:id",[1270,1765,1766,1769,1770],{},[1122,1767,1768],{},"abo"," plus related ",[1122,1771,1772],{},"bestellungen",[1252,1774,1775,1779,1784],{},[1270,1776,1777],{},[1122,1778,1291],{},[1270,1780,1781],{},[1122,1782,1783],{},"\u002Fapi\u002Fadmin\u002Fdigital\u002Fabos\u002F:id\u002Fkuendigen",[1270,1785,1786,1787,1790,1791,1794,1795],{},"Cancel; accepts ",[1122,1788,1789],{},"sofort"," (default ",[1122,1792,1793],{},"false",") and optional ",[1122,1796,1797],{},"grund",[1252,1799,1800,1804,1809],{},[1270,1801,1802],{},[1122,1803,1291],{},[1270,1805,1806],{},[1122,1807,1808],{},"\u002Fapi\u002Fadmin\u002Fdigital\u002Fabos\u002F:id\u002Ffortsetzen",[1270,1810,1811],{},"Withdraw a scheduled cancellation",[1118,1813,1814,1815,1145,1817,1145,1820,1823,1824,1145,1827,1145,1830,1833,1834,1823,1837,1145,1840,1145,1843,1145,1846,1145,1849,1145,1852,1854,1855,1145,1858,1149,1861,1864],{},"List filters include ",[1122,1816,1627],{},[1122,1818,1819],{},"q",[1122,1821,1822],{},"test"," (",[1122,1825,1826],{},"ja",[1122,1828,1829],{},"nein",[1122,1831,1832],{},"alle","), and ",[1122,1835,1836],{},"status",[1122,1838,1839],{},"laufend",[1122,1841,1842],{},"testzeit",[1122,1844,1845],{},"zahlung_offen",[1122,1847,1848],{},"gekuendigt",[1122,1850,1851],{},"beendet",[1122,1853,1832],{},"). Response subscription states use provider-style values such as ",[1122,1856,1857],{},"active",[1122,1859,1860],{},"trialing",[1122,1862,1863],{},"past_due",", rather than those list-filter names.",[1118,1866,1867,1868,1471,1871,1874,1875,1878,1879,1471,1882,1138],{},"Customer-account routes are ",[1122,1869,1870],{},"GET \u002Fapi\u002Fcustomer\u002Fabos",[1122,1872,1873],{},"POST \u002Fapi\u002Fcustomer\u002Fabos\u002F:id\u002Fkuendigen",", served by the shop backend with a customer session. Email management links use the token-protected ",[1122,1876,1877],{},"\u002Fapi\u002Fcheckout\u002Fabo\u002F:token"," routes on the checkout backend, including POST ",[1122,1880,1881],{},"\u002F:token\u002Fkuendigen",[1122,1883,1884],{},"\u002F:token\u002Ffortsetzen",[1118,1886,1887,1888,1145,1891,1149,1894,1897],{},"Customer cancellations take effect at the end of the paid period. Read ",[1122,1889,1890],{},"cancel_at_period_end",[1122,1892,1893],{},"current_period_end",[1122,1895,1896],{},"endet_am","; a cancellation request is not the same as immediate loss of access.",[1899,1900,1901],"style",{},"html pre.shiki code .sgsFI, html code.shiki .sgsFI{--shiki-default:#24292E}html pre.shiki code .sYu0t, html code.shiki .sYu0t{--shiki-default:#005CC5}html pre.shiki code .sYBdl, html code.shiki .sYBdl{--shiki-default:#032F62}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .s7eDp, html code.shiki .s7eDp{--shiki-default:#6F42C1}",{"title":1332,"searchDepth":185,"depth":224,"links":1903},[1904,1905,1906,1907,1908,1909],{"id":1157,"depth":185,"text":503},{"id":1193,"depth":185,"text":508},{"id":1241,"depth":185,"text":513},{"id":1498,"depth":185,"text":518},{"id":1654,"depth":185,"text":523},{"id":1707,"depth":185,"text":528},"md",{},true,"2026-10-19",{"title":86,"description":87},[1916,1917,1918,1919,1920,1921,1922,1923,1924],"api\u002Fsrc\u002Fdigital\u002Fdigital.routes.ts","api\u002Fsrc\u002Fdigital\u002Fdigital-modul.ts","api\u002Fsrc\u002Fdigital\u002Fabo-checkout.ts","api\u002Fsrc\u002Fdigital\u002Fabo.service.ts","api\u002Fsrc\u002Fcatalog\u002Fproducts.routes.ts","api\u002Fsrc\u002Forders\u002Fcheckout.routes.ts","api\u002Fsrc\u002Fjobs\u002Fjob-handlers.ts","api\u002Fsrc\u002Flib\u002Fapi-token-policy.ts","api\u002Fsrc\u002Fbackends.ts","api\u002Fdigital-and-subscriptions","UJ6TZvyMlu6Y0TTLJNWsiiGohESBrxT2xElVH8RJh1I",1792422314746]