[{"data":1,"prerenderedAt":1169},["ShallowReactive",2],{"docs-navigation":3,"docs-search":157,"page:\u002Fapi\u002Foverview":899},[4,11,17,23,30,36,42,48,54,60,66,72,78,84,90,96,102,108,114,121,127,133,139,145,151],{"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\u002Forders","Orders","Read order lists and detail records for your shop.",140,"api.orders",{"path":61,"title":62,"description":63,"group":27,"order":64,"docId":65},"\u002Fapi\u002Fcustomers","Customers","Search customer accounts and retrieve their order history.",145,"api.customers",{"path":67,"title":68,"description":69,"group":27,"order":70,"docId":71},"\u002Fapi\u002Fstorefront","Storefront","Identify your shop and retrieve the customer-facing product catalog.",150,"api.storefront",{"path":73,"title":74,"description":75,"group":27,"order":76,"docId":77},"\u002Fapi\u002Fcheckout-sessions","Checkout sessions","Create a hosted checkout and connect payment completion to your storefront.",160,"api.checkout-sessions",{"path":79,"title":80,"description":81,"group":27,"order":82,"docId":83},"\u002Fapi\u002Fwebhooks","Webhooks","Receive and verify signed notifications from the hosted checkout.",170,"api.webhooks",{"path":85,"title":86,"description":87,"group":27,"order":88,"docId":89},"\u002Fapi\u002Finventory-and-fulfillment","Inventory and fulfillment","Work with warehouses, stock, picking, returns, transfers, and procurement.",175,"api.inventory-and-fulfillment",{"path":91,"title":92,"description":93,"group":27,"order":94,"docId":95},"\u002Fapi\u002Ffinance-and-partners","Finance and partners","Find invoice, credit-note, vendor, and affiliate operations.",180,"api.finance-and-partners",{"path":97,"title":98,"description":99,"group":27,"order":100,"docId":101},"\u002Fapi\u002Fdata-and-media","Data and media","Import and export CSV data and upload images for your catalog.",185,"api.data-and-media",{"path":103,"title":104,"description":105,"group":27,"order":106,"docId":107},"\u002Fapi\u002Fcontent-and-email","Content and email","Manage CMS pages, newsletter subscribers, campaigns, and custom email content.",190,"api.content-and-email",{"path":109,"title":110,"description":111,"group":27,"order":112,"docId":113},"\u002Fapi\u002Fadministration","Administration","Inspect settings, credentials, jobs, audit events, and team access.",195,"api.administration",{"path":115,"title":116,"description":117,"group":118,"order":119,"docId":120},"\u002Fplugins\u002Fintroduction","Plugin overview","Understand how plugins extend the Sellforge kernel.","plugins",200,"plugins.introduction",{"path":122,"title":123,"description":124,"group":118,"order":125,"docId":126},"\u002Fplugins\u002Fquickstart","Your first plugin","Create a minimal plugin that listens for a new order.",210,"plugins.quickstart",{"path":128,"title":129,"description":130,"group":118,"order":131,"docId":132},"\u002Fplugins\u002Fmanifest","Package manifest","Describe your plugin and declare settings in package.json.",220,"plugins.manifest",{"path":134,"title":135,"description":136,"group":118,"order":137,"docId":138},"\u002Fplugins\u002Fhooks","Lifecycle hooks","Subscribe to order events inside the Sellforge kernel.",230,"plugins.hooks",{"path":140,"title":141,"description":142,"group":118,"order":143,"docId":144},"\u002Fplugins\u002Fcontext","Context and services","Use the services provided to your plugin's register function.",240,"plugins.context",{"path":146,"title":147,"description":148,"group":118,"order":149,"docId":150},"\u002Fplugins\u002Fmigrations","Database migrations","Create plugin-owned tables with versioned SQL migrations.",250,"plugins.migrations",{"path":152,"title":153,"description":154,"group":118,"order":155,"docId":156},"\u002Fplugins\u002Finstallation","Installation and lifecycle","Install, enable, inspect, and unload a plugin through the administrative API.",260,"plugins.installation",[158,162,168,173,178,183,186,191,196,202,207,212,217,220,225,230,235,240,245,250,252,257,262,267,272,277,282,285,290,295,300,305,307,312,317,322,327,332,334,339,344,349,354,356,361,366,371,374,379,384,389,394,399,404,409,414,419,424,429,432,437,442,447,452,457,462,467,472,474,479,484,489,494,499,502,507,512,517,522,527,529,534,539,544,549,554,559,564,566,571,576,581,586,589,594,599,604,609,614,617,622,627,632,637,642,645,650,655,660,665,670,675,680,683,688,693,698,703,708,710,715,720,725,730,735,737,742,747,752,757,762,764,769,774,779,784,789,794,797,802,807,812,817,822,824,829,834,839,844,846,851,856,861,866,871,874,879,884,889,894],{"id":109,"title":110,"titles":159,"content":160,"level":161},[],"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":163,"title":164,"titles":165,"content":166,"level":167},"\u002Fapi\u002Fadministration#settings-and-credentials","Settings and credentials",[110],"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":169,"title":170,"titles":171,"content":172,"level":167},"\u002Fapi\u002Fadministration#jobs","Jobs",[110],"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":174,"title":175,"titles":176,"content":177,"level":167},"\u002Fapi\u002Fadministration#audit-log","Audit log",[110],"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":179,"title":180,"titles":181,"content":182,"level":167},"\u002Fapi\u002Fadministration#users-and-team-members","Users and team members",[110],"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":31,"title":32,"titles":184,"content":185,"level":161},[],"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":187,"title":188,"titles":189,"content":190,"level":167},"\u002Fapi\u002Fauthentication#credential-types","Credential types",[32],"Type Intended use Header Scoped API token (sfk_…) Server-side integrations and machine access Authorization: Bearer sfk_… 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":192,"title":193,"titles":194,"content":195,"level":167},"\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 a session JWT. 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":197,"title":198,"titles":199,"content":200,"level":201},"\u002Fapi\u002Fauthentication#read-scopes","Read scopes",[32,193],"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.",3,{"id":203,"title":204,"titles":205,"content":206,"level":167},"\u002Fapi\u002Fauthentication#user-login","User login",[32],"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":208,"title":209,"titles":210,"content":211,"level":201},"\u002Fapi\u002Fauthentication#two-factor-authentication","Two-factor authentication",[32,204],"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. Authentication endpoints apply a limit of 10 requests per minute per IP.",{"id":213,"title":214,"titles":215,"content":216,"level":167},"\u002Fapi\u002Fauthentication#storefront-authentication","Storefront authentication",[32],"curl \"$SELLFORGE_STORE_URL\u002Fapi\u002Fstore\u002Fproducts\" \\\n  -H \"Authorization: Bearer $SELLFORGE_PUBLIC_KEY\" The publishable key selects the tenant. 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":218,"content":219,"level":161},[],"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":221,"title":222,"titles":223,"content":224,"level":167},"\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, and icon. 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":226,"title":227,"titles":228,"content":229,"level":167},"\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":231,"title":232,"titles":233,"content":234,"level":167},"\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":236,"title":237,"titles":238,"content":239,"level":167},"\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":241,"title":242,"titles":243,"content":244,"level":167},"\u002Fapi\u002Fcatalog-resources#discounts","Discounts",[50],"Use an admin session for these endpoints: 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":246,"title":247,"titles":248,"content":249,"level":167},"\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. 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":73,"title":74,"titles":251,"content":75,"level":161},[],{"id":253,"title":254,"titles":255,"content":256,"level":167},"\u002Fapi\u002Fcheckout-sessions#choose-a-checkout-mode","Choose a checkout mode",[74],"The shop's checkout setting determines who collects customer details and handles payment: Mode Address and shipping Payment hosted Sellforge checkout Sellforge checkout payment Your storefront 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.",{"id":258,"title":259,"titles":260,"content":261,"level":167},"\u002Fapi\u002Fcheckout-sessions#create-a-hosted-session","Create a hosted session",[74],"POST \u002Fapi\u002Fstore\u002Fcheckout-sessions Call the endpoint using your shop's publishable key. The following example assumes hosted mode: curl \"$SELLFORGE_CHECKOUT_URL\u002Fapi\u002Fstore\u002Fcheckout-sessions\" \\\n  -H \"Authorization: Bearer $SELLFORGE_PUBLIC_KEY\" \\\n  -H 'Content-Type: application\u002Fjson' \\\n  -d '{\n    \"items\": [\n      { \"product_id\": \"prod_example\", \"quantity\": 2 }\n    ],\n    \"return_url\": \"https:\u002F\u002Fshop.example.com\u002Fcheckout\u002Fresult\",\n    \"customer_email\": \"customer@example.com\"\n  }' Replace prod_example with a real product ID. 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. Sellforge calculates prices; the request does not accept caller-supplied item prices.",{"id":263,"title":264,"titles":265,"content":266,"level":201},"\u002Fapi\u002Fcheckout-sessions#response","Response",[74,259],"{\n  \"id\": \"cs_example\",\n  \"url\": \"https:\u002F\u002Fcheckout.example.com\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.",{"id":268,"title":269,"titles":270,"content":271,"level":167},"\u002Fapi\u002Fcheckout-sessions#payment-only-mode","Payment-only mode",[74],"In payment mode, the storefront supplies the customer object. A shipping_address is required. The object also accepts email, customer_id, billing_address, shipping_method, and discount_code. Address and shipping information are used to calculate tax and delivery charges. Supply the applicable shipping method for the order. The server remains responsible for totals and discount validation.",{"id":273,"title":274,"titles":275,"content":276,"level":167},"\u002Fapi\u002Fcheckout-sessions#session-lifecycle","Session lifecycle",[74],"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. 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.",{"id":278,"title":279,"titles":280,"content":281,"level":167},"\u002Fapi\u002Fcheckout-sessions#confirm-the-outcome","Confirm the outcome",[74],"A browser redirect alone is not an authoritative payment confirmation. Use the signed checkout notification and the order state on the backend to decide when to fulfill an order. 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":103,"title":104,"titles":283,"content":284,"level":161},[],"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":286,"title":287,"titles":288,"content":289,"level":167},"\u002Fapi\u002Fcontent-and-email#cms-pages","CMS pages",[104],"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":291,"title":292,"titles":293,"content":294,"level":167},"\u002Fapi\u002Fcontent-and-email#newsletter-subscribers","Newsletter subscribers",[104],"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. The legacy public subscription and tracking handlers use \u002Fstore\u002Fnewsletter..., without \u002Fapi. The split backend allowlists do not currently expose this prefix. Confirm the configured public routing before linking a signup form to those handlers.",{"id":296,"title":297,"titles":298,"content":299,"level":167},"\u002Fapi\u002Fcontent-and-email#campaigns","Campaigns",[104],"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":301,"title":302,"titles":303,"content":304,"level":167},"\u002Fapi\u002Fcontent-and-email#custom-emails-and-attachments","Custom emails and attachments",[104],"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. 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":306,"content":39,"level":161},[],{"id":308,"title":309,"titles":310,"content":311,"level":167},"\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":313,"title":314,"titles":315,"content":316,"level":167},"\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. Order line items preserve pricing and tax snapshots. Compute displayed totals from server-provided values, especially during checkout.",{"id":318,"title":319,"titles":320,"content":321,"level":167},"\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":323,"title":324,"titles":325,"content":326,"level":167},"\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":328,"title":329,"titles":330,"content":331,"level":167},"\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":61,"title":62,"titles":333,"content":63,"level":161},[],{"id":335,"title":336,"titles":337,"content":338,"level":167},"\u002Fapi\u002Fcustomers#list-customers","List customers",[62],"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":340,"title":341,"titles":342,"content":343,"level":167},"\u002Fapi\u002Fcustomers#retrieve-a-customer","Retrieve a customer",[62],"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":345,"title":346,"titles":347,"content":348,"level":167},"\u002Fapi\u002Fcustomers#order-history","Order history",[62],"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":350,"title":351,"titles":352,"content":353,"level":167},"\u002Fapi\u002Fcustomers#imports-and-exports","Imports and exports",[62],"Customer CSV templates, exports, and background imports are covered in Data and media. There is no customers:write token scope in the reviewed policy. 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":97,"title":98,"titles":355,"content":99,"level":161},[],{"id":357,"title":358,"titles":359,"content":360,"level":167},"\u002Fapi\u002Fdata-and-media#csv","CSV",[98],"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":362,"title":363,"titles":364,"content":365,"level":167},"\u002Fapi\u002Fdata-and-media#media-uploads","Media uploads",[98],"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":367,"title":368,"titles":369,"content":370,"level":167},"\u002Fapi\u002Fdata-and-media#content-types","Content types",[98],"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":91,"title":92,"titles":372,"content":373,"level":161},[],"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":375,"title":376,"titles":377,"content":378,"level":167},"\u002Fapi\u002Ffinance-and-partners#invoices","Invoices",[92],"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.",{"id":380,"title":381,"titles":382,"content":383,"level":167},"\u002Fapi\u002Ffinance-and-partners#credit-notes","Credit notes",[92],"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":385,"title":386,"titles":387,"content":388,"level":167},"\u002Fapi\u002Ffinance-and-partners#sevdesk","SevDesk",[92],"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":390,"title":391,"titles":392,"content":393,"level":167},"\u002Fapi\u002Ffinance-and-partners#vendors","Vendors",[92],"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":395,"title":396,"titles":397,"content":398,"level":201},"\u002Fapi\u002Ffinance-and-partners#vendor-earnings","Vendor earnings",[92,391],"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":400,"title":401,"titles":402,"content":403,"level":201},"\u002Fapi\u002Ffinance-and-partners#vendor-payouts","Vendor payouts",[92,391],"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":405,"title":406,"titles":407,"content":408,"level":201},"\u002Fapi\u002Ffinance-and-partners#vendor-statements","Vendor statements",[92,391],"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":410,"title":411,"titles":412,"content":413,"level":201},"\u002Fapi\u002Ffinance-and-partners#vendor-portal","Vendor portal",[92,391],"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":415,"title":416,"titles":417,"content":418,"level":167},"\u002Fapi\u002Ffinance-and-partners#affiliates","Affiliates",[92],"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":420,"title":421,"titles":422,"content":423,"level":201},"\u002Fapi\u002Ffinance-and-partners#affiliate-earnings-and-payouts","Affiliate earnings and payouts",[92,416],"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":425,"title":426,"titles":427,"content":428,"level":201},"\u002Fapi\u002Ffinance-and-partners#affiliate-portal","Affiliate portal",[92,416],"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":85,"title":86,"titles":430,"content":431,"level":161},[],"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":433,"title":434,"titles":435,"content":436,"level":167},"\u002Fapi\u002Finventory-and-fulfillment#warehouses","Warehouses",[86],"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":438,"title":439,"titles":440,"content":441,"level":167},"\u002Fapi\u002Finventory-and-fulfillment#inventory","Inventory",[86],"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":443,"title":444,"titles":445,"content":446,"level":167},"\u002Fapi\u002Finventory-and-fulfillment#fulfillments","Fulfillments",[86],"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":448,"title":449,"titles":450,"content":451,"level":167},"\u002Fapi\u002Finventory-and-fulfillment#returns","Returns",[86],"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":453,"title":454,"titles":455,"content":456,"level":167},"\u002Fapi\u002Finventory-and-fulfillment#transfers","Transfers",[86],"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":458,"title":459,"titles":460,"content":461,"level":167},"\u002Fapi\u002Finventory-and-fulfillment#production-orders","Production orders",[86],"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":463,"title":464,"titles":465,"content":466,"level":167},"\u002Fapi\u002Finventory-and-fulfillment#purchase-orders","Purchase orders",[86],"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":468,"title":469,"titles":470,"content":471,"level":167},"\u002Fapi\u002Finventory-and-fulfillment#shipping-and-tracking","Shipping and tracking",[86],"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. 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":55,"title":56,"titles":473,"content":57,"level":161},[],{"id":475,"title":476,"titles":477,"content":478,"level":167},"\u002Fapi\u002Forders#list-orders","List orders",[56],"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":480,"title":481,"titles":482,"content":483,"level":201},"\u002Fapi\u002Forders#query-parameters","Query parameters",[56,476],"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":485,"title":486,"titles":487,"content":488,"level":167},"\u002Fapi\u002Forders#retrieve-an-order","Retrieve an order",[56],"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.",{"id":490,"title":491,"titles":492,"content":493,"level":167},"\u002Fapi\u002Forders#historical-pricing","Historical pricing",[56],"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":495,"title":496,"titles":497,"content":498,"level":167},"\u002Fapi\u002Forders#write-operations","Write operations",[56],"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":500,"content":501,"level":161},[],"Find the right API surface for your application. Sellforge exposes HTTP endpoints grouped by domain. JSON endpoints accept Content-Type: application\u002Fjson; authentication is typically sent using the Authorization header.",{"id":503,"title":504,"titles":505,"content":506,"level":167},"\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 Hosted checkout \u002Fapi\u002Fcheckout\u002F:id Checkout session flow Plugin management \u002Fapi\u002Fadmin\u002Fplugins Admin session; mutations require platform context",{"id":508,"title":509,"titles":510,"content":511,"level":167},"\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 standard local ports are 9201, 9203, and 9204 respectively. Confirm deployed URLs with your platform operator.",{"id":513,"title":514,"titles":515,"content":516,"level":167},"\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":518,"title":519,"titles":520,"content":521,"level":167},"\u002Fapi\u002Foverview#resources-covered-here","Resources covered here",[25],"Products: catalog queries and product writes. 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.",{"id":523,"title":524,"titles":525,"content":526,"level":167},"\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":528,"content":45,"level":161},[],{"id":530,"title":531,"titles":532,"content":533,"level":167},"\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":535,"title":536,"titles":537,"content":538,"level":201},"\u002Fapi\u002Fproducts#filters","Filters",[44,531],"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 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.",{"id":540,"title":541,"titles":542,"content":543,"level":167},"\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":545,"title":546,"titles":547,"content":548,"level":201},"\u002Fapi\u002Fproducts#common-fields","Common fields",[44,541],"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",{"id":550,"title":551,"titles":552,"content":553,"level":167},"\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.",{"id":555,"title":556,"titles":557,"content":558,"level":167},"\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.",{"id":560,"title":561,"titles":562,"content":563,"level":167},"\u002Fapi\u002Fproducts#storefront-visibility","Storefront visibility",[44],"The public listing is GET \u002Fapi\u002Fstore\u002Fproducts. Publication, tenant, channel listing, and visibility rules determine what shoppers can see. 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":67,"title":68,"titles":565,"content":69,"level":161},[],{"id":567,"title":568,"titles":569,"content":570,"level":167},"\u002Fapi\u002Fstorefront#identify-the-shop","Identify the shop",[68],"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\" The key identifies the tenant. Origin restrictions, when configured, are checked in addition to the key. Configure the domains your actual storefront uses.",{"id":572,"title":573,"titles":574,"content":575,"level":167},"\u002Fapi\u002Fstorefront#read-the-public-catalog","Read the public catalog",[68],"GET \u002Fapi\u002Fstore\u002Fproducts returns a products array. The listing applies shop and publication rules and resolves the current sales channel. Products must be listed and visible in that channel to appear. A product being visible to an administrator does not by itself make it visible to customers. 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 is published and sellable. The storefront resolves to the intended channel. The product is listed and visible in that channel.",{"id":577,"title":578,"titles":579,"content":580,"level":167},"\u002Fapi\u002Fstorefront#use-the-correct-backend","Use the correct backend",[68],"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":582,"title":583,"titles":584,"content":585,"level":167},"\u002Fapi\u002Fstorefront#continue-to-checkout","Continue to checkout",[68],"Use product and variant IDs from your shop's catalog to create a checkout session. 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":79,"title":80,"titles":587,"content":588,"level":161},[],"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":590,"title":591,"titles":592,"content":593,"level":167},"\u002Fapi\u002Fwebhooks#configure-delivery","Configure delivery",[80],"The tenant settings are tenant_\u003Cid>.checkout.webhook_url and tenant_\u003Cid>.checkout.webhook_secret. Configure them through the supported checkout settings for your deployment. Delivery requires an HTTPS URL reachable by the server. The delivery worker does not follow redirects.",{"id":595,"title":596,"titles":597,"content":598,"level":167},"\u002Fapi\u002Fwebhooks#events","Events",[80],"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    \"status\": \"paid\",\n    \"amount_cents\": 2490,\n    \"currency\": \"EUR\",\n    \"reason\": null\n  }\n} The request includes X-Sellforge-Event-Id and X-Sellforge-Signature headers.",{"id":600,"title":601,"titles":602,"content":603,"level":167},"\u002Fapi\u002Fwebhooks#verify-the-signature","Verify the signature",[80],"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":605,"title":606,"titles":607,"content":608,"level":167},"\u002Fapi\u002Fwebhooks#handle-retries","Handle retries",[80],"New checkout delivery jobs allow up to five attempts. The event ID remains the same between retries; the delivery timestamp and signature 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":610,"title":611,"titles":612,"content":613,"level":167},"\u002Fapi\u002Fwebhooks#other-event-mechanisms","Other event mechanisms",[80],"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":615,"content":616,"level":161},[],"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":618,"title":619,"titles":620,"content":621,"level":167},"\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 React to order events inside Sellforge Plugin quickstart",{"id":623,"title":624,"titles":625,"content":626,"level":167},"\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":628,"title":629,"titles":630,"content":631,"level":167},"\u002Fgetting-started\u002Fintroduction#how-the-backends-fit-together","How the backends fit together",[6],"The kernel is shared by several backend processes. Each process exposes a specific set of routes: 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":633,"title":634,"titles":635,"content":636,"level":167},"\u002Fgetting-started\u002Fintroduction#about-this-reference","About this reference",[6],"These guides describe the checked-in kernel at commit fe5c475, reviewed on October 17, 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":638,"title":639,"titles":640,"content":641,"level":167},"\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":643,"content":644,"level":161},[],"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":646,"title":647,"titles":648,"content":649,"level":167},"\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":651,"title":652,"titles":653,"content":654,"level":167},"\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 session JWTs, scoped sfk_ tokens, and public pk_sf_ keys. Login with two-factor authentication uses a separate verification step. 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":656,"title":657,"titles":658,"content":659,"level":167},"\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":661,"title":662,"titles":663,"content":664,"level":167},"\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":666,"title":667,"titles":668,"content":669,"level":167},"\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":671,"title":672,"titles":673,"content":674,"level":167},"\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":676,"title":677,"titles":678,"content":679,"level":167},"\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":681,"content":682,"level":161},[],"Make your first authenticated request to the Sellforge product API. This walkthrough reads your product catalog without changing shop data.",{"id":684,"title":685,"titles":686,"content":687,"level":167},"\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 shop-admin backend, the usual base URL is http:\u002F\u002Flocalhost:9201. Use your deployment's configured URL when connecting to a hosted environment.",{"id":689,"title":690,"titles":691,"content":692,"level":167},"\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:9201'\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":694,"title":695,"titles":696,"content":697,"level":167},"\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":699,"title":700,"titles":701,"content":702,"level":167},"\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.",{"id":704,"title":705,"titles":706,"content":707,"level":167},"\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":140,"title":141,"titles":709,"content":142,"level":161},[],{"id":711,"title":712,"titles":713,"content":714,"level":167},"\u002Fplugins\u002Fcontext#context-overview","Context overview",[141],"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":716,"title":717,"titles":718,"content":719,"level":167},"\u002Fplugins\u002Fcontext#read-settings-and-log-an-event","Read settings and log an event",[141],"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":721,"title":722,"titles":723,"content":724,"level":167},"\u002Fplugins\u002Fcontext#schedule-periodic-work","Schedule periodic work",[141],"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":726,"title":727,"titles":728,"content":729,"level":167},"\u002Fplugins\u002Fcontext#database-and-storage","Database and storage",[141],"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":731,"title":732,"titles":733,"content":734,"level":167},"\u002Fplugins\u002Fcontext#current-limitations","Current limitations",[141],"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":134,"title":135,"titles":736,"content":136,"level":161},[],{"id":738,"title":739,"titles":740,"content":741,"level":167},"\u002Fplugins\u002Fhooks#register-a-handler","Register a handler",[135],"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":743,"title":744,"titles":745,"content":746,"level":167},"\u002Fplugins\u002Fhooks#order-events","Order events",[135],"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.",{"id":748,"title":749,"titles":750,"content":751,"level":167},"\u002Fplugins\u002Fhooks#execution-behavior","Execution behavior",[135],"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":753,"title":754,"titles":755,"content":756,"level":167},"\u002Fplugins\u002Fhooks#plugin-isolation-and-cleanup","Plugin isolation and cleanup",[135],"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":758,"title":759,"titles":760,"content":761,"level":167},"\u002Fplugins\u002Fhooks#http-delivery","HTTP delivery",[135],"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":152,"title":153,"titles":763,"content":154,"level":161},[],{"id":765,"title":766,"titles":767,"content":768,"level":167},"\u002Fplugins\u002Finstallation#prepare-the-runtime","Prepare the runtime",[153],"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":770,"title":771,"titles":772,"content":773,"level":167},"\u002Fplugins\u002Finstallation#install-a-package","Install a package",[153],"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":775,"title":776,"titles":777,"content":778,"level":167},"\u002Fplugins\u002Finstallation#enable-and-disable","Enable and disable",[153],"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":780,"title":781,"titles":782,"content":783,"level":167},"\u002Fplugins\u002Finstallation#inspect-status","Inspect status",[153],"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":785,"title":786,"titles":787,"content":788,"level":167},"\u002Fplugins\u002Finstallation#startup-and-failures","Startup and failures",[153],"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":790,"title":791,"titles":792,"content":793,"level":167},"\u002Fplugins\u002Finstallation#uninstall","Uninstall",[153],"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":115,"title":116,"titles":795,"content":796,"level":161},[],"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":798,"title":799,"titles":800,"content":801,"level":167},"\u002Fplugins\u002Fintroduction#when-to-use-a-plugin","When to use a plugin",[116],"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":803,"title":804,"titles":805,"content":806,"level":167},"\u002Fplugins\u002Fintroduction#runtime-model","Runtime model",[116],"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":808,"title":809,"titles":810,"content":811,"level":167},"\u002Fplugins\u002Fintroduction#available-building-blocks","Available building blocks",[116],"Registration and lifecycle Package manifest Order lifecycle hooks Context services SQL migrations Installation and operations",{"id":813,"title":814,"titles":815,"content":816,"level":167},"\u002Fplugins\u002Fintroduction#implementation-boundary","Implementation boundary",[116],"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":818,"title":819,"titles":820,"content":821,"level":167},"\u002Fplugins\u002Fintroduction#start-building","Start building",[116],"The quickstart creates a minimal package using the same default-export registration shape as the kernel's own test fixture.",{"id":128,"title":129,"titles":823,"content":130,"level":161},[],{"id":825,"title":826,"titles":827,"content":828,"level":167},"\u002Fplugins\u002Fmanifest#required-package-structure","Required package structure",[129],"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":830,"title":831,"titles":832,"content":833,"level":167},"\u002Fplugins\u002Fmanifest#settings-metadata","Settings metadata",[129],"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":835,"title":836,"titles":837,"content":838,"level":167},"\u002Fplugins\u002Fmanifest#what-metadata-does-not-do","What metadata does not do",[129],"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":840,"title":841,"titles":842,"content":843,"level":167},"\u002Fplugins\u002Fmanifest#migration-location","Migration location",[129],"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":146,"title":147,"titles":845,"content":148,"level":161},[],{"id":847,"title":848,"titles":849,"content":850,"level":167},"\u002Fplugins\u002Fmigrations#migration-files","Migration files",[147],"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":852,"title":853,"titles":854,"content":855,"level":167},"\u002Fplugins\u002Fmigrations#plugin-schema","Plugin schema",[147],"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":857,"title":858,"titles":859,"content":860,"level":167},"\u002Fplugins\u002Fmigrations#migration-history","Migration history",[147],"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":862,"title":863,"titles":864,"content":865,"level":167},"\u002Fplugins\u002Fmigrations#sql-restrictions","SQL restrictions",[147],"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":867,"title":868,"titles":869,"content":870,"level":167},"\u002Fplugins\u002Fmigrations#uninstall-behavior","Uninstall behavior",[147],"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":122,"title":123,"titles":872,"content":873,"level":161},[],"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":875,"title":876,"titles":877,"content":878,"level":167},"\u002Fplugins\u002Fquickstart#create-the-package","Create the package",[123],"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":880,"title":881,"titles":882,"content":883,"level":167},"\u002Fplugins\u002Fquickstart#register-an-event-handler","Register an event handler",[123],"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.",{"id":885,"title":886,"titles":887,"content":888,"level":167},"\u002Fplugins\u002Fquickstart#test-the-registration-locally","Test the registration locally",[123],"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":890,"title":891,"titles":892,"content":893,"level":167},"\u002Fplugins\u002Fquickstart#load-it-in-a-development-kernel","Load it in a development kernel",[123],"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":895,"title":896,"titles":897,"content":898,"level":167},"\u002Fplugins\u002Fquickstart#next-steps","Next steps",[123],"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":900,"title":25,"body":901,"description":26,"docId":29,"extension":1158,"group":27,"meta":1159,"navigation":1160,"order":28,"path":24,"reviewed":1161,"seo":1162,"sources":1163,"stem":1167,"__hash__":1168},"docs\u002Fapi\u002Foverview.md",{"type":902,"value":903,"toc":1150},"minimark",[904,917,921,1024,1027,1030,1074,1088,1091,1094,1104,1107,1136,1139],[905,906,907,908,912,913,916],"p",{},"Sellforge exposes HTTP endpoints grouped by domain. JSON endpoints accept ",[909,910,911],"code",{},"Content-Type: application\u002Fjson","; authentication is typically sent using the ",[909,914,915],{},"Authorization"," header.",[918,919,504],"h2",{"id":920},"api-surfaces",[922,923,924,940],"table",{},[925,926,927],"thead",{},[928,929,930,934,937],"tr",{},[931,932,933],"th",{},"Surface",[931,935,936],{},"Typical routes",[931,938,939],{},"Credentials",[941,942,943,961,973,986,998,1011],"tbody",{},[928,944,945,949,958],{},[946,947,948],"td",{},"Merchant operations",[946,950,951,954,955],{},[909,952,953],{},"\u002Fapi\u002Fadmin\u002Fproducts",", ",[909,956,957],{},"\u002Fapi\u002Fadmin\u002Forders",[946,959,960],{},"Admin session or scoped API token",[928,962,963,966,970],{},[946,964,965],{},"Product writes",[946,967,968],{},[909,969,43],{},[946,971,972],{},"Authorized session or supported write-scoped token",[928,974,975,978,983],{},[946,976,977],{},"Public storefront",[946,979,980],{},[909,981,982],{},"\u002Fapi\u002Fstore\u002Fproducts",[946,984,985],{},"Shop publishable key",[928,987,988,991,996],{},[946,989,990],{},"Checkout creation",[946,992,993],{},[909,994,995],{},"\u002Fapi\u002Fstore\u002Fcheckout-sessions",[946,997,985],{},[928,999,1000,1003,1008],{},[946,1001,1002],{},"Hosted checkout",[946,1004,1005],{},[909,1006,1007],{},"\u002Fapi\u002Fcheckout\u002F:id",[946,1009,1010],{},"Checkout session flow",[928,1012,1013,1016,1021],{},[946,1014,1015],{},"Plugin management",[946,1017,1018],{},[909,1019,1020],{},"\u002Fapi\u002Fadmin\u002Fplugins",[946,1022,1023],{},"Admin session; mutations require platform context",[918,1025,509],{"id":1026},"base-urls",[905,1028,1029],{},"Your deployment can serve these APIs on different hosts. Examples use these variables:",[922,1031,1032,1042],{},[925,1033,1034],{},[928,1035,1036,1039],{},[931,1037,1038],{},"Variable",[931,1040,1041],{},"Backend",[941,1043,1044,1054,1064],{},[928,1045,1046,1051],{},[946,1047,1048],{},[909,1049,1050],{},"SELLFORGE_ADMIN_URL",[946,1052,1053],{},"Shop-admin API",[928,1055,1056,1061],{},[946,1057,1058],{},[909,1059,1060],{},"SELLFORGE_STORE_URL",[946,1062,1063],{},"Public shop API",[928,1065,1066,1071],{},[946,1067,1068],{},[909,1069,1070],{},"SELLFORGE_CHECKOUT_URL",[946,1072,1073],{},"Checkout API",[905,1075,1076,1077,954,1080,1083,1084,1087],{},"The standard local ports are ",[909,1078,1079],{},"9201",[909,1081,1082],{},"9203",", and ",[909,1085,1086],{},"9204"," respectively. Confirm deployed URLs with your platform operator.",[918,1089,514],{"id":1090},"authentication-and-permissions",[905,1092,1093],{},"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.",[905,1095,1096,1097,1100,1101,1103],{},"Start with ",[1098,1099,32],"a",{"href":31},", then read ",[1098,1102,38],{"href":37},".",[918,1105,519],{"id":1106},"resources-covered-here",[1108,1109,1110,1116,1121,1126,1131],"ul",{},[1111,1112,1113,1115],"li",{},[1098,1114,44],{"href":43},": catalog queries and product writes.",[1111,1117,1118,1120],{},[1098,1119,56],{"href":55},": order lists, details, and status filtering.",[1111,1122,1123,1125],{},[1098,1124,68],{"href":67},": shop identification and channel-aware product visibility.",[1111,1127,1128,1130],{},[1098,1129,74],{"href":73},": create a checkout and return the customer to your shop.",[1111,1132,1133,1135],{},[1098,1134,80],{"href":79},": signed checkout notifications.",[918,1137,524],{"id":1138},"versioning",[905,1140,1141,1142,1145,1146,1149],{},"The current routes use ",[909,1143,1144],{},"\u002Fapi\u002F...",", without a ",[909,1147,1148],{},"\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.",{"title":1151,"searchDepth":167,"depth":201,"links":1152},"",[1153,1154,1155,1156,1157],{"id":920,"depth":167,"text":504},{"id":1026,"depth":167,"text":509},{"id":1090,"depth":167,"text":514},{"id":1106,"depth":167,"text":519},{"id":1138,"depth":167,"text":524},"md",{},true,"2026-10-17",{"title":25,"description":26},[1164,1165,1166],"kernel\u002Fsrc\u002Fbackends.ts","kernel\u002Fsrc\u002Fmiddleware\u002Fapi-token.ts","kernel\u002Fsrc\u002Fmiddleware\u002Fstore-auth.ts","api\u002Foverview","4pbXZJgva5PFcYtC-eMDvhsekphUKGTU18aj8pMup6Y",1792252769002]